Что такое 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. Стратегии для разных уровней (комбинация)

На практике используется иерархия:

  1. Сначала проверяется global — если превышен, сразу 429 (аварийный режим).
  2. Затем API key (или user) — для аутентифицированных запросов.
  3. Для неаутентифицированных — IP.
  4. Внутри уровня можно комбинировать: например, суммарный лимит по 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.

Инструменты

Шаги:

  1. Развернуть Redis локально (docker run -p 6379:6379 redis).
  2. Написать middleware для FastAPI, который перехватывает все запросы.
  3. В middleware определять клиента: берём API key из заголовка X-Api-Key (если есть), иначе IP из X-Forwarded-For или remote_addr.
  4. Вызывать check_rate_limit для IP (50 req/min) и, если есть API key, дополнительно для API key (1000 req/min).
  5. Добавить глобальный лимит (10000 req/min) — если превышен, отдавать 429 независимо от других уровней.
  6. Вернуть заголовки X-RateLimit-*.
  7. Написать тесты: проверить, что при превышении лимита возвращается 429, а при нормальной нагрузке — 200.

Ожидаемый результат

  • Работающий сервер с эндпоинтом /api/hello.
  • Тесты, демонстрирующие корректное срабатывание rate limit.
  • Понимание всех нюансов: TTL, pipeline, обработка отсутствующего ключа.

11. Связь с другими вопросами

ВопросТема
410Кэширование запросов может снизить нагрузку, rate limiting — дополнительная защита.
414Как клиент должен повторять запросы после 429 с учётом Retry-After.
416Rate limiting — неотъемлемая часть микросервисной архитектуры, часто выносится в API Gateway.
417Rate limiting — один из механизмов graceful degradation при перегрузке.
418Распределённый rate limiting требует trade-off между согласованностью и доступностью.
420Метрики rate limiting (кол-во 429) должны быть в дашбордах мониторинга.

Навигация