Как ограничить пропускную способность эндпоинта (кол-во запросов в единицу времени)?nodejs-99

1. Основные подходы к rate limiting

Для контроля нагрузки на API используются несколько стратегий ограничения запросов:

a) Фиксированное окно

const requestCounts = new Map();

app.use('/api', (req, res, next) => {
  const ip = req.ip;
  const currentWindow = Math.floor(Date.now() / 60000); // 1 минута
  const key = `${ip}:${currentWindow}`;

  requestCounts.set(key, (requestCounts.get(key) || 0) + 1);

  if (requestCounts.get(key) > 100) { // Лимит 100 запросов в минуту
    return res.status(429).send('Too Many Requests');
  }

  next();
});

b) Скользящее окно

Более точный, но сложнее в реализации:

const redis = require('redis');
const client = redis.createClient();

app.use('/api', async (req, res, next) => {
  const ip = req.ip;
  const now = Date.now();
  const windowSize = 60000; // 1 минута

  const result = await client.multi()
    .zRemRangeByScore(ip, 0, now - windowSize)
    .zCard(ip)
    .zAdd(ip, { score: now, value: now.toString() })
    .expire(ip, windowSize / 1000)
    .exec();

  if (result[1] > 100) {
    return res.status(429).send('Too Many Requests');
  }

  next();
});

2. Готовые решения middleware

express-rate-limit

const rateLimit = require('express-rate-limit');

const limiter = rateLimit({
  windowMs: 15 * 60 * 1000, // 15 минут
  max: 100, // Лимит 100 запросов с одного IP
  standardHeaders: true,
  legacyHeaders: false,
});

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

fastify-rate-limit

Для Fastify приложений:

const fastify = require('fastify')();
const fastifyRateLimit = require('fastify-rate-limit');

fastify.register(fastifyRateLimit, {
  max: 100,
  timeWindow: '1 minute'
});

3. Распределенный rate limiting

Для кластеров и микросервисов используйте Redis:

const { RateLimiterRedis } = require('rate-limiter-flexible');

const rateLimiter = new RateLimiterRedis({
  storeClient: redisClient,
  points: 10, // 10 запросов
  duration: 1, // за 1 секунду
});

app.use(async (req, res, next) => {
  try {
    await rateLimiter.consume(req.ip);
    next();
  } catch (rejRes) {
    res.status(429).send('Too Many Requests');
  }
});

4. Тонкая настройка лимитов

По разным эндпоинтам

const apiLimiter = rateLimit({
  windowMs: 15 * 60 * 1000,
  max: 100
});

const authLimiter = rateLimit({
  windowMs: 60 * 60 * 1000,
  max: 5
});

app.use('/api/', apiLimiter);
app.use('/auth/', authLimiter);

Для авторизованных пользователей

app.use((req, res, next) => {
  if (req.user?.isPremium) {
    return next(); // Пропускаем лимиты для премиум пользователей
  }
  limiter(req, res, next);
});

5. Алгоритм Token Bucket

Более гибкий подход для burst-трафика:

class TokenBucket {
  constructor(capacity, refillRate) {
    this.capacity = capacity;
    this.tokens = capacity;
    this.lastRefill = Date.now();
    this.refillRate = refillRate; // tokens/ms
  }

  consume(tokens = 1) {
    this.refill();
    if (this.tokens >= tokens) {
      this.tokens -= tokens;
      return true;
    }
    return false;
  }

  refill() {
    const now = Date.now();
    const elapsed = now - this.lastRefill;
    this.tokens = Math.min(
      this.capacity,
      this.tokens + elapsed * this.refillRate
    );
    this.lastRefill = now;
  }
}

const bucket = new TokenBucket(10, 0.001); // 10 токенов, 1 токен/сек

6. HTTP заголовки для rate limiting

Правильные заголовки помогают клиентам:

app.use((req, res, next) => {
  res.set({
    'X-RateLimit-Limit': '100',
    'X-RateLimit-Remaining': remainingRequests,
    'X-RateLimit-Reset': Math.ceil(resetTime / 1000)
  });
  next();
});

7. Защита от DDoS

Дополнительные меры для серьезной защиты:

  1. Промежуточный счетчик: Nginx rate limiting перед Node.js
  2. Блокировка IP: При превышении жестких лимитов
  3. Гео-фильтрация: Ограничение по регионам
  4. CAPTCHA: Для подозрительных запросов

Резюмируем:

  1. Для простых случаев: express-rate-limit
  2. Для кластеров: Redis-based решения
  3. Для сложных сценариев: Кастомные алгоритмы (Token Bucket)
  4. Обязательно: Возвращайте правильные HTTP заголовки
  5. Дополнительно: Комбинируйте с Nginx/IPTables

Выбор реализации зависит от:

  • Масштаба приложения
  • Требований к точности
  • Архитектуры (монолит/микросервисы)
  • Необходимости burst-трафика