Большинство реализаций ограничения частоты запросов API легко обходятся наивной ротацией прокси — но та же самая ротация в течение нескольких минут запускает эвристики защиты от злоупотреблений. Настоящая задача заключается не просто в том, чтобы оставаться ниже лимита, а в том, чтобы делать это, не выглядя как бот. Для этого необходимо понимать две различные области действия лимитов (на IP и на ключ), применять backoff с джиттером, рандомизировать ротацию пула и формировать тайминги запросов так, чтобы имитировать органический трафик. Паттерн «token bucket», наложенный на пул прокси, обеспечивает надёжную основу.
Per-IP vs Per-Key: две оси ограничения частоты запросов
Ограничения частоты действуют как минимум по двум независимым измерениям: исходный IP и ключ API (или токен). Один ключ может разрешать 5 000 запросов в час (аутентифицированный лимит GitHub), но тот же ключ с одного IP может быть ограничен более низким потолком burst-запросов. Неаутентифицированные запросы ограничены ещё сильнее — обычно 60 запросов в час на IP. Игнорирование измерения IP — самый быстрый способ получить ответ 429 Too Many Requests (RFC 6585). Пул прокси должен распределять запросы по нескольким IP, чтобы не насыщать ни один источник, но каждый IP всё равно использует один и тот же ключ. Если глобальный лимит ключа составляет 5 000/час, а у вас 50 прокси, каждый прокси может сделать только 100 запросов в час, прежде чем сработает счётчик на уровне ключа. Прежде чем писать хоть строчку кода, определите оба лимита.
Backoff с джиттером: разница между вежливостью и предсказуемостью
Экспоненциальный backoff без джиттера — это отпечаток пальца. Серверы видят запросы, поступающие с идеально удваивающимися интервалами, и помечают их как автоматизированные. Решение — полный джиттер: sleep(random.uniform(0, min(cap, base * 2 ** attempt))). Это размазывает повторные попытки по временному окну, делая паттерн неотличимым от всплеска реальных пользователей. Документация AWS по экспоненциальному backoff рекомендует джиттер именно по этой причине. Следующий фрагмент на Python реализует token bucket, который пополняется с фиксированной скоростью, при опустошении засыпает с джиттером и ротирует прокси при каждом успешном получении токена:
import time
import random
from collections import deque
class TokenBucketProxyPool:
def __init__(self, proxies, rate, burst):
self.proxies = deque(proxies)
self.rate = rate # tokens per second
self.burst = burst
self.tokens = burst
self.last_refill = time.monotonic()
def refill(self):
now = time.monotonic()
elapsed = now - self.last_refill
self.tokens = min(self.burst, self.tokens + elapsed * self.rate)
self.last_refill = now
def acquire(self):
while True:
self.refill()
if self.tokens >= 1:
self.tokens -= 1
self.proxies.rotate(1) # simple rotation, but see section below
return self.proxies[0]
else:
sleep_time = (1 - self.tokens) / self.rate
jitter = random.uniform(0, sleep_time * 0.5)
time.sleep(sleep_time + jitter)
# Usage: pool = TokenBucketProxyPool(proxies, rate=2.0, burst=10)
# proxy = pool.acquire() # blocks until token available
Ротация пула прокси: рандомизируйте, а не используйте round-robin
Последовательный round-robin по списку прокси обнаруживается. Сервер, видящий запросы с 1.2.3.4, затем с 5.6.7.8, затем с 9.10.11.12 в строгом порядке, сможет коррелировать паттерн между IP. Вместо этого выбирайте прокси случайным образом с возвращением и добавляйте задержку (cooldown) для каждого прокси. Публичные каталоги прокси сообщают о 60–80% отказов — прокси, вернувший 429 или ошибку соединения, следует помещать в очередь «мёртвых» как минимум на 60 секунд. Реализуйте взвешенный случайный выбор, где недавно успешные прокси выбираются с большей вероятностью. В token bucket выше для ясности используется простой rotate(1); в продакшене замените его на random.choice() и отслеживайте количество сбоев для каждого прокси.
Формирование запросов: имитируйте среднее, не пересекая черту
Даже с идеальным token bucket и пулом прокси постоянная частота запросов ровно 2,0 в секунду неестественна. Реальные пользователи делают паузы, работают пакетами и варьируют интервалы между запросами. Добавьте небольшую случайную задержку (например, time.sleep(random.uniform(0.1, 0.5))) перед каждым вызовом acquire() и изменяйте скорость token bucket на ±10% каждые несколько минут. Однако не добавляйте задержки, превышающие документированное окно лимита API — это лишает смысла. Цель — оставаться в пределах лимита, выглядя как группа легитимных клиентов. Чрезмерное формирование (например, вставка задержек, имитирующих скорость набора текста человеком) — напрасная трата усилий; API интересует частота запросов, а не время между нажатиями клавиш.
Этичное смягчение: знайте, где проходит граница
Каждый описанный здесь приём является легитимной оптимизацией — до тех пор, пока он не нарушает Условия использования API. Большинство ToS прямо запрещают «обход ограничений частоты» или «использование автоматизированных средств для доступа к сервису». Чтение ToS не является опциональным. Если API разрешает 100 запросов в минуту на ключ, использование 50 прокси, каждый из которых делает 2 запроса в минуту, соответствует правилам. Использование 100 прокси, каждый из которых делает 100 запросов в минуту, — нет; это злоупотребление, независимо от того, насколько умён ваш джиттер. Различие заключается в намерении и объёме. Token bucket, наложенный на пул прокси, — это инструмент; тот же инструмент, который этично парсит публичный каталог, может быть использован для DDoS-атаки на небольшое API. Документируйте целевые лимиты частоты, проверяйте логи на наличие 429 и никогда не превышайте документированный потолок на ключ. Если вам нужна большая пропускная способность — запросите более высокий лимит или оплатите выделенный тариф.