← Quay lại blog

Proxy trong Rust reqwest: cấu hình SOCKS5, pool khách hàng và xoay vòng IP cho trình phân tích dữ liệu

Phân tích cách kết nối SOCKS5-proxy trong Rust reqwest, xây dựng pool khách hàng và thực hiện xoay vòng IP để thu thập dữ liệu ổn định mà không bị chặn.

📅18 tháng 10, 2026

Bất kỳ trình phân tích nào hoạt động lâu hơn năm phút trên các trang web thực tế đều sớm hay muộn gặp phải vấn đề bị chặn theo IP. Rust với thư viện reqwest cung cấp hiệu suất tuyệt vời, nhưng không giải quyết vấn đề xoay vòng proxy ngay từ đầu — điều này cần phải được thực hiện bởi chính bạn. Trong bài viết này, chúng ta sẽ xem xét cách kết nối với proxy SOCKS5 và HTTP(S), xây dựng pool khách hàng và tổ chức xoay vòng IP sao cho, trình phân tích không bị ngừng hoạt động sau những phản hồi 429 và 403 đầu tiên.

Tại sao trình phân tích cần proxy và nguy hiểm của việc thiếu nó

Các trang web như marketplace, aggregators giá và mạng xã hội theo dõi hoạt động bất thường từ một IP: hàng chục yêu cầu mỗi giây, User-Agent giống nhau, không có cookies giữa các yêu cầu. Ngay khi bộ đếm các hành động nghi ngờ vượt quá ngưỡng, IP sẽ bị đưa vào danh sách cấm — tạm thời hoặc vĩnh viễn. Đối với trình phân tích, điều này có nghĩa là ngừng hoàn toàn việc thu thập dữ liệu, cho đến khi IP được thay đổi thủ công.

Proxy giải quyết vấn đề này bằng cách phân phối các yêu cầu giữa hàng chục hoặc hàng trăm địa chỉ IP. Tuy nhiên, điều quan trọng không chỉ là "thay thế proxy", mà còn phải tổ chức một cách hợp lý pool kết nối và xoay vòng, nếu không bạn sẽ gặp phải giới hạn của chính reqwest (một khách hàng = một pool kết nối), hoặc bắt đầu gửi yêu cầu từ cùng một proxy quá thường xuyên. Tiếp theo, chúng ta sẽ xem xét cả hai vấn đề theo thứ tự: trước tiên là kết nối cơ bản, sau đó là kiến trúc của pool.

Đối với trình phân tích sản xuất, không chỉ xoay vòng IP là quan trọng, mà còn là chất lượng của chính các địa chỉ. Nếu bạn đang phân tích Wildberries, Ozon hoặc Avito, các IP từ trung tâm dữ liệu bị chặn nhanh hơn so với IP của người dùng thực — điều này sẽ được đề cập chi tiết hơn trong phần về việc chọn loại proxy.

Cài đặt cơ bản HTTP/HTTPS proxy trong reqwest

reqwest hỗ trợ proxy thông qua cấu trúc Proxy, mà bạn cần truyền vào ClientBuilder. Thêm các phụ thuộc vào Cargo.toml:

[dependencies]
reqwest = { version = "0.12", features = ["socks", "json"] }
tokio = { version = "1", features = ["full"] }

Ví dụ tối thiểu với HTTP-proxy yêu cầu xác thực bằng tên đăng nhập và mật khẩu:

use reqwest::{Client, Proxy};

#[tokio::main]
async fn main() -> Result<(), Box<dyn std::error::Error>> {
    let proxy = Proxy::all("http://gate.proxycove.com:8000")?
        .basic_auth("user_login", "user_password");

    let client = Client::builder()
        .proxy(proxy)
        .timeout(std::time::Duration::from_secs(15))
        .build()?;

    let resp = client
        .get("https://httpbin.org/ip")
        .send()
        .await?
        .text()
        .await?;

    println!("Phản hồi qua proxy: {resp}");
    Ok(())
}

Phương thức Proxy::all chuyển hướng cả lưu lượng HTTP và HTTPS qua proxy. Nếu cần phân tách các giao thức, hãy sử dụng Proxy::http() và Proxy::https() riêng biệt — điều này hữu ích nếu nhà cung cấp proxy lắng nghe các cổng khác nhau cho các giao thức khác nhau.

Kết nối SOCKS5 proxy

SOCKS5 hoạt động ở mức thấp hơn so với HTTP-proxy và hỗ trợ kết nối TCP mà không cần phân tích nội dung yêu cầu — điều này nhanh hơn và ổn định hơn cho việc phân tích, đặc biệt nếu bạn không chỉ truy cập HTTP(S), mà còn gọi các điểm cuối WebSocket. Đối với SOCKS5, hãy chắc chắn rằng bạn đã bật tính năng socks trong Cargo.toml (đã được đề cập ở trên). Ví dụ kết nối:

use reqwest::{Client, Proxy};

async fn build_socks5_client(
    host: &str,
    login: &str,
    password: &str,
) -> reqwest::Result<Client> {
    let proxy_url = format!("socks5h://{host}");
    let proxy = Proxy::all(&proxy_url)?.basic_auth(login, password);

    Client::builder()
        .proxy(proxy)
        .timeout(std::time::Duration::from_secs(20))
        .build()
}

Lưu ý đến sơ đồ socks5h thay vì socks5. Chữ "h" ở cuối có nghĩa là việc phân giải DNS của miền diễn ra ở phía máy chủ proxy, không phải cục bộ. Điều này rất quan trọng cho tính ẩn danh: nếu không có nó, bộ phân giải DNS thực của bạn có thể "rò rỉ" các miền được yêu cầu qua proxy, ngay cả khi lưu lượng truy cập tự đi qua đường hầm SOCKS5.

Nếu nhà cung cấp proxy không yêu cầu xác thực bằng tên đăng nhập/mật khẩu (whitelisting IP), chỉ cần không gọi basic_auth — hãy chỉ định địa chỉ IP của máy chủ của bạn trong bảng điều khiển của nhà cung cấp, từ đó gửi yêu cầu.

Pool khách hàng reqwest cho các proxy khác nhau

Một lỗi thường gặp là tạo một Client mới cho mỗi yêu cầu. Điều này tốn kém: reqwest giữ một pool các kết nối TCP và các phiên TLS bên trong, và việc tái tạo khách hàng sẽ tiêu diệt tất cả các lợi ích của keep-alive. Cách tiếp cận đúng là tạo một khách hàng cho mỗi proxy trước và tái sử dụng chúng từ pool chung.

use reqwest::{Client, Proxy};
use std::sync::Arc;

#[derive(Clone)]
struct ProxyEndpoint {
    url: String,
    login: String,
    password: String,
}

struct ClientPool {
    clients: Vec<Client>,
}

impl ClientPool {
    fn build(endpoints: &[ProxyEndpoint]) -> reqwest::Result<Self> {
        let mut clients = Vec::with_capacity(endpoints.len());

        for ep in endpoints {
            let proxy = Proxy::all(&ep.url)?
                .basic_auth(&ep.login, &ep.password);

            let client = Client::builder()
                .proxy(proxy)
                .pool_max_idle_per_host(4)
                .timeout(std::time::Duration::from_secs(15))
                .build()?;

            clients.push(client);
        }

        Ok(Self { clients })
    }
}

Pool như vậy được tạo ra một lần khi khởi động trình phân tích. Số lượng khách hàng thường bằng số lượng duy nhất các địa chỉ proxy trong pool của bạn — nếu nhà cung cấp cho phép xoay vòng ở cấp độ gateway, chỉ cần một hoặc hai khách hàng là đủ, vì việc xoay vòng IP diễn ra tự động ở phía nhà cung cấp khi mỗi kết nối TCP mới.

Đối với các tác vụ có độ song song cao, hãy bao bọc pool trong Arc và chia sẻ giữa các tác vụ tokio — các khách hàng reqwest an toàn cho luồng và được thiết kế để sử dụng đồng thời.

Chiến lược xoay vòng IP

Có hai cách tiếp cận khác nhau về xoay vòng. Đầu tiên — xoay vòng ở phía nhà cung cấp: bạn kết nối với một địa chỉ gateway, và nhà cung cấp tự động thay thế IP mới cho mỗi yêu cầu hoặc cứ sau N phút. Thứ hai — xoay vòng ở phía bạn: bạn giữ một danh sách các proxy tĩnh và tự chọn cái nào để sử dụng cho yêu cầu tiếp theo.

Nếu bạn sử dụng proxy cư trú với xoay vòng tự động qua gateway, mã sẽ đơn giản hóa đến mức tối thiểu — chỉ cần gửi yêu cầu qua cùng một khách hàng, nhà cung cấp tự thay đổi IP đầu ra. Nhưng nếu cần kiểm soát (ví dụ, gán IP cho phiên trong 10 phút để mô phỏng hành vi của người dùng thực), hãy thực hiện xoay vòng một cách thủ công:

use rand::seq::SliceRandom;
use std::sync::atomic::{AtomicUsize, Ordering};

struct RotatingPool {
    clients: Vec<Client>,
    counter: AtomicUsize,
}

impl RotatingPool {
    // Round-robin: lần lượt lặp qua các khách hàng
    fn next_round_robin(&self) -> &Client {
        let idx = self.counter.fetch_add(1, Ordering::SeqCst) % self.clients.len();
        &self.clients[idx]
    }

    // Chọn ngẫu nhiên — hữu ích để chống lại phân tích mẫu
    fn next_random(&self) -> &Client {
        self.clients.choose(&mut rand::thread_rng()).unwrap()
    }
}

Round-robin phù hợp khi việc phân phối tải đều trên tất cả các IP là quan trọng. Chọn ngẫu nhiên tốt hơn trong việc che giấu mẫu yêu cầu, nếu trang web mục tiêu phân tích khoảng cách giữa các lần truy cập từ một địa chỉ. Đối với các tác vụ như giám sát giá trên Ozon hoặc thu thập quảng cáo từ Avito, trên thực tế hiệu quả hơn là sự kết hợp: chọn ngẫu nhiên proxy cộng với độ trễ ngẫu nhiên từ 1–3 giây giữa các yêu cầu của cùng một khách hàng.

Một điều riêng biệt cần nói đến là các phiên sticky. Nếu trình phân tích thực hiện một kịch bản nhiều bước (đăng nhập → chuyển trang → hành động), không thể thay đổi IP giữa phiên — trang web sẽ nhận thấy sự thay đổi IP khi có phiên cookie hoạt động và sẽ hoặc là đăng xuất, hoặc là chặn tài khoản. Đối với những kịch bản như vậy, hãy gán một khách hàng (và tương ứng một IP) cho "phiên" cụ thể của trình phân tích trong suốt thời gian sống của nó.

Xử lý lỗi và logic thử lại

Proxy thỉnh thoảng bị ngắt kết nối: thời gian chờ, 407 (Yêu cầu xác thực Proxy), ngắt kết nối TCP. Trình phân tích phải có khả năng phân biệt "proxy bị hỏng — loại bỏ khỏi pool" và "trang web trả về 429 — chỉ cần chờ và thay đổi IP". Đây là mẫu làm việc với thử lại và loại trừ các proxy chết:

use reqwest::StatusCode;
use std::time::Duration;

async fn fetch_with_retry(
    pool: &RotatingPool,
    url: &str,
    max_attempts: u32,
) -> Result<String, String> {
    let mut last_error = String::new();

    for attempt in 0..max_attempts {
        let client = pool.next_random();

        match client.get(url).send().await {
            Ok(resp) if resp.status() == StatusCode::TOO_MANY_REQUESTS => {
                last_error = "429: giới hạn tỷ lệ, đang thay đổi IP".into();
                tokio::time::sleep(Duration::from_millis(500 * (attempt as u64 + 1))).await;
                continue;
            }
            Ok(resp) if resp.status().is_success() => {
                return resp.text().await.map_err(|e| e.to_string());
            }
            Ok(resp) => {
                last_error = format!("Trạng thái không mong đợi: {}", resp.status());
            }
            Err(e) if e.is_timeout() || e.is_connect() => {
                last_error = format!("Vấn đề với proxy: {e}");
                continue;
            }
            Err(e) => {
                last_error = e.to_string();
            }
        }
    }

    Err(format!("Tất cả các nỗ lực đã cạn kiệt: {last_error}"))
}

Một điểm quan trọng: độ trễ theo cấp số nhân (500 * attempt) giảm tải cho trang web mục tiêu trong trường hợp có nhiều phản hồi 429 và đồng thời giảm nguy cơ bị chặn toàn bộ pool proxy ngay lập tức. Đối với các trình phân tích sản xuất, hãy thêm bộ đếm lỗi cho mỗi proxy trong pool và tự động loại bỏ địa chỉ nếu nó tạo ra 5+ lỗi liên tiếp — điều này sẽ bảo vệ bạn khỏi những nỗ lực vô ích qua IP đã bị cấm.

Loại proxy nào nên chọn cho việc phân tích

Việc chọn loại proxy ảnh hưởng trực tiếp đến số lượng yêu cầu thành công bạn nhận được trước khi bị chặn. IP từ trung tâm dữ liệu là rẻ nhất và nhanh nhất, nhưng thuộc về các dải IP nổi tiếng của các nhà cung cấp dịch vụ lưu trữ — các hệ thống chống bot của các marketplace và mạng xã hội phát hiện chúng gần như ngay lập tức từ các cơ sở dữ liệu ASN. IP cư trú và di động trông giống như người dùng thông thường, vì vậy chúng tồn tại lâu hơn nhiều.

Loại proxy Tốc độ Rủi ro bị chặn Khi nào áp dụng
Trung tâm dữ liệu Cao Cao API mở, trang web không có chống bot
Cư trú Trung bình Thấp Wildberries, Ozon, Avito, mạng xã hội
Di động Trung bình Tối thiểu Instagram, TikTok, chống bot nghiêm ngặt

Đối với việc phân tích các marketplace với khối lượng lớn, sự cân bằng hợp lý là proxy cư trú: chúng không đắt như proxy di động tính theo lưu lượng, nhưng vượt qua các kiểm tra ở cấp độ "người dùng thực", điều này cắt giảm các dải IP từ trung tâm dữ liệu. Nếu trình phân tích hoạt động qua API của mạng xã hội hoặc mô phỏng ứng dụng di động, proxy di động cung cấp tỷ lệ captcha và cấm tạm thời thấp nhất nhờ IP được gán cho các nhà mạng.

Hiệu suất và mẹo về tải

Rust cho phép giữ hàng nghìn yêu cầu đồng thời trên một instance, nhưng nhà cung cấp proxy và trang web mục tiêu vẫn giới hạn băng thông thực tế. Một vài quy tắc thực tiễn cho trình phân tích sản xuất:

  • Giới hạn độ song song thông qua tokio::sync::Semaphore — không quá 20-50 yêu cầu đồng thời trên một pool proxy, nếu không một số kết nối sẽ bắt đầu bị ngắt do thời gian chờ.
  • Sử dụng pool_idle_timeout trong ClientBuilder, để reqwest không giữ các kết nối chết qua các proxy không ổn định.
  • Ghi lại các trạng thái HTTP và thời gian phản hồi cho từng proxy riêng biệt — như vậy bạn sẽ nhanh chóng thấy những địa chỉ nào bắt đầu "hỏng" và cần thay thế.
  • Xoay vòng User-Agent cùng với xoay vòng IP giảm đáng kể khả năng phát hiện: hãy thay đổi chúng đồng bộ, thay vì theo các chu kỳ riêng biệt.
  • Đối với các trình phân tích lâu dài, hãy thêm kiểm tra sức khỏe: cứ vài phút gửi một yêu cầu thử nghiệm đến https://httpbin.org/ip qua từng proxy và loại bỏ những proxy không phản hồi.

Hãy chú ý đặc biệt đến các dấu vân tay TLS (JA3/JA3S). reqwest theo mặc định sử dụng rustls hoặc nativetls tùy thuộc vào các tính năng của bản dựng, và cả hai đều có các dấu vân tay TLS handshake khác nhau. Một số hệ thống chống bot (ví dụ, ở các marketplace lớn) phân tích chính xác dấu vân tay TLS, không chỉ IP — vì vậy ngay cả một proxy hoàn hảo cũng không cứu được nếu chính khách hàng tự thể hiện mình bằng một handshake TLS không chuẩn. Nếu bạn gặp phải các chặn khi sử dụng proxy "sạch", hãy kiểm tra các tính năng của reqwest và thử tính năng native-tls thay vì rustls, hoặc ngược lại.

Kết luận

Proxy trong Rust reqwest được cấu hình nhanh chóng — chỉ cần vài dòng với Proxy::all hoặc sơ đồ SOCKS5 socks5h://. Công việc chính bắt đầu từ đó: xây dựng pool khách hàng, chọn chiến lược xoay vòng (round-robin, ngẫu nhiên hoặc các phiên sticky) và xử lý lỗi một cách hợp lý với việc loại bỏ các địa chỉ không hoạt động khỏi pool. Nếu không, ngay cả trình phân tích nhanh nhất trên Rust cũng sẽ gặp phải các chặn chỉ sau vài phút hoạt động.

Nếu trình phân tích của bạn làm việc với các marketplace hoặc mạng xã hội, nơi bảo vệ chống bot phân tích không chỉ tần suất yêu cầu mà còn cả nguồn gốc IP, hãy bắt đầu với proxy cư trú — chúng cung cấp sự cân bằng tốt nhất giữa giá cả và độ bền của các phiên. Đối với các mục tiêu nghiêm ngặt hơn như Instagram và TikTok, bạn nên thử nghiệm với IP di động, còn đối với các tác vụ đơn giản không có chống bot thì hoàn toàn đủ với các proxy trung tâm dữ liệu nhanh và rẻ.