Спроектируйте распределённый rate limiter для публичного API.
- Как ограничить число запросов клиента, если серверов много?
- Как реализовать rate limiting на Redis?
Состояние лимита — в общем быстром хранилище (Redis), проверка и списание — атомарно одним скриптом (Lua) или командой. Лимитер — в шлюзе или сайдкаре, до бизнес-логики.
- Token bucket в Redis: два поля (жетоны, время) и Lua-скрипт на проверку
- Задержка: поход в Redis на каждый запрос — 0,2–1 мс; для огромных потоков — локальные квоты с периодической синхронизацией
- При недоступности Redis — осознанный выбор fail-open или fail-closed
А ещё Ответ 429 с Retry-After и заголовками RateLimit-*; разные лимиты по тарифам
Подробно
Требования: лимиты по API-ключу (тариф: 100 запросов/с, 1 млн в день), по IP для анонимных, по эндпоинту для тяжёлых операций; десятки экземпляров шлюза; добавка к задержке — не больше миллисекунды; точность ±несколько процентов допустима.
Почему не локально. Если у каждого из 20 экземпляров свой счётчик, клиент получит до 20× лимита (балансировщик раскидывает запросы). Делить лимит на число экземпляров — неточно при неравномерной балансировке и автомасштабировании.
Централизованное состояние в Redis:
- фиксированное окно —
INCR key:{client}:{minute}+EXPIRE: одна команда, но всплеск на стыке окон; - скользящий журнал — sorted set с метками времени,
ZREMRANGEBYSCORE+ZCARD+ZADDв одном скрипте: точно, но память на каждый запрос; - token bucket — хэш
{tokens, ts}и Lua-скрипт: пополнить по прошедшему времени, проверить, списать, записать — атомарно, потому что Redis исполняет скрипт целиком. Время берём из Redis (TIME), а не с шлюзов — их часы расходятся.
Масштаб Redis: ключи по клиентам шардируются в Redis Cluster естественно; горячий клиент — один ключ, но один Redis держит сотни тысяч операций в секунду. Реплики не помогают (запись на каждый запрос).
Снижение задержки и нагрузки:
- локальные квоты — шлюз берёт у Redis пачку жетонов (например, 10% лимита) и тратит локально, синхронизируясь при исчерпании. Точность чуть ниже, походов в Redis в разы меньше;
- предварительная проверка локальным приблизительным лимитером, к Redis — только близко к порогу.
Отказ хранилища: fail-open (пропускать — сервис доступен, но без защиты) или fail-closed (отказывать — защищено, но недоступно); часто — fail-open с грубым локальным лимитом на экземпляр как страховкой. Таймаут на поход в Redis — единицы миллисекунд.
Поведение для клиента: 429 Too Many Requests, Retry-After, RateLimit-Limit/Remaining/Reset; лимиты по тарифу из конфигурации, которую можно менять без выкатки; отдельные лимиты на тяжёлые операции (стоимость в жетонах).
Наблюдаемость: метрики отказов по клиентам и правилам — чтобы отличить атаку от ошибки конфигурации, которая режет честных клиентов.
Пример
-- KEYS[1] = bucket:{api_key}; ARGV = rate, burst, cost
local t = redis.call('TIME')
local now = t[1] + t[2] / 1e6
local b = redis.call('HMGET', KEYS[1], 'tokens', 'ts')
local tokens = tonumber(b[1]) or tonumber(ARGV[2])
local ts = tonumber(b[2]) or now
tokens = math.min(tonumber(ARGV[2]), tokens + (now - ts) * tonumber(ARGV[1]))
local ok = tokens >= tonumber(ARGV[3])
if ok then tokens = tokens - tonumber(ARGV[3]) end
redis.call('HSET', KEYS[1], 'tokens', tokens, 'ts', now)
redis.call('EXPIRE', KEYS[1], 3600)
return ok and 1 or 0Что хотят услышать
- Почему нужен общий счётчик и где разместить лимитер.
- Атомарность проверки и списания (Lua), время из одного источника.
- Выбор алгоритма и его стоимость в Redis.
- Снижение задержки локальными квотами, поведение при отказе Redis.
- Ответ клиенту и наблюдаемость.
Частые ошибки
- Читать счётчик и записывать отдельными командами — гонка между шлюзами.
- Не продумать, что делать при недоступности Redis.
Проверьте себя
Один верный вариант. Подумайте, потом откройте ответ.
Показать варианты и ответ
- ✗Лимит в памяти каждого экземпляра без синхронизации точен
- ✗Проверку и списание делать двумя независимыми запросами к Redis
- ✓Общее хранилище (Redis), атомарное списание одним скриптом
- ✗Лимитер лучше ставить после записи в базу
Связанные вопросы
Разбор собеседования с sotto
После собеседования sotto разбирает разговор: оценка, каждый вопрос — хорошо, частично или слабо, чего не хватило в ответе и над чем поработать к следующему разу. Первые 30 минут бесплатно.