Когда парсер переезжает с одного сервера на кластер из нескольких воркеров, возникает новая проблема — несколько машин начинают использовать один и тот же пул прокси одновременно. Без координации это приводит к дублированию 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 не возникнет, потому что блокировка атомарна на уровне хранилища, а не на уровне отдельного сервера.
Sticky-сессии и привязка прокси к задачам
Для некоторых задач простой выдачи "любого свободного прокси" недостаточно. Если парсер эмулирует поведение пользователя — например, последовательно просматривает карточки товара, добавляет в корзину, переходит по страницам каталога — важно, чтобы все запросы одной сессии шли с одного и того же IP. Иначе сайт увидит резкую смену гео или устройства посреди сессии и заблокирует её как подозрительную.
Для этого вводится понятие sticky-сессии: прокси привязывается не к одному запросу, а к конкретной задаче или пользовательской сессии на время её жизни. В 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 запросов.
- Процент успешных запросов (success rate) по серверам и по пулу в целом.
- Распределение нагрузки — сколько запросов обработал каждый сервер за период.
Для визуализации удобно складывать эти метрики в Prometheus через простой экспортер, который раз в минуту читает данные из Redis и отдаёт их в формате, понятном Grafana. Это позволяет быстро увидеть просадку success rate по конкретному гео или типу прокси и вовремя добавить свежие IP в пул, прежде чем парсер полностью остановится.
Отдельно стоит мониторить пики банов при запуске новых серверов в кластере — если при масштабировании с 3 до 6 серверов резко вырос процент банов, скорее всего проблема в шардировании: новые серверы начали пересекаться по диапазонам прокси со старыми. Это первый признак того, что нужно переходить с деления по модулю на консистентное хеширование.
Чек-лист запуска распределённого парсера
Перед тем как разворачивать парсер на несколько серверов, пройдитесь по следующим пунктам:
- Выбран механизм координации пула: статическое деление, консистентное хеширование или централизованный брокер на Redis.
- Реализована атомарная блокировка прокси, исключающая использование одного IP двумя серверами одновременно.
- Настроен TTL на блокировку и на бан, чтобы прокси не "зависали" в заблокированном состоянии навсегда при падении воркера.
- Для задач, требующих эмуляции сессии пользователя, настроена привязка sticky-прокси на весь срок задачи.
- Настроена детекция банов не только по HTTP-коду, но и по содержимому ответа.
- Включён мониторинг success rate, активных и забаненных прокси по всему кластеру.
- Пул прокси разделён по типам задач: дата-центровые IP — для менее чувствительных запросов, резидентные или мобильные прокси — для сессий, где важна максимальная похожесть на реального пользователя.
- Проверена нагрузка на хранилище состояния (Redis) — при большом количестве серверов и высокой частоте запросов может понадобиться кластер Redis вместо одного инстанса.
Заключение
Главная причина конфликтов при переходе парсера на несколько серверов — отсутствие единого источника правды о состоянии пула прокси. Как только появляется централизованный брокер с атомарными блокировками (на Redis или аналогичном хранилище), проблема дублирования IP и рассинхронизации банов исчезает сама собой. Дальше остаётся выбрать стратегию шардирования под масштаб вашего кластера: для фиксированного числа серверов достаточно простого деления, а для динамически растущей инфраструктуры лучше сразу закладывать консистентное хеширование.
Если ваш распределённый парсер работает с маркетплейсами или соцсетями, где важна правдоподобность трафика на уровне отдельной сессии, стоит разделить пул по типу задач: для массового сканирования каталогов подойдут недорогие прокси дата-центров, а для сессий с эмуляцией поведения пользователя — резидентные прокси с реальными домашними IP, которые заметно снижают процент банов в чувствительных к фроду системах.