awesome-everything EN
↑ Обратно к восхождению

Сети и протоколы

Ограничение скорости: алгоритмы и архитектура

Суть Token bucket, leaky bucket, fixed window и sliding window counter делают разные компромиссы между точностью, памятью, толерантностью к всплескам и сложностью реализации.
Высота — путь к senior
НольJuniorMiddleSenior
Ты на middle-высоте — в небе
◷ 14 min

Ваш API обслуживает 100 запросов/сек в среднем. Во время DDoS он получает 100 000 запросов/сек. Можно блокировать по IP, но атакующий использует тысячу разных IP, каждый из которых делает 100 запросов/сек. Любой per-IP лимит на 100 запросов/сек заблокирует и ваших легитимных пользователей. Вопрос не в «сколько запросов?», а в «какой алгоритм правильно определяет злоупотребление против нормального трафика?»

Token bucket. Токены накапливаются в bucket с фиксированной скоростью R (например, 100 токенов/сек) до максимальной ёмкости C (например, 1 000 токенов). Каждый запрос стоит один токен; если bucket пуст, запрос отклоняется. Допускает контролируемые всплески: если bucket полон в покое, первые 1 000 запросов проходят мгновенно, затем темп ограничивается до 100/сек. Самый распространённый выбор для публичных API: прост (один счётчик + timestamp) и допускает легитимные всплески трафика.

Leaky bucket. Запросы ставятся в очередь и вычерпываются с фиксированной скоростью D, сглаживая трафик. Всплеск в 1 000 запросов не проходит мгновенно — они входят в очередь и вычерпываются со скоростью D в секунду. Это добавляет задержку для легитимного всплескового трафика, но производит идеально гладкий вывод. Полезен для downstream-сервисов, которые не могут обрабатывать всплески, но не для пользовательских API, где важна задержка.

Fixed window. Считает запросы в временном окне (например, в секунду). Прост, но имеет проблему граничного всплеска: два запроса на 59,999 секунде и два на 60,001 секунде считаются двумя в каждом окне — четыре запроса за 4 миллисекунды — но логика окна видит их как отдельные окна. Атакующий, эксплуатирующий это, может удвоить эффективный лимит скорости, выбирая время запросов на границе окна.

Sliding window log. Хранит каждый timestamp запроса в списке; отклоняет, если более N timestamp за последние T секунд. Самый точный — нет проблемы границы — но память масштабируется с количеством запросов. При 10 000 запросов/сек хранение каждого timestamp за 1 секунду требует 10 000 записей на клиента. Непрактично для высоконагруженных API.

Sliding window counter. Смешивает эффективность fixed-window с точностью sliding-window. Хранит счётчики для предыдущего окна (prev) и текущего окна (curr). На момент запроса T: estimated_count = prev * (1 - elapsed/window_size) + curr. Если estimated_count < limit, разрешить; иначе отклонить. Память: O(1) на клиента (два счётчика). Точность: приблизительная, но намного лучше fixed window. Практический стандарт для распределённых систем.

АлгоритмОбработка всплесковПамятьГраничная проблемаЛучше всего для
Token bucketРазрешает всплески (до C)O(1)НетПубличные API, пользовательские
Leaky bucketСглаживает всплески (добавляет задержку)O(очередь)НетСглаживание downstream
Fixed windowНет (граничная проблема)O(1)ДаПростые внутренние счётчики
Sliding window logТочно, без всплесковO(n запросов)НетМалонагруженные строгие лимиты
Sliding window counterПриблизительно, хорошая точностьO(1)МинимальнаяРаспределённые системы

Ограничение скорости Token bucket

1/3

Где обеспечивать ограничение скорости. Три уровня, каждый останавливает разные паттерны атак:

  1. Edge (CDN/скруббинг-центр) — блокирует крупнейшие атаки до попадания в вашу сеть. Per-IP, per-ASN или per-country лимиты. Контекст приложения недоступен.
  2. Шлюз/load balancer — может ограничивать скорость по пользователю, по ключу API или по origin-соединению. Имеет auth-контекст. Останавливает bot-подобные паттерны.
  3. Прикладной уровень — может ограничивать скорость по бизнес-логике (например, макс 3 неудачных попытки входа в минуту). Медленнее, но наиболее контекстуален.

Обеспечивайте на всех трёх уровнях: edge останавливает объёмные наводнения, шлюз — bot-подобные паттерны, прикладной уровень — злоупотребления аутентифицированных пользователей.

Распределённое ограничение скорости через Redis. При работе нескольких app-серверов локальный in-memory счётчик недостаточен — клиент может попадать на разные серверы и каждый видит лишь свою долю трафика. Решение: общий счётчик в Redis. Каждый запрос атомарно инкрементирует ключ Redis и проверяет лимит. Стоимость: каждое решение добавляет round-trip на Redis (0,5–1 мс в той же сети). При 100k запросов/сек это 50–100 мс добавленной задержки на запрос. Смягчение: локальное ограничение скорости (per-server счётчик, нет Redis) плюс периодическая синхронизация с Redis — торгует небольшой неточностью за микросекундные решения.

Викторина

Почему алгоритм fixed window имеет проблему граничного всплеска?

Адаптивное ограничение конкурентности. Вместо лимита в секунду — ограничить количество in-flight запросов. Когда in-flight >= max_concurrency, отклонять новые запросы. Это предотвращает каскадный отказ. Load shedding: когда глубина очереди превышает порог, вероятностно отклонять новые запросы — survival probability = max_queue / queue_depth. Пользователи видят быстрые ошибки 503 вместо timeouts; сервис остаётся отзывчивым для остальных. Компромисс: пользователи получают ошибки, а не медленные ответы, при перегрузке.

Расставь шаги по порядку

Упорядочьте алгоритмы ограничения скорости по эффективности памяти (меньше памяти — первый):

  1. 1 Fixed window (один счётчик на окно)
  2. 2 Sliding window counter (два счётчика на клиента)
  3. 3 Token bucket (один счётчик + timestamp)
  4. 4 Sliding window log (хранить каждый timestamp запроса)
Викторина

В production вы повышаете WAF на Paranoia Level 3 и false positives прыгают с 0,2% до 3%. Что делать первым?

Почему это работает

Почему token bucket — самый популярный стандарт для публичных API? Три причины: (1) он разрешает легитимный всплесковый трафик — пользователь, простаивавший 10 секунд, имеет полный bucket и может сделать 1 000 запросов мгновенно, не попадая под лимит; (2) O(1) памяти на клиента; (3) прост в реализации в Redis одним INCR + expiry. Граничная проблема fixed window вызывает реальные production-инциденты — пользователь на границе окна может легитимно превысить лимит в 2 раза — поэтому большинство API используют token bucket или sliding window counter.

Вспомните перед уходом
  1. 01
    Объясните, почему распределённое ограничение скорости через Redis добавляет задержку и как измерить влияние.
  2. 02
    Что такое load shedding и когда он предпочтительнее ограничения скорости?
  3. 03
    Token bucket с ёмкостью 1000 и скоростью рефилла 100/сек. Пользователь опустошает его за 0,1 секунды. Сколько ждать до 200 следующих запросов?
Итог

Алгоритмы ограничения скорости компромиссируют между толерантностью к всплескам, памятью и точностью. Token bucket (O(1), разрешает всплески) и sliding window counter (O(1), нет граничной проблемы) — практические варианты для production API. Fixed window имеет граничную проблему всплеска — два полуокна запросов могут превысить лимит за короткое время. Sliding window log самый точный, но O(n) памяти делает его непрактичным в масштабе. Распределённые системы нуждаются в Redis-backed счётчиках (добавляет ~0,5–1 мс на решение) или в локальных счётчиках с глобальной синхронизацией. Помимо per-second лимитов, адаптивное ограничение конкурентности отклоняет запросы, когда in-flight count превышает ёмкость сервера — останавливая перегрузки без необходимости определять IP атакующего.

Связанные уроки
встречается в258
Продолжить восхождение ↑WAF, межсетевые экраны, mTLS и HSTS
хоткеи развернуть
поиск
K
пред. пьеса
k
след. пьеса
j
тиры
t
это меню
?
sources3
expand
  1. 01
  2. 02
  3. 03

Trademarks belong to their respective owners. Editorial reference only.