lenec ru

← все посты

Event loop в Node.js: phases, microtasks и оптимизация производительности

11K

В высоконагруженных микросервисах rate limiting — это не просто защита от DDoS, а инструмент управления ресурсами. Он предотвращает перегрузку сервисов, защищает от злоупотреблений API и обеспечивает справедливое распределение нагрузки между клиентами. Без rate limiting один клиент может исчерпать все ресурсы системы.

Выбор алгоритма влияет на поведение системы под нагрузкой. Рассмотрим три основных подхода и разберём, когда какой использовать.

Token Bucket: гибкость и burst-трафик

Token bucket — самый популярный алгоритм. Представьте ведро с токенами: каждый запрос забирает токен, токены пополняются с фиксированной скоростью. Если токены есть — запрос проходит, если нет — отклоняется.

Ключевое преимущество — поддержка burst-трафика. Если клиент долго не делал запросов, токены накопились, и он может отправить несколько запросов подряд.

class TokenBucket {
  private tokens: number;
  private lastRefill: number;
  
  constructor(
    private capacity: number,
    private refillRate: number
  ) {
    this.tokens = capacity;
    this.lastRefill = Date.now();
  }
  
  tryConsume(): boolean {
    this.refill();
    if (this.tokens >= 1) {
      this.tokens -= 1;
      return true;
    }
    return false;
  }
  
  private refill(): void {
    const now = Date.now();
    const elapsed = (now - this.lastRefill) / 1000;
    const tokensToAdd = elapsed * this.refillRate;
    this.tokens = Math.min(this.capacity, this.tokens + tokensToAdd);
    this.lastRefill = now;
  }
}

Недостаток: сложнее предсказать пиковую нагрузку. Если у вас 1000 клиентов с capacity=100, теоретически все могут одновременно отправить 100 запросов.

Leaky Bucket: стабильность и сглаживание

Leaky bucket работает как очередь с фиксированной скоростью обработки. Запросы попадают в ведро, а вытекают с постоянной скоростью. Если ведро переполнено — новые запросы отклоняются.

Главное отличие от token bucket — выходная скорость всегда постоянна. Даже если клиент долго молчал, он не может отправить burst. Это идеально для защиты downstream-сервисов с жёсткими ограничениями по throughput.

class LeakyBucket {
  private queue: number[] = [];
  
  constructor(
    private capacity: number,
    private leakRate: number
  ) {
    setInterval(() => this.leak(), 1000 / leakRate);
  }
  
  tryAdd(): boolean {
    if (this.queue.length < this.capacity) {
      this.queue.push(Date.now());
      return true;
    }
    return false;
  }
  
  private leak(): void {
    if (this.queue.length > 0) {
      this.queue.shift();
    }
  }
}

Sliding Window: точность без провалов

Sliding window считает запросы в скользящем временном окне. Например, не более 100 запросов за последние 60 секунд. В отличие от fixed window (который сбрасывается каждую минуту), sliding window не создаёт провалов на границах окон.

Проблема fixed window: клиент может отправить 100 запросов в 00:00:59 и ещё 100 в 00:01:00 — формально не нарушая лимит, но создав пик в 200 запросов за секунду.

class SlidingWindow {
  private requests: number[] = [];
  
  constructor(
    private limit: number,
    private windowMs: number
  ) {}
  
  tryRequest(): boolean {
    const now = Date.now();
    const cutoff = now - this.windowMs;
    this.requests = this.requests.filter(time => time > cutoff);
    
    if (this.requests.length < this.limit) {
      this.requests.push(now);
      return true;
    }
    return false;
  }
}

Реализация на Redis для distributed rate limiting

В микросервисной архитектуре с несколькими инстансами нужен централизованный rate limiter. Redis — стандартный выбор благодаря атомарным операциям и TTL.

Реализация token bucket на Redis через Lua-скрипт (атомарность гарантирована):

import Redis from 'ioredis';

const redis = new Redis();

const tokenBucketScript = `
local key = KEYS[1]
local capacity = tonumber(ARGV[1])
local rate = tonumber(ARGV[2])
local now = tonumber(ARGV[3])

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

local elapsed = now - last_refill
local tokens_to_add = elapsed * rate
tokens = math.min(capacity, tokens + tokens_to_add)

if tokens >= 1 then
  tokens = tokens - 1
  redis.call('HMSET', key, 'tokens', tokens, 'last_refill', now)
  redis.call('EXPIRE', key, 3600)
  return 1
else
  return 0
end
`;

async function rateLimitTokenBucket(
  userId: string,
  capacity: number,
  rate: number
): Promise<boolean> {
  const result = await redis.eval(
    tokenBucketScript,
    1,
    `rate_limit:${userId}`,
    capacity,
    rate,
    Date.now() / 1000
  );
  return result === 1;
}

Интеграция с Express

Для production используем express-rate-limit с Redis store:

import rateLimit from 'express-rate-limit';
import RedisStore from 'rate-limit-redis';
import Redis from 'ioredis';

const redis = new Redis();

const limiter = rateLimit({
  store: new RedisStore({
    client: redis,
    prefix: 'rl:',
  }),
  windowMs: 60 * 1000,
  max: 100,
  handler: (req, res) => {
    res.status(429).json({
      error: 'Too many requests',
      retryAfter: req.rateLimit.resetTime
    });
  }
});

app.use('/api/', limiter);

Подводные камни distributed rate limiting

Синхронизация между инстансами не мгновенная. Если Redis недоступен, нужна fallback-стратегия: либо пропускать все запросы (риск перегрузки), либо использовать локальный rate limiter (риск превышения лимита).

Race condition при высокой конкурентности: два инстанса одновременно читают счётчик, оба видят "99 запросов", оба пропускают — получается 101. Решение — Lua-скрипты или Redis transactions с WATCH.

Выбор ключа для rate limiting: по IP (легко обойти через прокси), по user ID (требует аутентификации), по API key (стандарт для публичных API). Для защиты от DDoS комбинируйте: грубый лимит по IP + точный по user ID.

Выводы

Token bucket — универсальный выбор для большинства API. Leaky bucket — когда критична стабильная нагрузка на backend. Sliding window — для точного контроля без провалов. В production используйте Redis с Lua-скриптами для атомарности и готовые библиотеки вроде express-rate-limit. Логируйте rate limit hits и анализируйте паттерны — возможно, лимиты слишком жёсткие или клиент ведёт себя подозрительно.

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

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

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