Kravchenko

Web Lab

АудитБлогКонтакты

Kravchenko

Web Lab

Разрабатываем сайты и автоматизацию на современных фреймворках под ключ

Услуги
ЛендингМногостраничныйВизитка
E-commerceБронированиеПортфолио
Навигация
БлогКонтактыАудит
Обратная связь
+7 921 567-11-16
info@kravlab.ru
с 09:00 до 18:00

© 2026 Все права защищены

•

ИП Кравченко Никита Владимирович

•

ОГРНИП: 324784700339743

Политика конфиденциальности

Ограничение скорости и защита от перегруза: выдерживаем пиковые нагрузки без падений и лишних трат

Разработка и технологии21 апреля 2026 г.
Пики трафика — это не только про «сайт лёг», но и про рост расходов, срывы SLA и злых пользователей. Разберём, как с помощью ограничения скорости запросов, приоритезации и управляемого сброса нагрузки переживать всплески без аварий и масштабирования «на авось». Дадим готовые примеры конфигураций и кода, метрики и чек-лист внедрения.
Ограничение скорости и защита от перегруза: выдерживаем пиковые нагрузки без падений и лишних трат

Оглавление

  • Почему бизнесу нужна защита от перегруза
  • Базовые принципы устойчивости под нагрузкой
  • Алгоритмы ограничения скорости (rate limiting) простыми словами
  • Где ставить лимиты: на периметре, в приложении, в Redis
    • Периметр: пример Nginx
    • Распределённый лимит: Redis + Lua (атомарно)
  • Приоритеты и управляемый сброс нагрузки: как защитить «деньги»
  • Адаптивные лимиты и динамическое управление
  • Наблюдаемость и SLO: что мерить, чтобы не гадать
  • Тестирование: нагрузка и отказоустойчивость без боли
  • Чек-лист внедрения
  • Пара советов напоследок

Почему бизнесу нужна защита от перегруза

Реклама выстрелила, на сайт пришёл пик трафика, а следом — ошибки оплаты и таймауты. Боты ломятся в API из-за промокода, и законопослушные покупатели получают 429. Интеграция партнёра даёт «бурст» из десятков тысяч запросов и сжигает ваш бюджет в облаке. Всё это симптомы одной проблемы: система не умеет дозировать входящий поток и защищать критические части.

Что получает бизнес от грамотных лимитов и защиты от перегруза:

  • Стабильный чек-аут и авторизация в пике — деньги не теряются.
  • Контроль расходов на инфраструктуру и сторонние сервисы — платим за то, что реально обрабатываем.
  • Меньше ночных аварий — предсказуемое поведение под нагрузкой.
  • Быстрые интеграции с партнёрами — прозрачные правила потребления API.

Базовые принципы устойчивости под нагрузкой

Прежде чем ставить лимиты, приложим «базовую гигиену» устойчивости:

  • Таймауты везде. Внешние вызовы, БД, кэш, очереди — без бесконечных ожиданий.
  • Повторные попытки с «джиттером». Повторяем осторожно, чтобы не устроить лавину.
  • Ограничение параллельности. Не запускаем тысячу тяжёлых операций одновременно, держим пулы.
  • Очереди и сглаживание. На входе — очередь, на выходе — стабильная скорость обработки.
  • Отказоустойчивые ответы. Лучше быстрый предсказуемый отказ (429/503), чем вечный спиннер и таймаут.

Алгоритмы ограничения скорости (rate limiting) простыми словами

  • Фиксированное окно. Считаем запросы в пределах секунды/минуты. Просто, но граничные эффекты (начало нового окна) дают «шипы».
  • Скользящее окно. Смотрим «последние N секунд» — сглаживает пики, но тяжелее считать.
  • Ведро с токенами (token bucket). Запрос «покупает» токен из ведра; токены подтекают в ведро с заданной скоростью. Позволяет краткие всплески, но держит среднюю скорость.
  • Протекающее ведро (leaky bucket). Выливаем с фиксированной скоростью, лишнее — в очередь или в отказ. Хорош для сглаживания.

Как выбрать:

  • Нужны краткие всплески без срыва средней — token bucket.
  • Нужна жёсткая «средняя» — leaky bucket или скользящее окно.
  • Хотите простоты на периметре — фиксированное окно (например, в Nginx) тоже ок, если есть запас.

Где ставить лимиты: на периметре, в приложении, в Redis

Идеально — многоуровневая защита:

  • На периметре (CDN/WAF/Nginx) — дёшево блокируем мусор, ботов, слишком частые запросы за IP/токен.
  • В приложении — тонкие лимиты на пользователя, организацию, маршрут, метод, «дорогие» операции.
  • В кластере (Redis) — распределённые лимиты, общие счётчики для нескольких инстансов/регионов.

Периметр: пример Nginx

# 10 запросов в секунду на IP, с всплеском до 20 без задержки
limit_req_zone $binary_remote_addr zone=perip:10m rate=10r/s;
limit_conn_zone $binary_remote_addr zone=conns:10m;

server {
  listen 80;
  server_name api.example.com;

  location /api/ {
    limit_req zone=perip burst=20 nodelay;
    limit_conn conns 50;  # не больше 50 одновременных соединений с одного IP

    # Маршруты можно разделять по «дороговизне»
    if ($request_uri ~* "/api/reports") {
      limit_req zone=perip burst=5 nodelay;  # отчёты — дороже, сжимаем сильнее
    }

    proxy_read_timeout 5s;
    proxy_connect_timeout 1s;
    proxy_pass http://app_backend;
  }
}

Распределённый лимит: Redis + Lua (атомарно)

  • Храним состояние ведра в Redis: количество токенов и время последнего пополнения.
  • Скрипт Lua выполняется атомарно, без гонок и блокировок на уровне приложения.

Скрипт Lua для token bucket:

-- KEYS[1] = ключ ведра, например rl:{client}
-- ARGV[1] = ёмкость ведра (capacity)
-- ARGV[2] = скорость пополнения (tokens_per_second)
-- ARGV[3] = текущее время в миллисекундах (now_ms)
-- Возвращает: {allowed (0/1), tokens_left, retry_after_ms}

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

local data = redis.call('HMGET', key, 'tokens', 'ts')
local tokens = tonumber(data[1])
local ts = tonumber(data[2])

if tokens == nil then
  tokens = capacity
  ts = now
else
  if now > ts then
    local delta = (now - ts) / 1000.0
    local filled = math.floor(delta * rate)
    if filled > 0 then
      tokens = math.min(capacity, tokens + filled)
      ts = ts + filled * 1000 / rate
    end
  end
end

local allowed = 0
local retry_after = 0
if tokens > 0 then
  allowed = 1
  tokens = tokens - 1
else
  allowed = 0
  local needed = 1 - tokens
  retry_after = math.ceil(needed * 1000 / rate)
end

redis.call('HMSET', key, 'tokens', tokens, 'ts', ts)
redis.call('PEXPIRE', key, math.max(2000, math.ceil(1000 * capacity / rate)))

return {allowed, tokens, retry_after}

Мидлварь для Node.js (Express) с ioredis:

// npm i express ioredis
const express = require('express');
const Redis = require('ioredis');
const crypto = require('crypto');

const redis = new Redis(process.env.REDIS_URL || 'redis://localhost:6379');
const app = express();

// Загрузим Lua-скрипт и запомним SHA
const luaScript = `
-- KEYS[1] = ключ ведра, например rl:{client}
-- ARGV[1] = ёмкость ведра (capacity)
-- ARGV[2] = скорость пополнения (tokens_per_second)
-- ARGV[3] = текущее время в миллисекундах (now_ms)
-- Возвращает: {allowed (0/1), tokens_left, retry_after_ms}
local key = KEYS[1]
local capacity = tonumber(ARGV[1])
local rate = tonumber(ARGV[2])
local now = tonumber(ARGV[3])
local data = redis.call('HMGET', key, 'tokens', 'ts')
local tokens = tonumber(data[1])
local ts = tonumber(data[2])
if tokens == nil then
  tokens = capacity
  ts = now
else
  if now > ts then
    local delta = (now - ts) / 1000.0
    local filled = math.floor(delta * rate)
    if filled > 0 then
      tokens = math.min(capacity, tokens + filled)
      ts = ts + filled * 1000 / rate
    end
  end
end
local allowed = 0
local retry_after = 0
if tokens > 0 then
  allowed = 1
  tokens = tokens - 1
else
  allowed = 0
  local needed = 1 - tokens
  retry_after = math.ceil(needed * 1000 / rate)
end
redis.call('HMSET', key, 'tokens', tokens, 'ts', ts)
redis.call('PEXPIRE', key, math.max(2000, math.ceil(1000 * capacity / rate)))
return {allowed, tokens, retry_after}
`;

let scriptSha;
redis.script('LOAD', luaScript).then(sha => { scriptSha = sha; });

function hashId(id) {
  return crypto.createHash('sha1').update(String(id)).digest('hex').slice(0, 12);
}

function rateLimit({ capacity, rate, keyFn }) {
  return async (req, res, next) => {
    try {
      const id = keyFn(req);
      const key = `rl:${id}`;
      const now = Date.now();
      const resp = await redis.evalsha(
        scriptSha,
        1,
        key,
        String(capacity),
        String(rate),
        String(now)
      );
      const allowed = Number(resp[0]);
      const retryAfterMs = Number(resp[2]);
      if (!allowed) {
        res.set('Retry-After', Math.ceil(retryAfterMs / 1000));
        return res.status(429).json({ error: 'Too Many Requests' });
      }
      return next();
    } catch (e) {
      // На сбое Redis не душим легитимный трафик, но логируем
      console.error('rate limit error', e);
      return next();
    }
  };
}

// Лимит по пользователю, берём из токена/заголовка, иначе — по IP
const userLimiter = rateLimit({
  capacity: 30,        // до 30 запросов «запаса»
  rate: 10,            // 10 запросов в секунду в среднем
  keyFn: (req) => hashId(req.header('X-Client-Id') || req.ip)
});

// «Дорогой» маршрут — отчёты: жёстче
const reportsLimiter = rateLimit({
  capacity: 5,
  rate: 2,
  keyFn: (req) => 'reports:' + hashId(req.header('X-Client-Id') || req.ip)
});

app.use('/api', userLimiter);
app.get('/api/ping', (req, res) => res.json({ ok: true }));
app.get('/api/reports', reportsLimiter, async (req, res) => {
  // Дорогая операция
  await new Promise(r => setTimeout(r, 150));
  res.json({ status: 'report queued' });
});

app.listen(3000, () => console.log('API on :3000'));

Приоритеты и управляемый сброс нагрузки: как защитить «деньги»

Не все запросы равны:

  • Критический путь денег: логин, корзина, оплата, подтверждение.
  • Не критическое: обновление рекомендаций, тяжёлые отчёты, экспорт, периодический синк.

Что делаем:

  • Разделяем очереди и лимиты по классам трафика (high/normal/low).
  • При перегрузе — «сбрасываем» low сначала (быстрые 429/503 с Retry-After), normal — позже.
  • Для high — держим отдельные пулы соединений/воркеров и отдельные лимиты.

Итог — при перегрузе падает не весь сайт, а только второстепенные части. Выручка не страдает.

Адаптивные лимиты и динамическое управление

Статичный лимит хорош как старт. Дальше — адаптив:

  • Цель: держать p95 задержки ниже X мс и ошибку 5xx+429 ниже Y%.
  • Если p95 растёт — постепенно снижаем целевой RPS/параллельность.
  • Если p95 устойчиво ниже цели — растим лимит небольшими шагами.

Простейший алгоритм (каждые 10–30 секунд):

  • Если p95 > SLO, уменьшить лимит на 10% (но не ниже минимума).
  • Если p95 < 0.7*SLO и ошибок мало — увеличить на 5% (до максимума).

Работает как «круиз-контроль», удерживая систему в рабочей зоне без больших колебаний.

Наблюдаемость и SLO: что мерить, чтобы не гадать

  • Принято/отклонено: requests_allowed_total, requests_rejected_total (с причинами: лимит, защита бэкенда и т. п.).
  • Глубина очередей и время ожидания.
  • Параллельность/занятость воркеров.
  • Внешние зависимости: p95/p99, error rate, таймауты.
  • «Дешёвые» синтетические проверки ключевых маршрутов (логин/оплата).

Опубликуйте SLO по ключевым сценариям: «p95 оплаты < 1200 мс, ошибки < 0.5%». Лимиты и отбрасывание нагрузки должны служить этим целям.

Тестирование: нагрузка и отказоустойчивость без боли

  • Нагрузочные тесты на «опасных» маршрутах. Ступенчато повышайте RPS до отказов.
  • Тесты на отказ Redis/периметра: что делает приложение, если лимитер недоступен? Должно деградировать безопасно (обычно — пропускать, но с логами и алертами).
  • Прогон с ботоподобным трафиком: короткие всплески, множественные соединения.

Пример скрипта k6 для проверки лимитов:

import http from 'k6/http';
import { sleep, check } from 'k6';

export const options = {
  stages: [
    { duration: '10s', target: 50 },
    { duration: '20s', target: 200 },
    { duration: '10s', target: 0 },
  ],
};

export default function () {
  const res = http.get('http://localhost:3000/api/reports', {
    headers: { 'X-Client-Id': 'loadtest' },
  });
  check(res, {
    'ok or limited': (r) => [200, 429].includes(r.status),
  });
  sleep(0.2);
}

Чек-лист внедрения

  • Определите критические сценарии и их SLO.
  • Разделите трафик на классы приоритета. Назначьте отдельные лимиты и пулы.
  • Включите лимиты на периметре (CDN/WAF/Nginx) для базовой фильтрации.
  • Реализуйте распределённый лимит в Redis для точных сценариев (по пользователю/организации/маршруту).
  • Добавьте быстрые отказы (429/503) с Retry-After, понятные ошибки для клиента и документацию партнёрам.
  • Введите метрики «принято/отклонено», p95/p99, глубину очередей, алерты на отклонения.
  • Обкатайте под нагрузкой и на отказ Redis/периметра.
  • Внедрите адаптивную корректировку лимитов по SLA.
  • Регулярно пересматривайте политики по данным из наблюдаемости и стоимости.

Пара советов напоследок

  • Не пытайтесь «спасти всё». Чёткие приоритеты и быстрые отказы — ваш друг.
  • Документируйте лимиты в API. Партнёры ценят предсказуемость и заголовки Retry-After.
  • Не забывайте про права доступа: лимит должен считаться по реальному потребителю, а не только по IP.
  • Измеряйте влияние на деньги: сопоставляйте выручку, ошибки, расходы на инфраструктуру до/после.

Внедрив многоуровневые лимиты, приоритезацию и метрики, вы перестаёте бояться промо, PR-акций и ботов. Система работает ровно настолько, насколько вы ей позволяете, — и это экономит деньги и нервы.


RedisNginxустойчивостьограничение скоростиперегрузка