Большинство ротаторов прокси отвечают не на тот вопрос. Они отвечают на "чья сейчас очередь?", тогда как вопрос, от которого зависит, дойдёт ли ваш конвейер до конца, звучит так: "какой маршрут должен взять этот запрос?" Круговой перебор даёт каждому маршруту равную долю трафика, и это верно только тогда, когда все маршруты одинаково хороши. Пулы никогда не бывают такими аккуратными: один маршрут быстрый, пока его не заблокируют, другой медленный, но не падает никогда, третий падает всплесками и восстанавливается.
Как только вы это принимаете, ротация перестаёт быть задачей планирования и становится задачей обратной связи. Ротатор наблюдает, что происходит с каждым запросом, ведёт текущую оценку поведения каждого маршрута и позволяет этим оценкам направлять следующий выбор. В этом и состоит идея оценки здоровья: пул сообщает, что он делает, а политика маршрутизации слушает.
Это руководство собирает такой ротатор на Python. Экспоненциально взвешенные оценки отслеживают успех и задержку, предохранитель берёт на себя резкие отказы, для которых средние слишком медлительны, а бенчмарк измеряет результат против обычного кругового перебора. Один из маршрутов это Crawlbase Smart AI Proxy, и он здесь именно потому, что снимает целый слой задачи: вы перестаёте оценивать отдельные резидентные IP и начинаете оценивать один маршрут, который сам занимается ротацией и обходом антибот-защиты.
- Разделите выполнение, здоровьеи выбор. Маршрут выполняет запрос, модель здоровья оценивает, политика решает. Каждую часть можно заменить или измерить отдельно.
- Две EWMA несут непрерывный сигнал: одна для успеха, одна для задержки. Обновляйте задержку только на успешных запросах, потому что неудавшийся запрос ничего не говорит о том, насколько маршрут быстр, когда он работает.
- Предохранитель делает то, чего не может среднее: серия подряд идущих отказов должна убрать маршрут сразу, а не постепенно.
- Здоровье перемножается, а не складывается:
success_ewma / (1 + latency_ewma). Маршрут должен быть и надёжным, и отзывчивым, чтобы получить высокую оценку. - Держите показатель степени при выборе умеренным, чтобы восстанавливающийся маршрут всё ещё получал редкий трафик и мог доказать, что вылечился.
- Круговой перебор это контроль, а не соломенное чучело. Без него вы не сможете показать, что обратная связь вообще что-то дала.
Почему ротации нужна оценка здоровья
Пул прокси не однороден и не стоит на месте. Маршруты различаются по задержке, надёжности и по тому, как с ними обходится конкретная цель, и всё это может измениться прямо во время работы задачи. Считать их взаимозаменяемыми значит сделать политику слепой ровно к тем условиям, которые и имеют значение.
Ротатору, учитывающему здоровье, нужны три сигнала, и это не один и тот же сигнал с разной чувствительностью:
- Живость. Какие маршруты сейчас успешны и от каких трафик должен уходить.
- Задержка. Среди работающих маршрутов предпочитайте отзывчивые. Успешный запрос, занявший девять секунд, всё равно стоил вам девяти секунд.
- Устойчивость отказов. Повторяющиеся отказы должны выводить маршрут из обычного трафика и возвращать его через контролируемую пробу, а не мгновенным возвратом в пул.
Круговой перебор не даёт ничего из этого. Он раскладывает запросы поровну, здоров маршрут, медленен или мёртв. Именно это и делает его правильным контролем: прогоните обе политики на одной и той же нагрузке, и разница будет ценой обратной связи.
Форма системы
Три зоны ответственности, намеренно разведённые. Маршрут выполняет запрос и сообщает нормализованный результат: успех или нет, HTTP-статус, длительность. Модель здоровья превращает этот поток результатов в оценку. Политика читает оценки и выбирает следующий маршрут. Поскольку они разделены, вы можете заменить политику, не трогая транспорт, или сравнить две политики на одинаковых маршрутах.
Smart AI Proxy заслуживает своё место на этой картинке тем, что поглощает целый слой. Он предоставляет одну точку входа и берёт на себя ротацию резидентных IP и обход антибот-защиты, так что ваше приложение оценивает маршрут, а не парк адресов. Ваша политика по-прежнему решает, какой путь получения данных заслуживает трафика.
Каждый фрагмент ниже взят из сопутствующего репозитория ScraperHub/smart-ai-proxy-rotation-in-python-health-scoring-at-scale, где лежит рабочая реализация в final/ и промежуточные контрольные точки в steps/.
Окружение
Python 3.11 или новее и учётная запись Crawlbase для проксированного маршрута. Здесь достаточно вашего Normal token; JavaScript token нужен лишь тогда, когда цели требуется рендеринг, чтобы выдать содержимое, а это отдельный от маршрутизации вопрос. Оба находятся в настройках консоли.
git clone https://github.com/ScraperHub/smart-ai-proxy-rotation-in-python-health-scoring-at-scale.git cd smart-ai-proxy-rotation-in-python-health-scoring-at-scale/final python -m venv .venv && source .venv/bin/activate # Windows: .venv\Scripts\activate pip install -r requirements.txt cp .env.example .env # then set CRAWLBASE_TOKEN
Токен принадлежит окружению, а не исходному коду, и это обычная гигиена, но заодно проводит ту границу конфигурации, на которую опирается следующий раздел.
Конфигурация как контракт
Конфигурация это первый контракт времени выполнения. Ротатор должен отказаться стартовать, когда обязательная зависимость отсутствует, а не подняться с непригодным маршрутом и обнаружить это уже под нагрузкой.
def _required(name: str) -> str: value = os.environ.get(name) if not value: raise RuntimeError(f"Missing required environment variable: {name}") return value @dataclass(frozen=True) class Config: crawlbase_token: str = field(default_factory=lambda: _required("CRAWLBASE_TOKEN")) smart_proxy_host: str = field(default_factory=lambda: os.environ.get("SMART_PROXY_HOST", "smartproxy.crawlbase.com")) smart_proxy_port: int = field(default_factory=lambda: int(os.environ.get("SMART_PROXY_PORT", "8012"))) success_decay: float = field(default_factory=lambda: float(os.environ.get("SUCCESS_DECAY", "0.3"))) latency_decay: float = field(default_factory=lambda: float(os.environ.get("LATENCY_DECAY", "0.3"))) breaker_threshold: int = field(default_factory=lambda: int(os.environ.get("BREAKER_THRESHOLD", "3"))) breaker_cooldown_s: float = field(default_factory=lambda: float(os.environ.get("BREAKER_COOLDOWN_S", "15")))
Обратите внимание, что говорит это разделение. CRAWLBASE_TOKEN это учётные данные, и они обязательны. Всё остальное это политика: насколько быстро реагирует оценка успеха, насколько быстро реагирует оценка задержки, сколько подряд идущих отказов открывают предохранитель и как долго открытый маршрут остаётся вне игры. Замороженный dataclass означает, что путь маршрутизации читает фиксированную конфигурацию, а не лезет за переменными окружения в момент запроса.
База, которую нужно превзойти
Маршрут здесь это всё, что умеет получить URL и сообщить, что произошло. Реализация даёт два: прямое соединение и Smart AI Proxy.
class CrawlbaseRoute(Route): name = "crawlbase-smart-proxy" def __init__(self, config: Config) -> None: proxy_url = ( f"http://{config.crawlbase_token}:@" f"{config.smart_proxy_host}:{config.smart_proxy_port}" ) self._client = httpx.Client(proxy=proxy_url, verify=False, timeout=config.request_timeout_s, follow_redirects=True)
Токен передаётся как имя пользователя прокси с пустым паролем, именно так аутентифицирует Smart AI Proxy. verify=False это не срезанный угол, и это стоит понять, а не скопировать: прокси терминирует TLS, чтобы добавить собственные заголовки, поэтому вашему клиенту предъявляется сертификат Crawlbase, а не цели, и строгая проверка его отвергла бы. Это документированное поведение точки входа, а не обход неверной настройки.
DirectRoute это тот же интерфейс без прокси. На незащищённой цели он часто быстрее и деградирует первым, когда появляются антибот-механизмы, что делает его полезным источником ровно того сигнала об отказах, который и должна потреблять модель здоровья.
Базовая политика намеренно игнорирует каждый из этих сигналов:
class NaiveRotator: def __init__(self, routes: list[Route]) -> None: self._routes = routes self._cycle = itertools.cycle(routes) def fetch(self, url: str) -> tuple[str, FetchResult]: route = next(self._cycle) return route.name, route.fetch(url)
Это вся политика. Она детерминирована и совершенно справедлива, и справедливость и есть проблема: она сохраняется после того, как маршруты перестали быть одинаково хорошими.
Оценка здоровья: два средних и предохранитель
Экспоненциально взвешенное скользящее среднее подходит здесь потому, что оно придаёт больший вес недавним наблюдениям и постепенно забывает старые. Маршрут, упавший десять минут назад, не должен наказываться вечно; маршрут, начавший падать тридцать секунд назад, должен упасть быстро. Коэффициент затухания это регулятор между этими крайностями.
def observe(self, ok: bool, latency_s: float) -> None: self.samples += 1 outcome = 1.0 if ok else 0.0 self.success_ewma = self.success_decay * outcome + (1 - self.success_decay) * self.success_ewma if ok: self.latency_ewma_s = self.latency_decay * latency_s + (1 - self.latency_decay) * self.latency_ewma_s self.consecutive_failures = 0 if self.breaker_state is BreakerState.HALF_OPEN: self.breaker_state = BreakerState.CLOSED else: self.consecutive_failures += 1 if self.consecutive_failures >= self.breaker_threshold: self.breaker_state = BreakerState.OPEN self.opened_at = time.monotonic()
Два решения здесь приняты намеренно. Задержка обновляется только при успехе, потому что время неудавшегося запроса описывает отказ, а не отзывчивость маршрута в рабочем состоянии; учитывать его значило бы выдать быстро падающий маршрут за быстрый. А устойчивость передана предохранителю, а не среднему, потому что подряд идущие отказы это свидетельство иной природы, чем дрейфующее среднее, и заслуживают более резкого ответа.
Затем две оценки схлопываются в одно число:
def value(self) -> float: if self.breaker_state is BreakerState.OPEN: return 0.0 latency_term = 1.0 / (1.0 + self.latency_ewma_s) return self.success_ewma * latency_term
Умножение вместо сложения это и есть проектное решение, и оно кодирует конъюнкцию: маршрут должен быть и надёжным, и отзывчивым, чтобы получить высокую оценку. Сложите слагаемые, и быстрый маршрут, который падает большую часть времени, всё равно наберёт приличную оценку за счёт своей половины по задержке. Перемножьте их, и близкая к нулю доля успеха утянет всё значение к нулю, каким бы быстрым ни был отказ. Открытый предохранитель коротко замыкает значение ровно в 0.0, что делает вывод маршрута из игры явным, а не побочным.
Петля, по одному запросу за раз
Каждый запрос проходит одни и те же четыре шага: найти допустимые маршруты, выбрать один по здоровью, выполнить, вернуть результат обратно.
observe это единственный шаг, который меняет будущее поведение.def fetch(self, url: str) -> tuple[str, FetchResult]: candidates = self._eligible() or self._scored chosen = self._select(candidates) result = chosen.route.fetch(url) chosen.health.observe(result.ok, result.latency_s) return chosen.route.name, result
Выбор взвешивает каждый допустимый маршрут его здоровьем в степени gamma. При нуле выбор равномерен; по мере роста трафик концентрируется на маршрутах с лучшими оценками. Держите её умеренной. Большая степень порождает ротатор, который намертво цепляется за маршрут, случайно оказавшийся хорошим первым, и затем не имеет никакой возможности узнать, что понижённый маршрут восстановился, потому что он ему ничего не шлёт.
Запасной вариант в первой строке значит больше, чем кажется: когда предохранитель открыт на всём, _eligible() пуст, и ротатор откатывается к полному оценённому набору, а не выбрасывает исключение. Пул, целиком нездоровый, всё равно должен попробовать наименее плохой вариант.
Что бенчмарк показывает на самом деле
python src/main.py benchmark 20 Policy comparison: policy requests success rate mean latency round-robin 20 100.0% 1.589s health-weighted 20 100.0% 0.289s
Читайте внимательно, потому что заголовочное число здесь наименее интересно. На незащищённой цели вроде example.com оба маршрута успешны, поэтому доли успеха совпадают, и вся разница уходит в задержку. Круговой перебор продолжает отправлять половину трафика по более медленному маршруту, потому что именно это и означает справедливость. Политика, взвешенная по здоровью, это замечает и прекращает.
Абсолютные задержки меняются от прогона к прогону и от цели к цели и не являются утверждением. Утверждение о поведении: одна политика реагирует на свидетельства, другая не может. Направьте тот же бенчмарк на защищённую цель, и тот же механизм проявит себя уже в доле успеха: EWMA успеха прямого маршрута падает, подряд идущие отказы срабатывают на предохранителе, и трафик уходит, пока круговой перебор продолжает кормить запросами маршрут, который их отвергает.
Одна точка входа перед ротацией резидентных IP, с обходом антибот-защиты на стороне сервиса, чтобы ваш ротатор оценивал маршрут, а не управлял парком адресов. Направьте на неё HTTP-клиент и оставьте политику маршрутизации там, где ей место. Начните бесплатно, до 5 000 запросов, без карты.
Переход в продакшен
Бенчмарк однопроцессный и синхронный, потому что так контур управления читается легче. Когда это перестаёт быть так, меняются четыре вещи.
Состояние здоровья нужно сделать общим. В памяти каждый воркер держит собственное мнение о пуле, поэтому один процесс может считать маршрут сломанным, пока другой преспокойно им пользуется, и предохранитель нигде не открывается согласованно. Перенос EWMA успеха, EWMA задержки, состояния предохранителя и счётчиков отказов во что-то вроде Redis, под согласованным ключом на маршрут, и делает сигнал общим, а не внутрипроцессным фольклором.
Обновления должны быть безопасны при параллелизме. Реальные нагрузки наблюдают результаты параллельно, и каждое поле в observe это чтение, изменение и запись. Без синхронизации или атомарных операций два одновременных отказа могут прочитать один и тот же счётчик и записать обратно одно и то же приращение, так что порог в три тихо превращается в порог в пять.
Коэффициенты затухания нужно настраивать под ваш трафик. 0.3 это защитимая отправная точка, а не ответ. Более высокие значения гонятся за недавними наблюдениями и реагируют быстрее ценой избыточной реакции на случайный всплеск. Более низкие спокойнее и позже замечают настоящее изменение. Какая ошибка вам предпочтительнее, зависит от того, насколько шумны ваши цели.
Здоровье может быть не единственной вашей целью. Маршруты различаются по стоимости не меньше, чем по качеству, и самый быстрый маршрут не обязательно тот, которому вы хотите отдать каждый запрос. Расширение скаляра слагаемым стоимости позволяет политике взвешивать надёжность и задержку против расходов, а не оптимизировать одно измерение и удивляться счёту.
Одна граница лежит вне модели: направляйте это только на цели, доступ к которым вам разрешён, и уважайте их условия, ожидания по частоте и ограничения. Бенчмарк использует example.com именно потому, что это контролируемая цель, которая ничего из этого не требует.
Главное
Ротатор становится полезным в тот момент, когда выбор ведётся наблюдаемым поведением, а не позицией в списке. Работу делают три механизма: EWMA по успеху для недавней надёжности, EWMA по задержке для отзывчивости и предохранитель для резких отказов, которые среднее сглаживает. Умножение первых двух держит маршрут честным по обеим осям; предохранитель берёт на себя случай, когда постепенно это неверная скорость.
Круговой перебор остаётся в картине как контроль, который делает улучшение измеримым. А Smart AI Proxy встраивается как маршрут, а не как зона ответственности: он поглощает ротацию IP и обход антибот-защиты, чтобы политика над ним оставалась решением о маршрутизации.
Выбрать, выполнить, наблюдать, обновить, выбрать снова. Всё остальное в этой статье это подробности того, насколько хорошо сделан каждый шаг.
Часто задаваемые вопросы
Зачем оценивать маршруты самому, если Smart AI Proxy уже ротирует?
Они работают на разных уровнях. Smart AI Proxy ротирует IP внутри собственного пула, поэтому вашему приложению не нужно моделировать отдельные адреса. Ваш ротатор оценивает маршруты: прямое соединение против проксированного, один региональный узел против другого, дешёвый путь против дорогого. Это решение может принять только ваше приложение, потому что именно оно знает, зачем нужен трафик. Модель в этой статье намеренно общая, чтобы подходить к любому набору маршрутов, который вы реально эксплуатируете.
Какой коэффициент затухания выбрать?
Начните с 0.3 для обоих. Каждое новое наблюдение вносит 30% в обновлённую оценку, а прежняя оценка несёт оставшиеся 70%. Повышайте его, когда цели быстро меняют поведение и политика должна замечать это раньше; понижайте, когда измерения шумные и одна медленная ответная реакция не должна двигать трафик.
Когда предохранитель помогает больше, чем одна лишь EWMA успеха?
Когда отказ приходит внезапно. EWMA по построению сглаживает, поэтому маршруту, умершему полностью, всё равно нужно несколько наблюдений, чтобы опуститься достаточно низко и начать иметь значение, и каждое из этих наблюдений это потраченный впустую запрос. Предохранитель же опирается на подряд идущие отказы и убирает маршрут немедленно, а после остывания предлагает контролируемую полуоткрытую пробу, вместо ожидания, пока среднее подрастёт обратно. Эти механизмы дополняют друг друга: среднее занимается деградацией, предохранитель обвалом.
Нужен ли для этого JavaScript token?
Нет. Normal token достаточен для маршрута Smart AI Proxy, который здесь используется. JavaScript token существует для целей, которым нужен рендеринг, прежде чем появится хоть какое-то содержимое, и это вопрос о цели, а не о маршрутизации. Логика ротации в обоих случаях одинакова.
Может ли эта же модель маршрутизировать больше двух вариантов?
Да, и так она даже полезнее. Ни модель здоровья, ни политика выбора не предполагают двух маршрутов: обе работают со списком. Два маршрута просто делают бенчмарк удобным для чтения. Добавить региональные узлы или второго поставщика значит дополнить список маршрутов, а взвешенный выбор распределит трафик по всему, что там есть.
Обходите любой сайт в масштабе, без борьбы с инфраструктурой.
Crawlbase берёт на себя прокси, отпечатки и CAPTCHA, чтобы ваша команда выпускала конвейеры данных вместо поддержки обвязки краулинга. 1 000 запросов бесплатно, без карты.
