← все задачи

Задача 3 из 10

Ограничение частоты запросов

Спроектируйте ограничитель частоты запросов для публичного API: не больше N запросов в минуту на клиента, чтобы один потребитель не выел ресурсы у всех остальных.

Уровень: Начальный На собеседовании: 30–40 минут алгоритмы окнаобщее состояниеатомарность

Функциональные требования

  • Лимит на ключ: 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: где заканчивается ваша зона

Следующая задача: Сервис уведомлений

Первая задача, где всё держится на очередях: приоритеты, ретраи, дедупликация и внешние провайдеры, которые падают.