Что такое rate limiting на разных уровнях (user, API key, IP, global) и как реализовать?
Краткий тезис
Rate limiting (ограничение частоты запросов) — механизм защиты API и бэкенда от перегрузки, злоупотреблений и DDoS-атак. Он реализуется на нескольких уровнях: по пользователю (user), API-ключу, IP-адресу и глобально (global). Каждый уровень имеет свой порог и алгоритм (чаще всего sliding window counter, token bucket или leaky bucket). Надёжная реализация в распределённой системе требует использования Redis для атомарных операций и хранения состояния с временем жизни.
1. Термин: Rate Limiting (ограничение частоты запросов)
Rate limiting — это практика, при которой сервер или прокси ограничивает количество запросов от клиента (или группы клиентов) за определённый временной интервал. Основные цели:
- Защита от DDoS-атак и перегрузки бэкенда.
- Справедливое распределение ресурсов между клиентами.
- Предотвращение ошибок (например, случайный runaway loop в коде клиента).
- Монетизация API (разные тарифы с разными лимитами).
Без rate limiting один злоумышленник или баг может положить всё приложение.
2. Уровни Rate Limiting
Каждый уровень отслеживает разного субъекта:
| Уровень | Ключ | Типичный лимит | Пример |
|---|---|---|---|
| User (пользователь) | User ID (или идентификатор сессии) | 100 req/min | После аутентификации; fair use |
| API key | Значение ключа | 1000 req/min | Для разработчиков, внешних интеграций |
| IP-адрес | IP (или подсеть) | 50 req/min | Анонимные запросы, защита от сканирования |
| Global (глобальный) | Единый ключ для всего сервиса | 10000 req/min | Защита инфраструктуры от пиковых нагрузок |
Комментарии
- User обычно точнее, но требует аутентификации. IP проще, но может быть общим для нескольких пользователей (NAT).
- API key гибче: можно задавать разные лимиты для разных тарифов.
- Global — last resort; если превышен, значит что-то не так с балансировкой или идёт атака.
3. Алгоритмы Rate Limiting
3.1 Fixed Window (фиксированное окно)
- Счётчик обнуляется каждую минуту/час.
- Проблема burst в конце окна может перегрузить систему (например, 100 запросов за 0.1 сек).
3.2 Sliding Window (скользящее окно)
- Учитываются запросы за последние N секунд/минут.
- Более плавное ограничение, но сложнее реализовать.
3.3 Token Bucket (ведро токенов)
- Ведро вмещает B токенов. Токены добавляются с постоянной скоростью (R токенов/сек). Каждый запрос забирает один токен. Если токенов нет — 429.
- Плюсы допускает короткие всплески (burst), при этом средняя скорость ограничена.
- Минусы нужна настройка B и R.
3.4 Leaky Bucket (дырявое ведро)
- Запросы ставятся в очередь и обрабатываются с фиксированной скоростью. Если очередь переполнена — 429.
- Плюсы сглаживает bursts, полный контроль скорости.
- Минусы вносит задержку.
3.5 Sliding Window Counter (скользящее окно со счётчиком) — де-факто стандарт
- Использует sorted set в Redis: каждый запрос добавляется с timestamp как score. Удаляем элементы старше окна, считаем количество оставшихся.
- Компромисс: точность и простая реализация.
4. Реализация sliding window counter с Redis
import time
import redis
r = redis.Redis(host='localhost', port=6379, decode_responses=True)
def check_rate_limit(key: str, limit: int, window: int = 60) -> bool:
"""
key: 'rate_limit:user:<user_id>' или 'rate_limit:ip:<ip>'
limit: максимальное количество запросов
window: размер окна в секундах (по умолчанию 60)
Возвращает True, если запрос разрешён, False - если превышен лимит.
"""
current = time.time()
pipe = r.pipeline()
# Удаляем записи старше окна
pipe.zremrangebyscore(key, 0, current - window)
# Добавляем текущий запрос
pipe.zadd(key, {str(current): current})
# Получаем количество элементов в сете (текущее)
pipe.zcard(key)
# Устанавливаем TTL, чтобы ключ не висел вечно
pipe.expire(key, window + 1)
_, _, count, _ = pipe.execute()
return count <= limit
Объяснение
- Каждый запрос сохраняет свой timestamp (с точностью до секунды или миллисекунды).
zremrangebyscoreудаляет все записи старшеcurrent - window.zaddдобавляет новый запрос.zcardвозвращает количество запросов в текущем окне.expireгарантирует очистку ключа после окна (экономия памяти).
Недостатки
- В Redis хранится один элемент на запрос — при большом количестве запросов может быть много данных.
- Альтернатива: sliding window log (хранить только последние timestamp) или token bucket.
5. Реализация Token Bucket с Redis
import time
import redis
r = redis.Redis(host='localhost', port=6379, decode_responses=True)
def token_bucket(key: str, capacity: int, refill_rate: float, refill_interval: float = 1.0) -> bool:
"""
capacity: максимальное число токенов (ведро)
refill_rate: токенов в секунду (R)
refill_interval: период пополнения (например, 1.0 сек)
"""
# Используем hash: храним текущее количество токенов и timestamp последнего пополнения
current_time = time.time()
pipe = r.pipeline()
pipe.hgetall(key)
pipe.execute()
# первый запуск
if not data:
tokens = capacity
last_refill = current_time
pipe.hset(key, mapping={'tokens': tokens, 'last_refill': last_refill})
pipe.expire(key, 3600)
pipe.execute()
return tokens > 0
else:
tokens = float(data.get('tokens', capacity))
last_refill = float(data.get('last_refill', current_time))
# сколько токенов добавить за прошедшее время
delta = current_time - last_refill
new_tokens = min(tokens + delta * refill_rate, capacity)
if new_tokens >= 1:
# забираем токен
pipe.hset(key, 'tokens', new_tokens - 1)
pipe.hset(key, 'last_refill', current_time)
pipe.expire(key, 3600)
pipe.execute()
return True
else:
return False
Примечание реализация выше не атомарна без Lua-скрипта. Для продакшена используйте Lua или паттерны с WATCH.
6. Обработка превышения лимита
При превышении сервер возвращает HTTP 429 Too Many Requests с заголовком Retry-After. Рекомендуется также включать в ответ:
X-RateLimit-Limit— общий лимит,X-RateLimit-Remaining— сколько осталось,X-RateLimit-Reset— Unix timestamp, когда лимит обновится.
Пример ответа:
HTTP/1.1 429 Too Many Requests
Content-Type: application/json
Retry-After: 30
X-RateLimit-Limit: 100
X-RateLimit-Remaining: 0
X-RateLimit-Reset: 1680000000
{"error": "rate limit exceeded", "retry_after": 30}
7. Распределённое Rate Limiting
Когда сервис масштабируется на несколько инстансов, rate limiting должен быть централизованным (через Redis или другую разделяемую БД). Проблемы:
- Согласованность — Redis cluster может иметь задержки. Используйте локальный кэш + Redis (примерно: счётчик в Redis, но для принятия решения можно использовать приблизительные данные с локальным допуском).
- Производительность — каждый запрос делает несколько операций с Redis. Оптимизация: пакетная обработка через pipeline (как в коде выше).
- Отказоустойчивость — при падении Redis можно либо блокировать запросы (fail-closed), либо пропускать с риском перегрузки (fail-open). Обычно выбирают fail-open с предупреждением.
Популярные готовые решения: Redis Rate Limiter (с Lua-скриптом), API Gateway (Kong, AWS API Gateway), Envoy с фильтрами rate limit.
8. Стратегии для разных уровней (комбинация)
На практике используется иерархия:
- Сначала проверяется global — если превышен, сразу 429 (аварийный режим).
- Затем API key (или user) — для аутентифицированных запросов.
- Для неаутентифицированных — IP.
- Внутри уровня можно комбинировать: например, суммарный лимит по API key + лимит на IP внутри этого ключа.
Пример реализации с несколькими ключами:
def check_all(user_id, api_key, ip):
if not check_rate_limit('global', 10000):
return False
if api_key and not check_rate_limit(f'apikey:{api_key}', 1000):
return False
if user_id and not check_rate_limit(f'user:{user_id}', 100):
return False
if not check_rate_limit(f'ip:{ip}', 50):
return False
return True
Порядок проверки сначала глобальный, потом более детальные, чтобы при атаке быстро отсечь все запросы.
9. Best Practices и мониторинг
- Всегда устанавливайте TTL на ключи Redis, чтобы не забивать память.
- Используйте согласованные часы (NTP) или время Redis (избегайте рассинхронизации).
- Делайте rate limiting на уровне Gateway (API Gateway) — это снимает нагрузку с бэкенда.
- Мониторьте количество отказов 429 в метриках. Внезапный рост может сигнализировать об атаке или ошибке клиента.
- Graceful degradation: при превышении лимита возвращайте информативную ошибку и дайте клиенту понять, когда можно повторить (Retry-After).
- Адаптивное rate limiting: динамически менять лимиты на основе текущей нагрузки (например, если CPU > 80%, снижаем лимиты на 20%).
10. Пет-проект для закрепления
Задача Реализовать микросервис на FastAPI с rate limiting для трёх уровней (IP, API key, global) с использованием Redis.
Инструменты
Шаги:
- Развернуть Redis локально (docker run -p 6379:6379 redis).
- Написать middleware для FastAPI, который перехватывает все запросы.
- В middleware определять клиента: берём API key из заголовка
X-Api-Key(если есть), иначе IP изX-Forwarded-Forилиremote_addr. - Вызывать
check_rate_limitдля IP (50 req/min) и, если есть API key, дополнительно для API key (1000 req/min). - Добавить глобальный лимит (10000 req/min) — если превышен, отдавать 429 независимо от других уровней.
- Вернуть заголовки X-RateLimit-*.
- Написать тесты: проверить, что при превышении лимита возвращается 429, а при нормальной нагрузке — 200.
Ожидаемый результат
- Работающий сервер с эндпоинтом
/api/hello. - Тесты, демонстрирующие корректное срабатывание rate limit.
- Понимание всех нюансов: TTL, pipeline, обработка отсутствующего ключа.
11. Связь с другими вопросами
| Вопрос | Тема |
|---|---|
| 410 | Кэширование запросов может снизить нагрузку, rate limiting — дополнительная защита. |
| 414 | Как клиент должен повторять запросы после 429 с учётом Retry-After. |
| 416 | Rate limiting — неотъемлемая часть микросервисной архитектуры, часто выносится в API Gateway. |
| 417 | Rate limiting — один из механизмов graceful degradation при перегрузке. |
| 418 | Распределённый rate limiting требует trade-off между согласованностью и доступностью. |
| 420 | Метрики rate limiting (кол-во 429) должны быть в дашбордах мониторинга. |
Навигация
- Предыдущий: 414
- Следующий: 416
- Индекс: 00. Индекс разборов