Проектування 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 і обов'язково тестуйте бурсти і відмову сховища.