Как ограничить количество запросов клиента к сервису
Что проверяют
Знаете ли вы алгоритмы ограничения частоты и понимаете ли, где его ставить и где хранить счётчики, когда у приложения несколько воркеров. Следом обычно спрашивают, по какому ключу считать клиента и что он получит при превышении.
Как отвечать
Сначала алгоритмы и их компромиссы:
- fixed window:
INCRключа видаuser:42:202609111405плюсEXPIREв Redis. Просто, но на стыке окон клиент может сделать вдвое больше лимита; - sliding window log: метки времени в sorted set (
ZADD,ZREMRANGEBYSCORE,ZCARD). Точно, но память растёт с числом запросов; - sliding window counter: взвешенная сумма текущего и прошлого окна, хороший компромисс;
- token bucket: ведро наполняется с постоянной скоростью и допускает короткий всплеск при заданном среднем темпе. Самый частый выбор для API;
- leaky bucket: выравнивает поток, по нему работает
limit_reqв nginx.
Где ставить. Грубую защиту от флуда лучше на входе: nginx (limit_req_zone, burst, nodelay) или API-шлюз. Бизнесовые лимиты по тарифу держат в приложении: slowapi для FastAPI, throttling-классы DRF (UserRateThrottle, AnonRateThrottle, DEFAULT_THROTTLE_RATES), django-ratelimit, Flask-Limiter.
Хранилище общее, обычно Redis, а проверка и инкремент атомарны: Lua-скрипт или MULTI с INCR и EXPIRE с флагом NX, чтобы TTL не продлевался каждым запросом. Ключ клиента: id пользователя или API-ключ, IP только для анонимов. Ответ при превышении: 429 Too Many Requests и заголовок Retry-After.
Если вопрос про исходящие запросы вашего сервиса к чужому API, то нужен лимитер на своей стороне: asyncio.Semaphore для числа параллельных запросов и aiolimiter для частоты.
Где спотыкаются
Хранят счётчики в памяти процесса. При gunicorn с четырьмя воркерами клиент получает четыре лимита, у DRF с LocMemCache та же проблема.
Берут IP из X-Forwarded-For как есть. Заголовок подделывается, доверять ему можно только от своего прокси, во Flask для этого есть ProxyFix из Werkzeug.
Делают GET, сравнение и INCR тремя отдельными командами. Между ними успевают пройти параллельные запросы, и лимит пробивается.