Задача 3 из 10
Ограничение частоты запросов
Спроектируйте ограничитель частоты запросов для публичного API: не больше N запросов в минуту на клиента, чтобы один потребитель не выел ресурсы у всех остальных.
Функциональные требования
- Лимит на ключ: API-токен или IP
- Разные лимиты для разных тарифов
- Ответ 429 и заголовки с остатком лимита
- Изменение лимитов без выката кода
Нефункциональные требования
- 50 000 запросов/с проходят через ограничитель
- Проверка лимита добавляет не больше 2 мс
- Приложение живёт на 20 инстансах — лимит общий, а не на каждый
- Кратковременный перерасход допустим, полный отказ сервиса — нет
Нарисуйте архитектуру
Не гонитесь за красотой: на собеседовании от схемы нужно, чтобы по ней было видно путь запроса и где лежат данные. Сначала нарисуйте сами — эталон и разбор ниже.
Блок из палитры — добавить. Тащите мышкой или пальцем. Двойной клик по названию — переименовать. «Связь» — щёлкнуть по одному блоку, потом по другому: получится стрелка.
Схема сохраняется в этом браузере — вкладку можно закрыть и вернуться позже.
1 С чего начать, если ступор
- Сначала выберите алгоритм и объясните выбор: фиксированное окно, скользящее окно, token bucket, leaky bucket.
- Фиксированное окно ломается на границе: 2× лимита за две соседние секунды.
- Состояние общее, значит нужно внешнее хранилище — и оно становится горячей точкой в каждом запросе.
- INCR и EXPIRE двумя командами — это гонка: ключ может остаться без TTL навсегда.
- Решите заранее, что делать при недоступности хранилища лимитов: пропускать всех или блокировать всех.
2 Эталонная схема
Счётчик один на всех: если держать его в памяти инстанса, реальный лимит умножится на число инстансов. Тарифы лежат в БД и кэшируются в памяти — они меняются редко.
Это один из рабочих вариантов, а не единственно верный. Если у вас иначе, но вы можете объяснить почему — на собеседовании это ровно то, что нужно.
3 Разбор: что должно прозвучать
Выбор алгоритма
- Фиксированное окно: просто и дёшево, но на стыке окон пропускает удвоенный лимит
- Скользящее окно (лог запросов точен, счётчик с весом — компромисс) убирает всплеск ценой памяти
- Token bucket разрешает всплески и хорошо описывает «в среднем N, но burst допустим»
Общее состояние
- Счётчик в памяти процесса на 20 инстансах превращает лимит N в 20N
- Внешнее хранилище (Redis) встаёт в горячий путь каждого запроса — отсюда требование 2 мс
- Ключ вида limit:{клиент}:{окно} и TTL чуть больше длины окна
Атомарность
- INCR + EXPIRE двумя командами: между ними процесс может упасть, ключ останется без TTL
- Решение — один атомарный шаг: Lua-скрипт или SET с NX и EX
- Проверка «прочитал, сравнил, записал» без атомарности — классическая гонка
Отказ хранилища лимитов
- Fail-open: пропускаем всех — сервис жив, но защиты нет
- Fail-closed: блокируем всех — защита есть, но упавший Redis кладёт весь API
- Обычно выбирают fail-open плюс грубый локальный лимит как страховку
Ответ клиенту
- Код 429 и Retry-After, чтобы клиент знал, когда повторить
- Заголовки с лимитом и остатком помогают клиенту не биться в стену
- Ошибка должна быть дешёвой: не тратить на отклонённый запрос всю обработку
Куда копать дальше, если спросят
- Несколько уровней лимитов: глобальный, на endpoint, на пользователя
- Лимит по IP и CGNAT: за одним адресом сидят тысячи людей
- Локальный счётчик с периодической сверкой — когда 2 мс на Redis уже дорого
- Отличие ограничения частоты от защиты от DDoS: где заканчивается ваша зона
Следующая задача: Сервис уведомлений
Первая задача, где всё держится на очередях: приоритеты, ретраи, дедупликация и внешние провайдеры, которые падают.