وقتی تجزیه کننده از یک سرور به یک خوشه از چندین کارگر منتقل می شود، یک مشکل جدید ایجاد می شود - چندین ماشین همزمان از یک استخر پروکسی استفاده می کنند. بدون هماهنگی، این منجر به تکرار IP در درخواست ها، تجاوز از محدودیت ها برای یک آدرس و ممنوعیت های گسترده می شود. در این مقاله بررسی خواهیم کرد که چگونه به درستی پروکسی را بین سرورها تقسیم کنیم، کدام طرح های توزیع بهترین عملکرد را دارند و چگونه این را در عمل با کد در Python و Redis پیاده سازی کنیم.
چرا پروکسی ها در تجزیه کننده توزیع شده تضاد دارند
یک وضعیت معمولی را تصور کنید: شما یک استخر از 500 پروکسی و سه سرور دارید که هر کدام 50 کارگر موازی برای تجزیه Wildberries، Ozon یا Avito را اجرا می کنند. اگر هر سرور به طور مستقل پروکسی را از لیست مشترک بدون هماهنگی بگیرد، دو کارگر در ماشین های مختلف می توانند به طور همزمان از یک IP به یک دامنه یکسان درخواست ارسال کنند. برای سیستم های ضد ربات، این به عنوان یک فرکانس غیرعادی بالا از درخواست ها از یک آدرس به نظر می رسد - و IP در عرض چند دقیقه ممنوع می شود.
مشکل دوم - توزیع نامتعادل بار. اگر پروکسی ها به سادگی "هر طور که پیش می آید" تقسیم شوند، یک سرور ممکن است سریع ترین و تازه ترین IP ها را به خود بگیرد، در حالی که دیگری باقی مانده با شهرت خراب را می گیرد. در نتیجه، سرعت تجزیه در سرورها به طور قابل توجهی متفاوت خواهد بود و درصد خطاها نیز همینطور.
مشکل سوم - از دست دادن وضعیت. اگر پروکسی در یک سرور ممنوع شود، سایر سرورها از این موضوع مطلع نخواهند شد و به استفاده از آن ادامه خواهند داد تا خودشان نیز ممنوع شوند. بدون ثبت مرکزی وضعیت پروکسی، هر سرور "آمریکا را دوباره کشف می کند" و درخواست ها را بیهوده هدر می دهد.
راه حل هر سه مشکل - معرفی یک مکانیزم هماهنگی واحد: یا از طریق یک ذخیره مرکزی وضعیت (Redis، PostgreSQL، Consul) یا از طریق تقسیم تعیین شده استخر از قبل، بدون تداخل دامنه ها بین سرورها.
معماری تجزیه کننده توزیع شده
قبل از تقسیم پروکسی، باید با معماری کلی خوشه تعیین کنید. دو رویکرد اساسی وجود دارد.
طرح استاتیک (shared-nothing). استخر پروکسی از قبل بین سرورها تقسیم می شود، یک بار در شروع. هر سرور بخش غیر تداخلی خود را دریافت می کند و به طور مستقل با آن کار می کند، بدون اینکه به سایر سرورها مراجعه کند. مزیت - حداکثر سادگی و مقاومت در برابر خطا: سقوط یک سرور بر سایرین تأثیر نمی گذارد. معایب - اگر نیمی از پروکسی ها در یک سرور ممنوع شوند، نمی تواند IP های تازه را از همسایه "قرض بگیرد".
طرح دینامیک (shared-state). همه سرورها به یک ذخیره مشترک (معمولاً Redis) مراجعه می کنند که وضعیت هر پروکسی را ذخیره می کند و آن را بر اساس درخواست با قفل ارائه می دهد. مزیت - استفاده یکنواخت از استخر و واکنش فوری به ممنوعیت ها. معایب - ذخیره به یک نقطه شکست تبدیل می شود، به منطق جداگانه ای در صورت عدم دسترسی نیاز است.
در عمل برای تجزیه بازارها و شبکه های اجتماعی معمولاً یک ترکیب انتخاب می شود: بروکر در Redis پروکسی را با قفل برای مدت زمان کوتاه (TTL) ارائه می دهد و در صورت عدم دسترسی Redis، سرورها به طور موقت به استخر محلی fallback سوئیچ می شوند. این رویکرد قابلیت اطمینان طرح استاتیک را با انعطاف پذیری طرح دینامیک ترکیب می کند.
روش های تقسیم استخر پروکسی بین سرورها
بیایید الگوریتم های خاصی را بررسی کنیم که می توان برای تقسیم استخر استفاده کرد.
تقسیم ساده بر اساس مدول (round-robin sharding). پروکسی ها را از 0 تا N-1 شماره گذاری کردیم، سرور با شماره i تمام پروکسی هایی را که index % تعداد_سرورها == i می گیرد. ساده است، اما اضافه کردن یا حذف سرورها را به خوبی تحمل نمی کند - با تغییر تعداد ماشین ها تقریباً تمام استخر دوباره توزیع می شود.
هشینگ سازگار. به هر سرور و هر پروکسی یک موقعیت در حلقه هش ها اختصاص داده می شود (معمولاً بر اساس هش IP آدرس یا ID سرور). پروکسی به نزدیک ترین سرور در حلقه متصل می شود. هنگام اضافه کردن یا حذف سرور، تنها بخش کوچکی از پروکسی ها دوباره توزیع می شوند، نه کل استخر - این یک مزیت کلیدی برای خوشه هایی است که به طور دینامیک مقیاس می شوند (به عنوان مثال، در Kubernetes با مقیاس خودکار).
بروکر مرکزی با قفل ها. به جای اینکه پروکسی را به طور سخت به سرور متصل کنیم، همه سرورها از یک سرویس مشترک (Redis، RabbitMQ) پروکسی "آزاد" درخواست می کنند که آن را با قفل موقتی ارائه می دهد. پس از استفاده، پروکسی به استخر برمی گردد. این گزینه انعطاف پذیرترین است، به ویژه اگر بار در سرورها نامتعادل باشد.
تقسیم بر اساس دامنه ها یا وظایف. اگر تجزیه کننده به طور همزمان با چندین منبع (به عنوان مثال، Wildberries، Ozon و Avito به طور موازی) کار می کند، منطقی است که نه تنها پروکسی ها را بین سرورها تقسیم کنیم، بلکه پروکسی ها را بین منابع نیز تقسیم کنیم - تا یک IP یکسان به طور همزمان در چندین پلتفرم ظاهر نشود. این خطر قرار گرفتن IP در لیست سیاه مشترک سیستم های ضد تقلب را کاهش می دهد، اگر چنین پلتفرم هایی داده ها را مبادله کنند.
جدول: مقایسه روش های شاردینگ پروکسی
| روش | پیچیدگی | مقاومت در برابر مقیاس | کی استفاده کنیم |
|---|---|---|---|
| تقسیم بر اساس مدول | پایین | بد | تعداد ثابت سرورها |
| هشینگ سازگار | متوسط | خوب | مقیاس خودکار خوشه |
| بروکر مرکزی | متوسط-بالا | عالی | بار نامتعادل، ترافیک بالا |
| تقسیم بر اساس دامنه ها | پایین | متوسط | تجزیه چندین پلتفرم به طور همزمان |
پیاده سازی بروکر پروکسی در Redis
عملی ترین گزینه برای اکثر تجزیه کننده های توزیع شده - بروکر مرکزی در Redis است. Redis به دلیل سرعت بالا، پشتیبانی از عملیات اتمیک و TTL "از جعبه" مناسب است. ایده ساده است: تمام پروکسی های موجود در Redis Set قرار دارند و هنگام ارائه پروکسی به یک کارگر خاص، آن برای مدت معین از طریق کلید با TTL قفل می شود.
در زیر یک مثال عملی از بروکر در Python با کتابخانه redis-py آورده شده است. این پیاده سازی ارائه پروکسی آزاد با قفل، آزادسازی پس از استفاده و علامت گذاری پروکسی به عنوان ممنوع شده را انجام می دهد.
import redis
import time
import random
r = redis.Redis(host="localhost", port=6379, db=0, decode_responses=True)
PROXY_POOL_KEY = "proxy:pool" # مجموعه تمام پروکسی های کاری
LOCK_PREFIX = "proxy:lock:" # پیشوند قفل پروکسی خاص
BANNED_PREFIX = "proxy:banned:" # پیشوند علامت گذاری ممنوعیت
LOCK_TTL = 30 # ثانیه، برای چه مدت پروکسی را قفل می کنیم
BAN_TTL = 3600 # ثانیه، برای چه مدت پروکسی را ممنوع می دانیم
def load_pool(proxy_list):
"""لیست پروکسی را یک بار در شروع خوشه به Redis بارگذاری می کنیم."""
if proxy_list:
r.sadd(PROXY_POOL_KEY, *proxy_list)
def get_free_proxy(server_id, max_attempts=20):
"""هر سرور این تابع را برای دریافت پروکسی آزاد فراخوانی می کند."""
candidates = list(r.smembers(PROXY_POOL_KEY))
random.shuffle(candidates)
for proxy in candidates[:max_attempts]:
if r.exists(BANNED_PREFIX + proxy):
continue
lock_key = LOCK_PREFIX + proxy
# تنظیم اتمیک قفل، اگر پروکسی آزاد باشد
acquired = r.set(lock_key, server_id, nx=True, ex=LOCK_TTL)
if acquired:
return proxy
return None # پروکسی آزاد پیدا نشد، نیاز به fallback است
def release_proxy(proxy):
"""پروکسی را پس از درخواست موفق آزاد می کنیم."""
r.delete(LOCK_PREFIX + proxy)
def mark_banned(proxy):
"""پروکسی را به عنوان ممنوع شده برای BAN_TTL ثانیه علامت گذاری می کنیم، قفل را حذف می کنیم."""
r.set(BANNED_PREFIX + proxy, 1, ex=BAN_TTL)
r.delete(LOCK_PREFIX + proxy)
r.srem(PROXY_POOL_KEY, proxy) # به طور موقت از استخر حذف می کنیم
def return_to_pool_if_recovered():
"""وظیفه دوره ای: پروکسی هایی را که ممنوعیت آنها منقضی شده است، برمی گردانیم."""
for proxy in r.scan_iter(match=BANNED_PREFIX + "*"):
clean_proxy = proxy.replace(BANNED_PREFIX, "")
if not r.exists(BANNED_PREFIX + clean_proxy):
r.sadd(PROXY_POOL_KEY, clean_proxy)
نکته کلیدی در اینجا استفاده از SET ... NX EX در Redis است. این دستور اتمیک است: اگر دو سرور به طور همزمان سعی کنند یک پروکسی یکسان را قفل کنند، تنها یکی از آنها موفق خواهد شد، دیگری رد خواهد شد و به کاندیدای بعدی در لیست خواهد رفت. این دقیقاً تضادها را بین سرورها در دسترسی همزمان به استخر مشترک حذف می کند.
مثال استفاده از بروکر در کارگر تجزیه کننده:
import requests
def fetch_with_proxy(url, server_id):
proxy = get_free_proxy(server_id)
if not proxy:
raise RuntimeError("هیچ پروکسی آزاد در استخر وجود ندارد")
proxies = {"http": f"http://{proxy}", "https": f"http://{proxy}"}
try:
response = requests.get(url, proxies=proxies, timeout=10)
if response.status_code in (403, 429):
mark_banned(proxy)
else:
release_proxy(proxy)
return response
except requests.exceptions.RequestException:
mark_banned(proxy)
raise
این طرح به راحتی مقیاس پذیر است - می توان 10 یا 50 سرور اضافه کرد، آنها به یک نمونه Redis یکسان مراجعه خواهند کرد و تضادهای IP ایجاد نخواهد شد، زیرا قفل در سطح ذخیره اتمیک است، نه در سطح یک سرور خاص.
جلسات چسبنده و پیوند پروکسی به وظایف
برای برخی از وظایف، ارائه "هر پروکسی آزاد" کافی نیست. اگر تجزیه کننده رفتار کاربر را شبیه سازی کند - به عنوان مثال، به طور متوالی کارت های محصول را مرور کند، به سبد خرید اضافه کند، به صفحات کاتالوگ برود - مهم است که تمام درخواست های یک جلسه از یک IP یکسان ارسال شود. در غیر این صورت، سایت تغییر ناگهانی جغرافیا یا دستگاه را در وسط جلسه مشاهده کرده و آن را به عنوان مشکوک مسدود می کند.
برای این منظور مفهوم جلسه چسبنده معرفی می شود: پروکسی به یک درخواست خاص پیوند نمی خورد، بلکه به یک وظیفه خاص یا جلسه کاربر در طول عمر آن پیوند می خورد. در Redis این از طریق یک کلید جداگانه به شکل session:{task_id} -> proxy با TTL برابر با مدت زمان مورد انتظار جلسه پیاده سازی می شود.
SESSION_PREFIX = "proxy:session:"
SESSION_TTL = 600 # 10 دقیقه برای جلسه
def get_sticky_proxy(task_id, server_id):
existing = r.get(SESSION_PREFIX + task_id)
if existing:
return existing
proxy = get_free_proxy(server_id)
if proxy:
r.set(SESSION_PREFIX + task_id, proxy, ex=SESSION_TTL)
return proxy
برای وظایفی که نیاز به جلسات طولانی دارند - به عنوان مثال، مرور کاتالوگ Wildberries با شبیه سازی رفتار خریدار واقعی - استفاده از پروکسی هایی که بیشترین شباهت را به یک کاربر عادی دارند، حیاتی است. در اینجا پروکسی های مسکونی به خوبی عمل می کنند، زیرا به IP های واقعی خانگی متصل هستند و نسبت به آدرس های مراکز داده کمتر مشکوک به سیستم های ضد ربات بازارها هستند.
مدیریت ممنوعیت ها و چرخش در خوشه
در یک سیستم توزیع شده، مهم است که اطلاعات مربوط به ممنوعیت پروکسی به سرعت برای همه سرورها در دسترس باشد، نه فقط آن سروری که این ممنوعیت را دریافت کرده است. در مثال بالا، این وظیفه بر عهده تابع mark_banned است - به محض اینکه یک سرور 403 یا 429 را از سایت هدف ثبت کند، پروکسی از استخر مشترک در Redis حذف می شود و سایر سرورها به طور خودکار از دریافت آن متوقف می شوند.
برای تعیین ممنوعیت، باید نه تنها کد HTTP پاسخ را تحلیل کرد، بلکه محتوای صفحه را نیز بررسی کرد - برخی از سایت ها 200 OK را برمی گردانند، اما با کد کپچا یا صفحه ای به جای محتوای واقعی. منطق تشخیص ممنوعیت معمولاً به این شکل است:
- کدهای HTTP 403، 429، 503 - نشانه واضحی از مسدود شدن یا محدودیت نرخ.
- ریدایرکت به صفحه کپچا یا احراز هویت - بررسی URL پس از درخواست.
- کاهش ناگهانی اندازه پاسخ یا عدم وجود انتخابگرهای CSS مورد انتظار - نشانه ای از صفحه مسدود کننده.
- زمان انتظار اتصال - اغلب به این معنی است که پروکسی به طور کلی دیگر پاسخ نمی دهد.
پس از ثبت ممنوعیت، منطقی است که پروکسی را به سادگی برای همیشه از استخر حذف نکنیم، بلکه آن را در "قرنطینه" با TTL قرار دهیم (مانند مثال - BAN_TTL)، که پس از آن پروکسی به طور خودکار به استخر باز می گردد. بسیاری از ممنوعیت ها در بازارها و شبکه های اجتماعی موقتی هستند و پس از 30-60 دقیقه برداشته می شوند.
اگر ممنوعیت ها حتی با توزیع یکنواخت بار خیلی زیاد اتفاق بیفتند، این معمولاً نشانه ای است که خود استخر پروکسی کیفیت پایینی دارد یا اندازه کافی برای حجم درخواست های مورد نیاز نیست. در چنین مواردی، گسترش استخر با استفاده از پروکسی های مراکز داده برای وظایف کمتر حساس (به عنوان مثال، بررسی در دسترس بودن صفحات) و استفاده از IP های مسکونی گران تر فقط برای جلسات بحرانی به کاهش درصد کلی ممنوعیت ها بدون افزایش هزینه ها برای کل ترافیک کمک می کند.
نظارت بر سلامت استخر پروکسی
در یک خوشه از چندین سرور، به راحتی تصویر کلی را از دست می دهید: چند پروکسی در حال حاضر فعال هستند، چندتا در ممنوعیت هستند، میانگین سرعت پاسخ هر کدام چقدر است. بدون نظارت، سخت است که بفهمید چرا تجزیه کند شده است - آیا یک سرور سقوط کرده است یا پروکسی های زنده تمام شده اند.
حداقل مجموعه ای از معیارهایی که باید در Redis یا یک پایگاه داده جداگانه جمع آوری شود:
- تعداد پروکسی های فعال در استخر در حال حاضر.
- تعداد پروکسی های ممنوع و میانگین زمان تا بازیابی آنها.
- میانگین زمان پاسخ برای هر پروکسی در آخرین N درخواست.
- درصد درخواست های موفق (نرخ موفقیت) بر اساس سرورها و بر اساس کل استخر.
- توزیع بار - هر سرور در یک دوره چه تعداد درخواست را پردازش کرده است.
برای تجسم، راحت است که این معیارها را در Prometheus از طریق یک اکسپورتر ساده که هر دقیقه داده ها را از Redis می خواند و آنها را در فرمت قابل فهم برای Grafana ارائه می دهد، جمع آوری کنید. این به سرعت امکان مشاهده کاهش نرخ موفقیت در یک جغرافیا خاص یا نوع پروکسی را فراهم می کند و به موقع IP های تازه را به استخر اضافه می کند، قبل از اینکه تجزیه کننده به طور کامل متوقف شود.
به طور جداگانه، باید اوج های ممنوعیت را هنگام راه اندازی سرورهای جدید در خوشه نظارت کرد - اگر هنگام مقیاس بندی از 3 به 6 سرور، درصد ممنوعیت به شدت افزایش یابد، احتمالاً مشکل در شاردینگ است: سرورهای جدید شروع به تداخل در دامنه های پروکسی با سرورهای قدیمی کرده اند. این اولین نشانه ای است که باید از تقسیم بر اساس مدول به هشینگ سازگار منتقل شوید.
چک لیست راه اندازی تجزیه کننده توزیع شده
قبل از اینکه تجزیه کننده را بر روی چندین سرور راه اندازی کنید، از طریق موارد زیر عبور کنید:
- مکانیزم هماهنگی استخر انتخاب شده است: تقسیم استاتیک، هشینگ سازگار یا بروکر مرکزی در Redis.
- قفل اتمیک پروکسی پیاده سازی شده است که استفاده از یک IP توسط دو سرور به طور همزمان را حذف می کند.
- TTL برای قفل و ممنوعیت تنظیم شده است، تا پروکسی ها در حالت قفل شده برای همیشه در صورت سقوط کارگر "آویزان" نشوند.
- برای وظایفی که نیاز به شبیه سازی جلسه کاربر دارند، پیوند پروکسی چسبنده برای تمام مدت وظیفه تنظیم شده است.
- تشخیص ممنوعیت نه تنها بر اساس کد HTTP، بلکه بر اساس محتوای پاسخ تنظیم شده است.
- نظارت بر نرخ موفقیت، پروکسی های فعال و ممنوع در کل خوشه فعال شده است.
- استخر پروکسی بر اساس نوع وظایف تقسیم شده است: IP های مراکز داده - برای درخواست های کمتر حساس، پروکسی های مسکونی یا پروکسی های موبایل - برای جلساتی که شباهت حداکثری به کاربر واقعی مهم است.
- بار روی ذخیره وضعیت (Redis) بررسی شده است - با تعداد زیادی سرور و فرکانس بالای درخواست ها ممکن است نیاز به خوشه Redis به جای یک نمونه واحد باشد.
نتیجه گیری
دلیل اصلی تضادها هنگام انتقال تجزیه کننده به چندین سرور، عدم وجود یک منبع حقیقت واحد در مورد وضعیت استخر پروکسی است. به محض اینکه یک بروکر مرکزی با قفل های اتمیک (در Redis یا یک ذخیره مشابه) ایجاد می شود، مشکل تکرار IP و عدم همزمانی ممنوعیت ها به طور خودکار حل می شود. سپس تنها باید استراتژی شاردینگ را بر اساس مقیاس خوشه خود انتخاب کنید: برای تعداد ثابت سرورها، تقسیم ساده کافی است، و برای زیرساخت های در حال رشد دینامیک، بهتر است از ابتدا هشینگ سازگار را در نظر بگیرید.
اگر تجزیه کننده توزیع شده شما با بازارها یا شبکه های اجتماعی کار می کند که در سطح یک جلسه خاص، اعتبار ترافیک مهم است، بهتر است استخر را بر اساس نوع وظایف تقسیم کنید: برای اسکن انبوه کاتالوگ ها، پروکسی های ارزان قیمت مراکز داده مناسب هستند، و برای جلسات با شبیه سازی رفتار کاربر - پروکسی های مسکونی با IP های واقعی خانگی که به طور قابل توجهی درصد ممنوعیت ها را در سیستم های حساس به تقلب کاهش می دهند، مناسب هستند.