Rate limiting в API
Ограничение частоты - то, что отделяет сервис, переживший всплеск и абьюз, от лежащего. Собес проверяет, знаешь ли ты алгоритмы (token bucket против sliding window), правильный код ответа и главную ловушку - локальные счётчики на нескольких инстансах.
Типовые формулировки: «как ограничить частоту запросов?», «какой код вернуть при превышении?», «как считать лимит на нескольких серверах».
Зачем, код ответа и на что вешать
Rate limiting ограничивает число запросов за окно: защита от перегрузки и DDoS, пресечение абьюза (скрейпинг, перебор паролей) и справедливость между клиентами. Превышение это 429 Too Many Requests с заголовком Retry-After, чтобы клиент знал, когда повторить; 403 или 503 вместо 429 оставляют его в неведении.
Лимит вешают на ИЗМЕРЕНИЕ клиента - API-ключ, пользователь, IP, а не в общий котёл на всех. Логин лимитируют строже (brute-force), а дорогие запросы считают по стоимости (cost-based), а не поштучно: один тяжёлый запрос способен незаметно исчерпать ресурс, пока счётчик показывает единицу.
// Базовый лимит удобно поставить на gateway или nginx, не доводя нагрузку до приложения.
- 429
- статус превышения лимита частоты запросов
- cost-based
- квота по весу запроса, а не по числу
Алгоритмы и общий счётчик
Алгоритмы. Token bucket: токены капают с постоянной скоростью, запрос тратит токен, допускается всплеск до ёмкости корзины - удобно, когда короткие пики норма. Leaky bucket даёт ровный выходной поток. Fixed window против sliding window: у fixed есть неприятность - всплеск на стыке окон (в конце одного и начале следующего пролезает двойная порция), sliding window его сглаживает.
Главная ловушка масштаба: в нескольких инстансах ЛОКАЛЬНЫЕ счётчики множатся на их число - реальный лимит становится в N раз больше задуманного. Нужен ОБЩИЙ стор (Redis) с атомарным инкрементом, чтобы счётчик был один на всех.
// Клиент со своей стороны должен уважать Retry-After и делать экспоненциальный backoff, а не долбить сразу.
- token bucket
- токены пополняются, запрос тратит; допускает всплеск
- sliding window
- скользящее окно, сглаживает граничный всплеск
Как отвечать: «Как считать rate limit на нескольких инстансах?»
Через общий стор, обычно Redis с атомарным инкрементом. Если каждый инстанс считает у себя в памяти, счётчики независимы и реальный лимит множится на число инстансов - задумал 100 запросов в минуту, а при пяти подах получил 500. Поэтому счётчик должен быть один на всех: инстансы инкрементируют ключ в Redis атомарно, скажем через INCR с TTL или скрипт, реализующий token bucket. Ключом беру измерение клиента - API-ключ или пользователя. При превышении отвечаю 429 с Retry-After. Базовый грубый лимит можно ещё и на gateway повесить, чтобы не пускать флуд до приложения.
Названа корневая проблема (умножение локальных счётчиков), решение с атомарностью, выбор ключа и правильный ответ, и добавлен слой gateway; ответ инженера, думавшего про распределённость.
На чём валят
- −Считать все запросы поштучно: дорогой запрос исчерпывает ресурс незаметно - нужен cost-based учёт.
- −Локальные счётчики в нескольких инстансах: реальный лимит множится на их число - нужен общий Redis.
- −Отвечать 403/503 вместо 429 или не давать Retry-After - клиент не знает, когда повторить.
- −Fixed window без сглаживания: на стыке окон пролезает двойная порция запросов.
Проверьте себя
Пять вопросов из банка по этой подтеме. Всего их 12, остальные разбираются в тренажёре.
- Клиент превысил лимит. Какой статус и заголовок ему вернуть?A)400 Bad Request — запрос сверх лимита следует считать некорректным по формеB)403 Forbidden — у клиента просто нет прав слать столько запросов подрядC)429 Too Many Requests с Retry-After — когда можно повторитьD)503 Service Unavailable — сервер ведь фактически отказал в обслуживании запроса
показать ответ и разбор
+C)429 Too Many Requests с Retry-After — когда можно повторить// разбор: Стандартный ответ на превышение лимита — 429 Too Many Requests. Заголовок Retry-After (секунды или дата) подсказывает клиенту, когда безопасно повторить, чтобы он не долбил сервер вслепую. Часто добавляют X-RateLimit-Limit/Remaining/Reset для прозрачности остатка. Хорошо ведущий себя клиент читает эти заголовки и делает backoff.
- Как работает алгоритм token bucket?A)Токены раздаются поровну всем клиентам сразу в начале каждой новой минутыB)Токены капают с фиксированной скоростью, запрос тратит токен; пусто — отказC)Каждый запрос кладёт токен в ведро, и при переполнении сервер начинает отказыватьD)Ведро жёстко пропускает ровно один запрос в секунду без всякого запаса на всплеск
показать ответ и разбор
+B)Токены капают с фиксированной скоростью, запрос тратит токен; пусто — отказ// разбор: Token bucket: в «ведро» с фиксированной ёмкостью токены добавляются с постоянной скоростью. Каждый запрос забирает токен; нет токенов — запрос отклоняют или ставят в ожидание. Накопленные токены позволяют кратковременный всплеск (burst) до ёмкости ведра, при этом средняя скорость держится на заданном уровне. Популярный и гибкий алгоритм.
- Чем фиксированное окно (fixed window) хуже скользящего (sliding window) для лимитирования?A)На стыке двух окон возможен двойной всплеск — граница обнуляет счётчикB)Фиксированное окно вообще не способно ограничивать частоту запросов в принципеC)Скользящее окно пропускает больше запросов, чем фиксированное, при тех же настройкахD)Фиксированное окно требует заметно больше памяти на хранение счётчиков запросов
показать ответ и разбор
+A)На стыке двух окон возможен двойной всплеск — граница обнуляет счётчик// разбор: Fixed window считает запросы в жёстких интервалах (например, за календарную минуту) и обнуляет счётчик на границе. Клиент может выдать лимит в конце одного окна и сразу лимит в начале следующего — до 2× за короткий промежуток вокруг стыка. Sliding window (скользящее) учитывает недавнюю историю и сглаживает этот всплеск ценой чуть большей сложности/памяти.
- Сервис работает в 4 инстанса, лимит считают в памяти каждого. Почему реальный лимит вчетверо больше?A)Инстансы дублируют каждый запрос между собой, и счётчик срабатывает четыреждыB)Это нормально и ожидаемо: суммарный лимит и должен расти пропорционально инстансамC)Балансировщик умножает лимит на число инстансов намеренно, ради отказоустойчивостиD)У каждого инстанса свой счётчик — нужен общий стор (Redis) на все инстансы
показать ответ и разбор
+D)У каждого инстанса свой счётчик — нужен общий стор (Redis) на все инстансы// разбор: Локальный счётчик видит только свои запросы. Балансировщик размазывает трафик по 4 инстансам, каждый независимо разрешает «лимит» — суммарно до 4× задуманного. Для глобального лимита счётчики выносят в общее хранилище (Redis с атомарными инкрементами/скриптами) либо применяют распределённые алгоритмы. Иначе гарантия лимита ломается при масштабировании.
- По какому ключу обычно считают лимит?A)Один общий счётчик на всех сразу — так проще всего реализовать ограничениеB)По случайному идентификатору, который сервер генерирует на каждый новый запросC)По измерению клиента: API-ключ, пользователь или IP — чтобы изолировать ихD)По номеру эндпоинта без учёта того, кто именно шлёт эти запросы к нему
показать ответ и разбор
+C)По измерению клиента: API-ключ, пользователь или IP — чтобы изолировать их// разбор: Лимит навешивают на измерение, идентифицирующее источник: API-ключ, аутентифицированного пользователя, IP (для анонимных). Так один агрессивный клиент не исчерпывает квоту для всех. Часто комбинируют уровни: глобальный предохранитель сервиса + персональные лимиты + отдельные лимиты на дорогие эндпоинты. Ключ выбирают так, чтобы его нельзя было дёшево подменять.
дальше
Теорию прочитали. Навык ставится повторением
В Сеньорчике эта подтема идёт в ежедневных сессиях: движок возвращает её, пока ответы не станут уверенными, и ведёт прогресс отдельно по каждой подтеме. Теория внутри тоже бесплатна, лимит только на количество вопросов в день.