زمانی که یک پارسر یا ربات صدها درخواست در دقیقه از یک IP ارسال میکند، سرور به سرعت محدودیت نرخ را فعال میکند و بعد از آن — کد کپچا یا مسدودیت. راه حل — صف وظایف، که درخواستها را بین کارگران و پروکسیها توزیع میکند. در این مقاله بررسی میکنیم که چنین معماری در Celery، RQ و BullMQ چگونه کار میکند و چگونه به درستی محدودیت درخواستها را برای هر آدرس IP مدیریت کنیم.
چرا صف وظایف هنگام کار با پروکسی
اگر وظیفه — استخراج 50,000 کارت کالا از Wildberries یا بررسی وضعیت 10,000 حساب Instagram باشد، اجرای متوالی درخواستها ساعتها طول میکشد و اجرای موازی بدون کنترل به سرعت منجر به مسدودیت IP میشود. صف وظایف (task queue) هر دو مشکل را حل میکند: این صف کار بزرگ را به وظایف کوچک تقسیم میکند، آنها را بین چندین کارگر توزیع میکند و به هر کارگر پروکسی جداگانهای اختصاص میدهد.
معماری معمولاً به این شکل است: کارگزار پیام (Redis یا RabbitMQ) صف وظایف را ذخیره میکند، کارگران وظایف را از صف برمیدارند و آنها را اجرا میکنند، هر کارگر از پروکسی خود استفاده میکند یا پروکسیها را از استخر چرخش میدهد. این کار اجازه میدهد تا بار روی هر IP در محدودههای ایمن نگه داشته شود، حتی اگر حجم کلی درخواستها بسیار زیاد باشد.
این رویکرد فقط توسط توسعهدهندگان پارسرها استفاده نمیشود، بلکه توسط تیمهایی که بررسی خودکار حسابهای تبلیغاتی Facebook Ads را انجام میدهند، قیمتها را در Ozon نظارت میکنند یا دادهها را به طور همزمان از دهها وبسایت جمعآوری میکنند، نیز به کار میرود. بدون صف و توزیع مناسب بر اساس IP، هر سیستم چنین به سرعت در روز اول کار خود با مسدودیت مواجه میشود.
مشکل محدودیت درخواستها برای یک IP
اکثر وبسایتها و APIها از بارگذاری بیش از حد محافظت میکنند. این میتواند به اشکال مختلفی بروز کند: سرور HTTP 429 (Too Many Requests) را برمیگرداند، کپچا نشان میدهد، IP را به مدت 10-60 دقیقه به طور موقت مسدود میکند یا به طور دائمی آن را به لیست سیاه اضافه میکند. محدودیتها در پلتفرمهای مختلف به شدت متفاوت است — به عنوان مثال، API Wildberries تعداد محدودی درخواست در ثانیه از یک آدرس را مجاز میدارد، در حالی که Instagram به فعالیت مشکوک پس از 15-20 اقدام متوالی واکنش نشان میدهد.
اشتباه اصلی در ساخت صف — راهاندازی همه کارگران از طریق یک IP واحد است. حتی اگر وظایف بین 20 کارگر توزیع شده باشد، اما همه آنها از یک پروکسی استفاده کنند یا اصلاً از آن استفاده نکنند، محدودیت برای این IP به همان سرعتی که در اجرای متوالی اتفاق میافتد، اعمال میشود. بنابراین تعداد کارگران باید با تعداد IPهای موجود در استخر پروکسی تناسب داشته باشد.
برای وظایفی که درخواستهای مکرر به یک دامنه دارند، بهتر است از پروکسیهای دیتاسنتر استفاده کنید — اینها تأخیر پایینی دارند و برای استخراج موازی مقیاسپذیر مناسب هستند، جایی که سرعت مهم است و نه پنهان شدن به عنوان یک کاربر عادی. اگر هدف — دور زدن محافظتهای سختتر مانند سیستمهای ضد ربات در بازارها یا شبکههای اجتماعی باشد، به IPهایی نیاز است که شبیه کاربران عادی باشند، یعنی پروکسیهای مسکونی یا موبایل.
Celery: معماری و چرخش پروکسی در کد
Celery — بالغترین و محبوبترین صف وظایف در اکوسیستم Python است. این صف بر روی کارگزار (معمولاً Redis یا RabbitMQ) کار میکند و پیکربندی منعطفی برای همزمانی، تلاشهای مجدد و اولویتهای وظایف را پشتیبانی میکند. برای چرخش پروکسی، مناسب است که استخر IP را در Redis ذخیره کرده و پروکسی را به هر وظیفه هنگام قرار دادن در صف اختصاص دهید.
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 از طریق Redis، تا به یک 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("Rate limited"))
return response.status_code
except requests.exceptions.RequestException as exc:
raise self.retry(exc=exc)
نکته کلیدی — تابع get_proxy()، که از طریق شمارنده Redis پروکسی بعدی را به صورت چرخشی از استخر ارائه میدهد. این تضمین میکند که بار به طور یکنواخت بین تمام IPها توزیع میشود و بر روی یک IP متمرکز نمیشود. پارامتر max_retries و پردازش کد 429 به وظیفه اجازه میدهد به طور خودکار با پروکسی دیگری دوباره ارسال شود، اگر IP فعلی محدودیت دریافت کرده باشد.
برای کنترل همزمانی در سطح کارگران، Celery اجازه میدهد تعداد وظایف همزمان را از طریق پارامتر --concurrency هنگام راهاندازی کارگر محدود کنید، و همچنین از طریق صفهای با اولویتهای مختلف (به عنوان مثال، یک صف جداگانه برای وظایف کند با محدودیت نرخ سخت).
RQ (Redis Queue): جایگزین ساده
RQ — کتابخانهای سبکتر از Celery است، بدون کارگزار RabbitMQ و با پیکربندی حداقلی. این کتابخانه مناسب است اگر پروژه کوچک باشد و به مسیریابی پیچیده وظایف نیاز نداشته باشد، بلکه فقط یک صف با کارگران و امکان پیوند پروکسی به وظیفه لازم باشد.
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:
# قرار دادن دوباره در صف با تأخیر
q.enqueue_in(60, fetch_with_proxy, url)
return None
return response.status_code
# قرار دادن وظایف در صف
urls = ["https://example.com/page1", "https://example.com/page2"]
for url in urls:
q.enqueue(fetch_with_proxy, url)
در RQ، چرخش از طریق itertools.cycle در داخل فرآیند کارگر کار میکند — این سادهتر از شمارنده Redis است، اما در مقیاسپذیری با تعداد زیادی کارگر موازی ضعیفتر عمل میکند، زیرا هر کارگر چرخه را از ابتدا شروع میکند. برای پروژههای کوچک با 1-5 کارگر این کافی است، اما برای پروژههای بزرگتر بهتر است به شمارنده متمرکز در Redis برگردید، همانطور که در مثال Celery.
RQ برای توسعهدهندگانی که قبلاً از Redis در پروژه استفاده میکنند و نمیخواهند یک کارگزار پیام جداگانه راهاندازی کنند، مناسب است. نقطه ضعف — مکانیزمهای داخلی اولویتبندی و نظارت کمتری نسبت به Celery دارد، بنابراین برای سیستمهای تولیدی با صدها هزار وظیفه در روز معمولاً Celery انتخاب میشود.
BullMQ: صف در Node.js با محدودیت IP
BullMQ — استاندارد صفهای وظایف در اکوسیستم Node.js است، که بر روی Redis ساخته شده و دارای محدودکننده نرخ داخلی است، که به ویژه برای کنترل محدودیت درخواستها بر روی IP مناسب است. به جای اینکه به صورت دستی درخواستها را شمارش کنید، میتوانید محدودکننده را مستقیماً در سطح صف مشخص کنید.
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 });
// محدودکننده: حداکثر 5 وظیفه در ثانیه در صف
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('Rate limited, retrying with another proxy');
}
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 },
});
}
پارامتر limiter در BullMQ — یک مکانیزم داخلی است که تعداد کل وظایف پردازش شده را در یک بازه زمانی محدود میکند، که برای رعایت محدودیت درخواستها در سطح کل سیستم مناسب است. و پارامتر concurrency، که برابر با اندازه استخر پروکسی است، تضمین میکند که دقیقاً به اندازه IPهای آزاد، کارگران همزمان کار میکنند.
این رویکرد اغلب توسط تیمهایی که رباتهای خود را برای اتوماسیون فعالیتها در TikTok یا Telegram مینویسند استفاده میشود — BullMQ به خوبی با سایر استکهای Node.js ادغام میشود و نیازی به تغییر به Python برای یک وظیفه ندارد.
مقایسه Celery، RQ و BullMQ
| پارامتر | Celery | RQ | BullMQ |
|---|---|---|---|
| زبان | Python | Python | Node.js |
| کارگزار | Redis / RabbitMQ | فقط Redis | فقط Redis |
| محدودکننده نرخ داخلی | ندارد (به صورت دستی نوشته میشود) | ندارد | بله، از ابتدا |
| آستانه ورود | متوسط | پایین | متوسط |
| مناسب برای | سیستمهای تولیدی بزرگ | پروژههای کوچک، پروتوتایپها | استک Node.js، رباتها، اتوماسیون |
استراتژیهای توزیع بار بر اساس IP
Round-robin، که در مثالهای بالا استفاده شده، سادهترین استراتژی است، اما همیشه بهینه نیست. در اینجا چند گزینه وجود دارد که بسته به وظیفه باید در نظر گرفته شوند:
- Round-robin — هر درخواست بعدی از طریق پروکسی بعدی به صورت چرخشی میرود. مناسب است زمانی که همه IPها برابر هستند و محدودیتها بر روی آنها یکسان است.
- Sticky session — یک پروکسی به یک حساب خاص یا جلسه در تمام مدت کار با آن اختصاص داده میشود. این برای شبکههای اجتماعی و بازارها حیاتی است، جایی که تغییر IP در وسط جلسه مشکوک به نظر میرسد.
- Weighted distribution — بخشی از درخواستها به IPهای گرانتر و قابل اعتمادتر (به عنوان مثال، موبایل) هدایت میشود، و بخشی به IPهای ارزانتر دیتاسنتر، بسته به دامنهای که پردازش میشود.
- Adaptive throttling — سرعت ارسال وظایف به طور خودکار کاهش مییابد، اگر درصد خطاهای 429 یا کپچا افزایش یابد و دوباره به حالت عادی برمیگردد، زمانی که خطاها از بین میروند.
برای وظایفی که پنهان شدن به عنوان یک کاربر زنده مهم است — به عنوان مثال، گرم کردن و بررسی حسابها در Instagram یا TikTok از طریق مرورگرهای ضد شناسایی مانند Dolphin Anty یا AdsPower — بهتر است از sticky session با پروکسیهای موبایل استفاده کنید: اینها کمتر تحت نظر سیستمهای ضد ربات قرار میگیرند و مانند کاربران عادی اینترنت موبایل رفتار میکنند.
برای جمعآوری دادههای انبوه از وبسایتها، جایی که سرعت و حجم مهم است و نه اعتماد به الگوریتم، round-robin با پروکسیهای مسکونی تعادل بین ثبات و قیمت را فراهم میکند: IPها به عنوان کاربران واقعی به نظر میرسند، اما میتوان آنها را به طور مکرر بدون خطر برای یک جلسه خاص تغییر داد.
نظارت بر خطاهای 429 و مسدودیتها
صف وظایف بدون نظارت — مانند یک جعبه سیاه است: دشوار است بفهمید چرا بخشی از وظایف شکست میخورد و کدام IPها در حال حاضر تحت نظر هستند. حداقل مجموعهای از معیارها که باید پیگیری شوند:
- درصد پاسخها با کد 429 و 403 برای هر پروکسی به صورت جداگانه
- زمان پاسخ متوسط — افزایش تأخیر اغلب قبل از مسدودیت رخ میدهد
- تعداد تلاشهای مجدد (retries) برای هر وظیفه
- طول صف وظایف در زمان واقعی — اگر در حال افزایش است، کارگران نمیتوانند به خوبی عمل کنند
در Celery برای این کار میتوان از Flower استفاده کرد — رابط وب برای نظارت بر وظایف و کارگران در زمان واقعی. در BullMQ، Bull Board با عملکرد مشابه وجود دارد. برای RQ، RQ Dashboard وجود دارد. همه این سه ابزار وضعیت وظایف، تعداد تلاشهای ناموفق را نشان میدهند و به سرعت درک میکنند که کدام پروکسی از استخر باید به طور موقت از چرخش خارج شود.
یک عمل خوب — به طور خودکار پروکسی را به عنوان "در قرنطینه" به مدت 15-30 دقیقه پس از چند خطای متوالی 429 از این IP علامتگذاری کنید و تنها پس از یک درخواست آزمایشی موفق آن را به استخر برگردانید. این کار از وضعیتی جلوگیری میکند که IP مسدود شده به دریافت وظایف ادامه دهد و زمان کارگران را به هدر دهد.
نتیجهگیری
صف وظایف مشکل مقیاسپذیری درخواستها را حل میکند، اما به تنهایی از محدودیت IP محافظت نمیکند — برای این کار به چرخش پروکسیهای مدبرانه در داخل Celery، RQ یا BullMQ نیاز است. انتخاب ابزار بستگی به استک دارد: پروژههای Python بیشتر از Celery برای تولید و RQ برای وظایف ساده استفاده میکنند، در حالی که توسعهدهندگان Node.js BullMQ را با محدودکننده نرخ داخلی آن انتخاب میکنند.
صرف نظر از صف انتخاب شده، نتیجه به شدت به کیفیت خود استخر پروکسی بستگی دارد. اگر وظیفه — استخراج سریع و انبوه باشد، به پروکسیهای دیتاسنتر برای سرعت توجه کنید، و برای کار با شبکههای اجتماعی و بازارها، جایی که وفاداری سیستمهای ضد ربات مهم است، پروکسیهای مسکونی یا موبایل مناسب هستند — اینها درصد خطاهای 429 و مسدودیت را کاهش میدهند، که به طور مستقیم تعداد تلاشهای مجدد را کاهش میدهد و اجرای کل صف وظایف را تسریع میکند.