← Quay lại blog

Cần tây, RQ hoặc BullMQ: cách xây dựng hàng đợi tác vụ với proxy và vượt qua giới hạn yêu cầu trên IP

Phân tích kiến trúc hàng đợi tác vụ với việc xoay vòng proxy trong Celery, RQ và BullMQ - cách phân phối tải giữa các địa chỉ IP và tránh giới hạn tốc độ.

📅20 tháng 10, 2026

Khi một trình phân tích hoặc bot gửi hàng trăm yêu cầu mỗi phút từ một IP, máy chủ nhanh chóng kích hoạt giới hạn tốc độ, và sau đó - captcha hoặc cấm. Giải pháp là hàng đợi tác vụ, phân phối các yêu cầu giữa các worker và proxy. Trong bài viết này, chúng ta sẽ phân tích cách kiến trúc như vậy hoạt động trong Celery, RQ và BullMQ, và cách quản lý giới hạn yêu cầu cho mỗi địa chỉ IP một cách hợp lý.

Tại sao cần hàng đợi tác vụ khi làm việc với proxy

Nếu nhiệm vụ là thu thập 50.000 thẻ sản phẩm từ Wildberries hoặc kiểm tra trạng thái 10.000 tài khoản Instagram, việc khởi động các yêu cầu một cách tuần tự sẽ mất hàng giờ, trong khi việc khởi động song song mà không kiểm soát sẽ nhanh chóng bị cấm theo IP. Hàng đợi tác vụ (task queue) giải quyết cả hai vấn đề: nó chia nhỏ khối lượng công việc lớn thành các tác vụ nhỏ, phân phối chúng giữa nhiều worker và cho phép gán một proxy riêng cho mỗi worker.

Kiến trúc thường trông như sau: một broker tin nhắn (Redis hoặc RabbitMQ) lưu trữ hàng đợi tác vụ, các worker lấy các tác vụ từ hàng đợi và thực hiện chúng, mỗi worker sử dụng proxy riêng hoặc xoay vòng proxy từ pool. Điều này cho phép giữ tải trên mỗi IP trong giới hạn an toàn, ngay cả khi tổng khối lượng yêu cầu rất lớn.

Cách tiếp cận này không chỉ được các nhà phát triển trình phân tích sử dụng, mà còn được các đội ngũ tự động hóa kiểm tra các tài khoản quảng cáo Facebook Ads, theo dõi giá trên Ozon hoặc thu thập dữ liệu từ hàng chục trang web cùng một lúc. Nếu không có hàng đợi và phân phối hợp lý theo IP, bất kỳ hệ thống nào như vậy đều sẽ bị cấm ngay trong ngày đầu tiên hoạt động.

Vấn đề giới hạn yêu cầu trên một IP

Hầu hết các trang web và API đều có bảo vệ chống lại tải quá mức. Điều này có thể biểu hiện theo nhiều cách khác nhau: máy chủ trả về HTTP 429 (Quá nhiều yêu cầu), hiển thị captcha, tạm thời cấm IP từ 10-60 phút hoặc vĩnh viễn đưa nó vào danh sách đen. Các nền tảng khác nhau có giới hạn rất khác nhau - ví dụ, API Wildberries cho phép một số lượng yêu cầu hạn chế mỗi giây từ một địa chỉ, trong khi Instagram phản ứng với hoạt động nghi ngờ ngay sau 15-20 hành động liên tiếp.

Lỗi chính khi xây dựng hàng đợi là khởi động tất cả các worker thông qua cùng một IP. Ngay cả khi các tác vụ được phân phối giữa 20 worker, nhưng tất cả đều sử dụng một proxy hoặc không sử dụng proxy, giới hạn trên IP này sẽ đến nhanh như khi thực hiện tuần tự. Do đó, số lượng worker phải tương ứng với số lượng địa chỉ IP có sẵn trong pool proxy.

Đối với các tác vụ có tần suất yêu cầu cao trên một miền, tốt hơn là sử dụng proxy trung tâm dữ liệu - chúng cung cấp độ trễ thấp và phù hợp cho việc phân tích song song quy mô lớn, nơi tốc độ quan trọng hơn là việc ngụy trang thành người dùng bình thường. Nếu mục tiêu là vượt qua các bảo vệ nghiêm ngặt hơn như hệ thống chống bot của các thị trường hoặc mạng xã hội, cần những IP giống như người dùng bình thường, tức là proxy dân cư hoặc di động.

Celery: kiến trúc và xoay vòng proxy trong mã

Celery là hàng đợi tác vụ trưởng thành và phổ biến nhất trong hệ sinh thái Python. Nó hoạt động trên một broker (thường là Redis hoặc RabbitMQ) và hỗ trợ cấu hình linh hoạt về độ cạnh tranh, số lần thử lại và ưu tiên tác vụ. Để xoay vòng proxy, tiện lợi khi lưu trữ pool IP trong Redis và gán proxy cho mỗi tác vụ khi đưa vào hàng đợi.

from celery import Celery
import requests
import redis
import random

app = Celery('tasks', broker='redis://localhost:6379/0')
redis_client = redis.Redis(host='localhost', port=6379, db=1)

PROXY_POOL = [
    "http://user:[email protected]:8000",
    "http://user:[email protected]:8000",
    "http://user:[email protected]:8000",
]

def get_proxy():
    # Round-robin qua Redis, để không đánh vào một IP
    idx = redis_client.incr("proxy_cursor") % len(PROXY_POOL)
    return PROXY_POOL[idx]

@app.task(bind=True, max_retries=3, default_retry_delay=30)
def fetch_url(self, url):
    proxy = get_proxy()
    try:
        response = requests.get(
            url,
            proxies={"http": proxy, "https": proxy},
            timeout=15
        )
        if response.status_code == 429:
            raise self.retry(exc=Exception("Giới hạn tốc độ"))
        return response.status_code
    except requests.exceptions.RequestException as exc:
        raise self.retry(exc=exc)

Điểm chính là hàm get_proxy(), hàm này thông qua bộ đếm Redis cung cấp proxy tiếp theo từ pool theo vòng tròn. Điều này đảm bảo rằng tải được phân phối đồng đều giữa tất cả các IP, chứ không tập trung vào một IP. Tham số max_retries và xử lý mã 429 cho phép tác vụ tự động gửi lại với một proxy khác nếu IP hiện tại bị giới hạn.

Để kiểm soát độ cạnh tranh ở cấp độ worker, Celery cho phép giới hạn số lượng tác vụ đồng thời thông qua tham số --concurrency khi khởi động worker, cũng như thông qua các hàng đợi với mức độ ưu tiên khác nhau (ví dụ: một hàng đợi riêng cho các tác vụ chậm với giới hạn tốc độ nghiêm ngặt).

RQ (Redis Queue): lựa chọn đơn giản

RQ là một thư viện nhẹ hơn so với Celery, không có broker RabbitMQ và cấu hình tối thiểu. Nó phù hợp nếu dự án nhỏ và không yêu cầu định tuyến tác vụ phức tạp, mà chỉ cần một hàng đợi với các worker và khả năng gán proxy cho tác vụ.

from redis import Redis
from rq import Queue
import requests
import itertools

redis_conn = Redis()
q = Queue(connection=redis_conn)

PROXY_POOL = [
    "http://user:[email protected]:8000",
    "http://user:[email protected]:8000",
]
proxy_cycle = itertools.cycle(PROXY_POOL)

def fetch_with_proxy(url):
    proxy = next(proxy_cycle)
    response = requests.get(
        url,
        proxies={"http": proxy, "https": proxy},
        timeout=15
    )
    if response.status_code == 429:
        # Đưa lại vào hàng đợi với độ trễ
        q.enqueue_in(60, fetch_with_proxy, url)
        return None
    return response.status_code

# Đưa tác vụ vào hàng đợi
urls = ["https://example.com/page1", "https://example.com/page2"]
for url in urls:
    q.enqueue(fetch_with_proxy, url)

Trong RQ, việc xoay vòng qua itertools.cycle hoạt động bên trong quá trình của worker - điều này đơn giản hơn so với bộ đếm Redis, nhưng khó mở rộng hơn khi có nhiều worker song song, vì mỗi worker bắt đầu vòng lặp từ đầu. Đối với các dự án nhỏ với 1-5 worker, điều này là đủ, nhưng đối với các dự án lớn hơn, tốt hơn là quay lại với bộ đếm trung tâm trong Redis, như trong ví dụ với Celery.

RQ rất phù hợp với các nhà phát triển đã sử dụng Redis trong dự án và không muốn khởi động một broker tin nhắn riêng. Nhược điểm là ít cơ chế ưu tiên và giám sát tích hợp hơn so với Celery, vì vậy đối với các hệ thống sản xuất với hàng trăm nghìn tác vụ mỗi ngày, thường chọn Celery.

BullMQ: hàng đợi trên Node.js với giới hạn theo IP

BullMQ là tiêu chuẩn cho các hàng đợi tác vụ trong hệ sinh thái Node.js, được xây dựng trên Redis và có bộ giới hạn tốc độ tích hợp, điều này đặc biệt thuận tiện cho việc kiểm soát giới hạn yêu cầu trên IP. Thay vì phải đếm yêu cầu bằng tay, bạn có thể chỉ định bộ giới hạn ngay tại cấp độ hàng đợi.

const { Queue, Worker } = require('bullmq');
const axios = require('axios');

const connection = { host: 'localhost', port: 6379 };

const proxyPool = [
  'http://user:[email protected]:8000',
  'http://user:[email protected]:8000',
  'http://user:[email protected]:8000',
];

const queue = new Queue('fetchQueue', { connection });

// Bộ giới hạn: không quá 5 tác vụ mỗi giây trên hàng đợi
const worker = new Worker('fetchQueue', async job => {
  const proxy = proxyPool[job.id % proxyPool.length];
  try {
    const response = await axios.get(job.data.url, {
      proxy: false,
      httpsAgent: new (require('https-proxy-agent'))(proxy),
      timeout: 15000,
    });
    return response.status;
  } catch (err) {
    if (err.response && err.response.status === 429) {
      throw new Error('Giới hạn tốc độ, đang thử lại với một proxy khác');
    }
    throw err;
  }
}, {
  connection,
  limiter: { max: 5, duration: 1000 },
  concurrency: proxyPool.length,
});

for (let i = 0; i < 100; i++) {
  queue.add('fetch', { url: `https://example.com/item/${i}` }, {
    attempts: 3,
    backoff: { type: 'exponential', delay: 5000 },
  });
}

Tham số limiter trong BullMQ là cơ chế tích hợp, giới hạn tổng số tác vụ được xử lý trong một khoảng thời gian, điều này thuận tiện cho việc tuân thủ giới hạn yêu cầu ở cấp độ toàn hệ thống. Tham số concurrency, bằng kích thước của pool proxy, đảm bảo rằng số lượng worker hoạt động đồng thời đúng bằng số IP có sẵn.

Cách tiếp cận này thường được các đội ngũ viết bot tự động hóa hành động trong TikTok hoặc Telegram sử dụng - BullMQ tích hợp tốt với phần còn lại của stack Node.js và không yêu cầu chuyển sang Python chỉ để thực hiện một tác vụ.

So sánh Celery, RQ và BullMQ

Tham số Celery RQ BullMQ
Ngôn ngữ Python Python Node.js
Broker Redis / RabbitMQ Chỉ Redis Chỉ Redis
Bộ giới hạn tốc độ tích hợp Không (viết bằng tay) Không Có, từ hộp
Ngưỡng đầu vào Trung bình Thấp Trung bình
Phù hợp cho Hệ thống sản xuất lớn Dự án nhỏ, nguyên mẫu Stack Node.js, bot, tự động hóa

Chiến lược phân phối tải theo IP

Round-robin, được sử dụng trong các ví dụ trên, là chiến lược đơn giản nhất, nhưng không phải lúc nào cũng tối ưu. Dưới đây là một số tùy chọn mà bạn nên xem xét tùy thuộc vào nhiệm vụ:

  • Round-robin - mỗi yêu cầu tiếp theo đi qua proxy tiếp theo theo vòng tròn. Phù hợp khi tất cả các IP đều tương đương và giới hạn trên chúng là như nhau.
  • Sticky session - một proxy được gán cho một tài khoản hoặc phiên cụ thể trong suốt thời gian làm việc với nó. Điều này rất quan trọng cho các mạng xã hội và thị trường, nơi việc thay đổi IP giữa phiên làm việc trông có vẻ đáng ngờ.
  • Weighted distribution - một phần yêu cầu được gửi đến các IP đắt tiền và đáng tin cậy hơn (ví dụ: di động), phần khác đến các IP trung tâm dữ liệu rẻ hơn, tùy thuộc vào miền nào đang được xử lý.
  • Adaptive throttling - tốc độ gửi tác vụ tự động giảm nếu tỷ lệ lỗi 429 hoặc captcha tăng lên, và phục hồi khi lỗi biến mất.

Đối với các tác vụ mà việc ngụy trang thành người dùng thực là quan trọng - chẳng hạn như làm ấm và kiểm tra tài khoản trên Instagram hoặc TikTok thông qua các trình duyệt chống phát hiện như Dolphin Anty hoặc AdsPower - tốt hơn là áp dụng sticky session với proxy di động: chúng ít bị nghi ngờ bởi các hệ thống chống bot và hành xử như người dùng bình thường của internet di động.

Đối với việc thu thập dữ liệu hàng loạt từ các trang web, nơi tốc độ và khối lượng quan trọng hơn là sự tin cậy của thuật toán, round-robin với proxy dân cư cung cấp sự cân bằng giữa độ ổn định và giá cả: các IP trông giống như người dùng thực, nhưng có thể thay đổi thường xuyên mà không có rủi ro cho một phiên cụ thể.

Giám sát lỗi 429 và cấm

Hàng đợi tác vụ mà không có giám sát là một chiếc hộp đen: khó hiểu tại sao một phần các tác vụ thất bại và những IP nào đã bị nghi ngờ. Bộ chỉ số tối thiểu mà bạn nên theo dõi:

  • Tỷ lệ phản hồi với mã 429 và 403 cho mỗi proxy riêng biệt
  • Thời gian phản hồi trung bình - sự gia tăng độ trễ thường xảy ra trước khi bị cấm
  • Số lần thử lại (retries) cho mỗi tác vụ
  • Độ dài hàng đợi tác vụ theo thời gian thực - nếu nó tăng lên, các worker không thể xử lý

Trong Celery, bạn có thể sử dụng Flower - giao diện web để giám sát các tác vụ và worker theo thời gian thực. Trong BullMQ, có Bull Board với chức năng tương tự. Đối với RQ, có RQ Dashboard. Cả ba công cụ này đều hiển thị trạng thái của các tác vụ, số lượng các lần thử không thành công và cho phép nhanh chóng hiểu được proxy nào trong pool cần tạm thời loại bỏ khỏi vòng quay.

Một thực hành tốt là tự động đánh dấu proxy là "trong cách ly" trong 15-30 phút sau một vài lỗi 429 liên tiếp từ IP này, và chỉ đưa nó trở lại pool sau khi có một yêu cầu thử nghiệm thành công. Điều này ngăn chặn tình huống mà một IP bị cấm vẫn tiếp tục nhận tác vụ và lãng phí thời gian của các worker.

Kết luận

Hàng đợi tác vụ giải quyết vấn đề mở rộng yêu cầu, nhưng tự nó không bảo vệ khỏi giới hạn trên IP - để làm điều này cần có một vòng xoay proxy hợp lý bên trong Celery, RQ hoặc BullMQ. Lựa chọn công cụ phụ thuộc vào stack: các dự án Python thường chọn Celery cho sản xuất và RQ cho các tác vụ đơn giản, trong khi các nhà phát triển Node.js chọn BullMQ với bộ giới hạn tốc độ tích hợp của nó.

Bất kể hàng đợi nào được chọn, kết quả phụ thuộc rất nhiều vào chất lượng của pool proxy. Nếu nhiệm vụ là phân tích hàng loạt nhanh chóng, hãy xem xét proxy trung tâm dữ liệu để có tốc độ, và cho việc làm việc với các mạng xã hội và thị trường, nơi sự trung thành của các hệ thống chống bot quan trọng, các IP dân cư hoặc di động sẽ phù hợp - chúng giảm tỷ lệ lỗi 429 và cấm, điều này trực tiếp làm giảm số lần thử lại và tăng tốc độ thực hiện toàn bộ hàng đợi tác vụ.