Rate limiting: token bucket и где ставить лимит

О лекции

Token bucket дозирует среднюю скорость и разрешает короткий всплеск до ёмкости ведра. Лимит на gateway режет дёшево по IP или ключу. В приложении — по пользователю после auth. Без общего счётчика N реплик дают N-кратную квоту.

Авторизация и устойчивость HTTP http собеседование

Без лимита один клиент (или бот) съедает пул БД, воркеры и соседей по кластеру. Rate limit — справедливость и защита ёмкости, не «безопасность» в смысле криптографии. Честному клиенту отвечают 429 и желательно Retry-After, а не молчаливым таймаутом каскадом.

Алгоритмов несколько. Fixed window прост и даёт всплеск на границе окна. Sliding window ровнее и дороже по состоянию. Leaky bucket сглаживает жёстче. Token bucket объясняют чаще всего: ведро жетонов хорошо рисуется.

Token bucket

Token bucket простыми словами: ведро с жетонами. Каждый запрос берёт жетон. Жетоны капают с постоянной скоростью. Если ведро пустое — 429. Если ведро полное — можно пачкой запросов сразу. Это всплеск.

Ёмкость ведра — B. Скорость капели — R. Запрос стоит один жетон. Или вес: тяжёлый export стоит больше. Средняя скорость не выше R. Это не «ровно N каждые 60 секунд без всплесков» и не бесконечный трафик «потому что есть JWT».

Пример. Ведро на 20 жетонов, капает 5 в секунду. Можно сразу стукнуть 20 запросов. Потом — не быстрее 5 в секунду. Короткий всплеск разрешён, средняя скорость ограничена.

// tokens = min(B, tokens + R*dt)
// if tokens >= cost { tokens -= cost; allow } else { 429 }
//
// B=20, R=5/s → короткие 20 запросов подряд, затем ~5 rps
//
// gateway: лимит по IP / API-key, дёшево, до auth
// app:     лимит по user_id после JWT, разные квоты на POST /export
//
// 3 реплики × 100 rps локально ≈ 300 rps суммарно без Redis
// лучше 429, чем таймаут пула БД

Ведро: всплеск до ёмкости, refill задаёт среднюю скорость. Общий счётчик нужен, если реплик несколько.

Где ставить

Ключ лимита важнее формулы. IP за NAT — общая квота на офис. API-ключ и user id точнее, но появляются после auth: CPU на TLS и парсинг уже потрачены. Поэтому грубый край и тонкий слой внутри часто живут вместе.

  • API gateway, ingress, CDN — рано и дёшево, по IP или ключу. Не знают «этот user — VIP», пока нет заголовка.
  • В приложении — после auth, квоты по ручкам. Поздно: парсинг и TLS уже были.
  • Оба слоя: грубый на краю, тонкий внутри.

В нескольких репликах локальный счётчик в памяти даёт суммарную квоту ≈ лимит × число подов. Нужен Redis (или аналог) либо доля квоты на под с запасом. 429 предсказуемее, чем исчерпанный пул и 502 у соседей.

Пример. Три пода, на каждом «100 rps». Без общего счётчика это 300 rps на БД. БД не выдержит. Клиенты увидят таймауты, не честный 429.

Типичная ошибка

Лимитировать только по IP и удивиться, что мобильный оператор сидит на одном адресе. Или поставить лимит в каждом поде «по 100 rps» без общего состояния и получить кратный запас, который БД не выдержит.

На собеседовании

Нарисуйте token bucket: ёмкость, refill, всплеск. Спросите ключ (IP ломается NAT) и согласованность между репликами. Край режет дёшево по IP/ключу; приложение — по пользователю после auth.