lenec ru

← все посты

Rate limiting: алгоритмы, реализация и защита API от перегрузки

16K

Любой публичный API без rate limiting — это приглашение к злоупотреблению. Один агрессивный клиент способен положить сервис для всех остальных. Rate limiting ограничивает количество запросов от клиента за единицу времени, защищая от DDoS, brute-force атак и обеспечивая fair usage между пользователями.

Зачем rate limiting

  • Защита от DDoS и abuse — ограничение скорости запросов не даёт одному клиенту исчерпать ресурсы.
  • Fair usage — все пользователи получают равный доступ к API.
  • Контроль затрат — предотвращение неожиданных счетов за инфраструктуру.
  • Защита downstream-сервисов — API Gateway ограничивает нагрузку до того, как она дойдёт до базы данных.

Алгоритмы rate limiting

Fixed Window — простейший: считаем запросы в фиксированном окне (например, 1 минута). Проблема — burst на границе окон: 100 запросов в конце минуты + 100 в начале следующей = 200 за 2 секунды.

Sliding Window Log — храним timestamp каждого запроса, считаем попавшие в окно. Точный, но дорогой по памяти (O(n) на клиента).

Sliding Window Counter — компромисс: комбинируем счётчики текущего и предыдущего окна с весом. Точность ~99.7% при минимальной памяти.

Token Bucket — самый популярный. Корзина наполняется токенами с фиксированной скоростью. Запрос забирает токен. Нет токенов — отказ. Позволяет burst до размера корзины:

Token Bucket (capacity=10, refill=2/sec):

  [||||||||||]  ← 10 токенов (полная корзина)
  
  Burst: 10 запросов мгновенно ✓
  Затем: 2 запроса/сек (скорость пополнения)
  
  Запрос → забирает 1 токен
  Время  → добавляет 2 токена/сек (до capacity)

Leaky Bucket — запросы попадают в очередь фиксированного размера и обрабатываются с постоянной скоростью. Сглаживает burst, но добавляет latency.

Реализация на Redis

Redis — стандартный выбор для rate limiting: атомарные операции, TTL, высокая скорость. Token bucket через Lua-скрипт:

import redis
import time

RATE_LIMIT_LUA = """
local key = KEYS[1]
local capacity = tonumber(ARGV[1])
local refill_rate = tonumber(ARGV[2])
local now = tonumber(ARGV[3])

local data = redis.call('HMGET', key, 'tokens', 'last_refill')
local tokens = tonumber(data[1]) or capacity
local last_refill = tonumber(data[2]) or now

-- Пополняем токены
local elapsed = now - last_refill
local new_tokens = math.min(capacity, tokens + elapsed * refill_rate)

if new_tokens >= 1 then
    redis.call('HSET', key, 'tokens', new_tokens - 1, 'last_refill', now)
    redis.call('EXPIRE', key, math.ceil(capacity / refill_rate) + 1)
    return 1  -- разрешено
else
    redis.call('HSET', key, 'tokens', new_tokens, 'last_refill', now)
    redis.call('EXPIRE', key, math.ceil(capacity / refill_rate) + 1)
    return 0  -- отклонено
end
"""

r = redis.Redis()
script = r.register_script(RATE_LIMIT_LUA)

def is_allowed(client_id: str, capacity=100, refill_rate=10) -> bool:
    """Token bucket: capacity=100, refill=10 req/sec"""
    result = script(
        keys=[f"ratelimit:{client_id}"],
        args=[capacity, refill_rate, time.time()]
    )
    return bool(result)

Lua-скрипт выполняется атомарно — нет race conditions даже при тысячах конкурентных запросов.

Rate limiting в API Gateway

Nginx — встроенный модуль limit_req (leaky bucket):

# nginx.conf
http {
    limit_req_zone $binary_remote_addr zone=api:10m rate=10r/s;

    server {
        location /api/ {
            limit_req zone=api burst=20 nodelay;
            limit_req_status 429;
            proxy_pass http://backend;
        }
    }
}

Envoy — token bucket через route-level config:

# envoy rate limit
rate_limits:
  - actions:
      - remote_address: {}
    limit:
      requests_per_unit: 100
      unit: MINUTE

Kong — plugin rate-limiting с поддержкой Redis для distributed:

curl -X POST http://kong:8001/services/my-api/plugins \
  --data "name=rate-limiting" \
  --data "config.minute=100" \
  --data "config.policy=redis" \
  --data "config.redis_host=redis"

Distributed rate limiting

На нескольких инстансах возникает проблема: каждый узел видит только свою часть трафика. Решения:

  • Централизованный Redis — все узлы ходят в один Redis. Просто, но Redis становится SPOF и добавляет latency на каждый запрос.
  • Local + sync — каждый узел считает локально, периодически синхронизирует с Redis. Допускает кратковременное превышение лимита.
  • Sliding window с Redis Cluster — шардирование по ключу клиента. Масштабируется, но усложняет операции.

На практике для большинства API достаточно централизованного Redis с Lua-скриптами. Latency ~0.5ms на запрос — приемлемо для API с ответом в десятки миллисекунд.

Вывод

Rate limiting — обязательный компонент любого публичного API. Token bucket через Redis Lua — универсальное решение: атомарное, быстрое, поддерживает burst. Для простых случаев хватит nginx limit_req. Для микросервисов — Envoy или Kong с Redis-бэкендом. Главное — не забыть вернуть клиенту заголовки Retry-After и X-RateLimit-Remaining, чтобы он мог адаптироваться.

Комментарии 0

  • Будьте первым, кто оставит комментарий.

Войдите, чтобы оставить комментарий.