Спроектируйте распределённый rate limiter для API-гейтвея: сотни инстансов, лимиты на пользователя и на ключ, минимальная задержка.
Короткий ответ
- Локальный счётчик на инстанс не работает — нужно общее состояние
- Центр — Redis: token bucket на ключ, атомарность через Lua
- Задержку прячут локальным кэшем и батч-синхронизацией квот
- Точность против скорости — главный компромисс дизайна
- Поведение при недоступности Redis: fail-open или fail-closed
- Ответ клиенту: 429, Retry-After, X-RateLimit-заголовки
Классика: token bucket в Redis с Lua-атомарностью, компромисс точности ради задержки и явная политика на случай отказа хранилища.
Как сказать вслух
пример ответаРаз инстансов сотни, локальные счётчики дадут лимит, умноженный на число инстансов, — состояние должно быть общим. Базовое решение — token bucket в Redis: храню для каждого ключа число токенов и время последнего пополнения, проверку и списание делаю атомарно Lua-скриптом. Чтобы не ходить в Redis на каждый запрос, можно выдавать инстансам локальные порции квоты и синхронизироваться батчами — точность чуть падает, зато задержка почти нулевая. Отдельно проговариваю, что делать при падении Redis: обычно пропускаем трафик, потому что лимитер не должен ронять API.
Подробный ответ
Основной ответ
Требования: лимиты по нескольким измерениям (user, api-key, IP, endpoint), задержка решения — единицы миллисекунд, горизонтальный рост. Наивный вариант — независимые лимиты на инстансе — ломается балансировкой. Централизованный: Redis (кластер, шардирование по ключу лимита) хранит состояние token bucket — tokens и last_refill; Lua-скрипт атомарно пополняет по прошедшему времени, проверяет и списывает — исключая гонки между инстансами. Оптимизация задержки: локальный кэш решений на десятки миллисекунд для злостных нарушителей и схема с «арендой» порций квоты инстансами (каждый берёт у центра пачку токенов, тратит локально, досинхронизируется) — жертвуем точностью на единицы процентов ради снятия Redis из hot path. Политика отказа: fail-open (пропускать) для продуктового API, fail-closed для дорогих или опасных операций. Конфигурация лимитов — динамическая, по тарифам клиентов. Наблюдаемость: метрики отказов по ключам, алерты на всплески, доля ошибок Redis.
Ключевые моменты
- Атомарность в Redis. Пара GET/SET без Lua или транзакции даёт гонку: два инстанса одновременно видят последний токен и оба пропускают запрос.
- Точность — разменная монета. Строгая глобальная точность требует похода в центр на каждый запрос; аренда квот и кэш снижают точность на проценты и задержку в разы.
- Fail-open vs fail-closed. Падение лимитера не должно уронить всё API — но для логина или SMS разумнее закрыться. Политика объявляется явно, по типам операций.
- Многоуровневые ключи. Реальный запрос проверяет несколько бакетов (глобальный, на пользователя, на endpoint) — порядок проверок и стоимость важны.
Практический контекст
Задача проверяет сеньорское умение торговаться между консистентностью, задержкой и устойчивостью — здесь нет идеального ответа, есть осознанные компромиссы. Интервьюер ждёт эволюции решения от простого к сложному и обязательных вопросов самому себе: что при падении Redis, что с горячим ключом-гигантом, как выкатывать изменение лимитов. Упоминание готовых реализаций (nginx limit_req, envoy ratelimit) — плюс, если кандидат понимает их устройство.
Частые ошибки
- Предлагают счётчики в памяти инстансов, не замечая умножения лимита на их число
- Делают проверку и списание в Redis неатомарно — гонка между инстансами
- Не проговаривают поведение при отказе хранилища лимитов