Проектирование rate limiter’ов
1) Зачем rate limiting
Rate limiting защищает доступность и экономику API: останавливает флуды, «бурсты» ретраев, credential stuffing, защищает дорогие операции (денежные транзакции, генерацию отчетов), сглаживает нагрузку на зависимые системы (БД/провайдеры). Хороший дизайн дает справедливость (fairness), предсказуемость латентности и четкие SLO.
Ключевые цели
Стабильность RPS и защита бэкенда от перегрузки.
Управляемая «эластичность» (burst allowance).
Дифференциация клиентов (пер-пользователь/пер-организация/пер-ключ/пер-IP/пер-регион).
Стоимостная модель: разные «цены» для разных операций.
2) Типы лимитов
RPS-лимиты: запросов в секунду/минуту.
Квоты: суммарный бюджет в период (день/месяц).
Конкурентность: одновременные операции (checkout, heavy job).
Скорость/полоса: байт/сек (загрузка/выгрузка).
Взвешенные лимиты: «стоимость» запроса по сложности (например, GraphQL complexity, размер batch).
Адаптивные: ужесточаются при аномалиях (подозрительная активность/ошибки 401/403/5xx).
3) Алгоритмы и когда их применять
3.1 Fixed window counter
Просто: счетчик на интервал (например, 100 r/min).
Плюсы: минимальная стоимость. Минусы: «краевые берсты» на границах окна.
Когда: панели админок, невысокая точность, низкая стоимость.
3.2 Sliding window (log / counter)
Log: хранит метки времени последних запросов, точен, дорог по памяти.
Counter: среднее двух соседних окон (rolling), компромисс точности и цены.
Когда: публичные API среднего трафика, нужна плавность без сложной математики.
3.3 Token bucket
Параметры: скорость `r` (токенов/сек) и емкость `b` (burst). Каждый запрос «сжигает» токен.
Плюсы: естественный burst allowance, простая реализация. Минусы: нет строгой равномерности.
Когда: почти всегда для RPS, если нужны «залпы» в пределах `b`.
3.4 Leaky bucket (drip)
Очередь, из которой «утекает» с фиксированной скоростью.
Плюсы: ровный выходной поток. Минусы: больше задержек.
Когда: сглаживание к внешним «хрупким» провайдерам.
3.5 GCRA (Generalized Cell Rate Algorithm)
Модель теоретического времени прибытия (TAT):- `TAT_next = max(TAT_current, now) + 1/r`, запрос принят, если `now <= TAT_current + burst/r`.
- Плюсы: строгая, точная, мало памяти (по ключу храним TAT). Минусы: сложнее для понимания.
Когда: нужен строгий контроль и плавность, распределенные лимиты.
3.6 Конкурентные семафоры
Счетчик активных операций; вход — если «билеты» есть; выход — освобождение.
Когда: long-running операции, потоки, WebSocket, загрузки.
4) Модель ключей лимитов
Ключ = комбинация атрибутов:- `client_id`/`api_key`/`user_id`/`org_id`
- `IP/ASN/гео` (грубая защита)
- `endpoint/method` (горячие маршруты)
- `scope/plan/tier` (монетизация)
- `idempotency_key` (write-операции)
- Используйте иерархию: сначала строгие пер-ключ, затем пер-организация, затем глобальные.
5) Вес запроса (cost model)
Определяйте «стоимость» `cost(q)`:- GraphQL: сложность по полям × глубина.
- REST: размер ответа/запроса, тип операции (read=1, write=3, отчет=10).
- Batch: `cost = min(n, cap)`.
- Лимитируем токены, а не «запросы»: `budget -= cost(q)`.
6) Распределенная реализация
6.1 Хранилища
In-process: ультра-быстро, но не общий лимит (годится для локальных «мягких» лимитов).
Redis: де-факто стандарт. INCR/EXPIRE, Lua-скрипты (атомарность), ZSET для sliding window, ключи с TTL.
Envoy/NGINX/Kong/Traefik: встроенные фильтры; удобно для периметра.
Service Mesh: локальные лимиты на sidecar + глобальная синхронизация.
6.2 Атомарность и гонки
Lua в Redis: проверка и инкремент в одном шаге.
GCRA: хранить один TAT с CAS/скриптом.
Согласованность часов: NTP, монотонные таймеры.
Sharding: консистентный хеш по ключу; избегайте «горячих» шардов.
6.3 Геораспределение
Локальные лимиты на региональных кластерах + верхний глобальный (coarse).
CRDT/репликация — осторожно (задержки, двойной расход). Предпочтительнее региональные лимиты с запасом.
7) Политики и приоритизация
Планы: Free/Pro/Enterprise с разными `r`, `b`, квотами.
Приоритеты: «дорогие» маршруты получают меньший лимит или больший cost.
Списки: allow-list для интеграций, deny по ASN/прокси/ТОR.
Эскалация: при повторном превышении — понижаем лимит, вводим proof-of-work/капчу/челленджи.
8) Примеры конфигов
8.1 Envoy (HTTP rate limit filter, псевдо)
yaml rate_limit:
domain: public-api descriptors:
- key: api_key rate_limit:
unit: second requests_per_unit: 50 burst: 100
- key: api_key value: payments. write rate_limit:
unit: second requests_per_unit: 5 burst: 10
8.2 NGINX (lua + Redis, псевдо)
nginx lua_shared_dict limits 10m;
location /api/ {
access_by_lua_block {
local key = ngx. var. arg_apikey.. ":".. ngx. var. request_method.. ":".. ngx. var. uri
-- token bucket in Redis (evalsha)
local allowed, retry_after = ratelimit_allow(key, 50, 100) -- r=50/s, b=100 if not allowed then ngx. header["Retry-After"] = retry_after return ngx. exit(429)
end
}
proxy_pass http://backend;
}
8.3 Конкурентные лимиты (псевдокод)
pseudo on_request_start(key):
if redis. incr_with_ttl("sem:" + key, ttl=60) > MAX_CONCURRENCY:
redis. decr("sem:" + key); reject(429)
on_request_finish(key):
redis. decr("sem:" + key)
8.4 GCRA (псевдокод)
pseudo params: r tokens/sec, burst b tat = redis. get(key) or now allowed_time = tat - (b / r)
if now < allowed_time: reject(429, retry_after = allowed_time - now)
tat_next = max(tat, now) + 1/r redis. set(key, tat_next, ttl = ceil(b/r) + safety)
9) Интеграция с ретраями, таймаутами и circuit breaker
Retry-budget: ограничьте долю ретраев до X% от основного трафика.
Jitter: при backoff всегда добавляйте джиттер — уменьшает синхронные всплески.
Circuit breaker: при высокой ошибочности (`5xx`, timeouts) снижайте лимиты или переводите часть маршрутов в «read-only».
Hedging: аккуратно; учитывайте cost, чтобы не удваивать расход бюджета.
10) Наблюдаемость и управление
Метрики: `rps_allowed`, `rps_blocked`, `429_rate`, `retry_after_avg`, `burst_used`, `quota_remaining`, `active_concurrency`.
Лейблы: по ключу лимита, региону, эндпоинту, плану.
Логи решений (сэмплированные): причина отказа, текущие счетчики, TTL ключа.
Дашборды: тепловые карты по ключам/эндпоинтам, «горячие» клиенты.
Алерты: рост 429>2–5% на критичных маршрутах, частые «исчерпания» квот, дисбаланс шардов.
11) Тестирование и валидация
Контрактные тесты политик (таблицы «если-то»).
Нагрузочные: бурсты (x10 от r), длительные плато, «грязные» паттерны (slow-POST, долгие соединения).
Chaos-трафик: неравномерные потоки, clock drift, выпадение Redis/mesh.
А/В-включение: canary rollout лимитов, shadow-решения (логируем, но не блокируем) перед включением.
12) Edge-кейсы и тонкости
Clock skew: используйте `now()` из единого источника (сервер), а не из заголовков клиента.
Idempotency-Key: для write — снижает amplification при ретраях.
Batch-операции: лимитируйте по размеру батча и по суммарному cost.
Long-poll/WebSocket: лимитируйте по количеству каналов/подписок и по длительности.
Cold start: «теплый» старт счетчиков/предзагрузка; иначе всплески ложных 429.
Вычислительно дорогие запросы: лимитируйте до выполнения бизнес-логики.
Границы TTL: TTL ключей должен покрывать окно + запас (safety margin).
13) Антибот-эскалации
Ступени: предупреждение → 429 + `Retry-After` → чаллендж (капча/пазл) → временный блок.
Сигналы: device-fingerprint, поведение курсора/тайминги, TOR/прокси/хостинги.
Политики должны быть детерминированны и воспроизводимы для форензики.
14) Безопасность и комплаенс
Deny-by-default на критичных маршрутах (write/финансы).
Аудит: храните решения по лимитам для регуляторных кейсов и разборов инцидентов.
PII: ключи лимитов не должны раскрывать персональные данные в логах.
15) Чек-лист prod-готовности
- Определены ключи лимитов и cost-модель.
- Выбран алгоритм (token bucket/GCRA) и хранилище (Redis/шлюз).
- Политики для tier’ов клиентов + глобальные «предохранители».
- Конкурентные лимиты для долгих операций.
- Retry-budget, backoff с джиттером, интеграция с circuit breaker.
- Дашборды/алерты, сэмплированные логи решений.
- Canary-включение и shadow-режим.
- Тесты бурстов, длительных плато, сбоев Redis, clock skew.
- Документация для клиентов: коды 429, `Retry-After`, примеры экспоненциального backoff.
16) TL;DR
Используйте token bucket или GCRA с Redis/шлюзом, проектируйте ключи лимитов и стоимость запросов, добавляйте конкурентные семафоры для долгих операций, интегрируйте с retry-budget и circuit breaker, наблюдайте за 429 и «бурст-емкостью», раскатывайте лимиты через canary/shadow и обязательно тестируйте бурсты и отказ хранилища.