open atlas
↑ К треку
Основы System Design SD · 08 · 09

Строительные блоки: распределённый rate limiter от начала до конца

Практический проект: построй корректный распределённый token-bucket лимитер — общий счётчик, атомарный инкремент, 429 + Retry-After, fail-open — затем докажи, что гонка ушла под конкурентной нагрузкой, и порассуждай, как остальные блоки юнита сложатся в тот же сервис.

SD Senior ◷ 240 min
Уровень
ОсновыJuniorMiddleSenior

Прочитать, что гонка распределённого счётчика — сложная часть rate limiting, не то же самое, что увидеть, как наивный лимитер протекает под нагрузкой, а затем увидеть, как атомарная версия держит линию. Построй настоящий token-bucket лимитер на общем хранилище, эмпирически продемонстрируй гонку потерянного обновления неатомарной версией, почини её и добавь продакшен-поведения — 429 + Retry-After, fail-open — отделяющие игрушку от того, что ты бы выкатил.

Этот проект делает центральный урок юнита операционным: алгоритм тривиален, но корректность под конкурентностью — вся проблема. Ты построишь лимитер, измеришь гонку, починишь её атомарно и закалишь — затем порассуждаешь, как остальные блоки юнита встанут в тот же шлюз.

Проект
0 из 8
Цель

Построй и закали распределённый token-bucket лимитер на общем хранилище (Redis или эквивалент), эмпирически продемонстрируй гонку read-modify-write в наивной версии и докажи, что она исчезает в атомарной версии, и реализуй продакшен-поведения отказа и устойчивости — замыкая петлю между алгоритмом и сервисом, который ты бы реально гонял.

Требования
Критерии приёмки
  • Рабочий token-bucket лимитер (ёмкость B, пополнение R) на общем хранилище, обслуживающий несколько инстансов к одному счётчику на ключ.
  • Воспроизводимый эксперимент: таблица или график, показывающие рост перелива наивного лимитера с конкурентностью и атомарную версию, держащую на уровне лимита или ниже под той же нагрузкой — с приведёнными числами.
  • Корректный путь отказа: ответы 429 несут точный Retry-After, а заголовки позволяют клиенту сам тормозить до удара в стену.
  • Путь fail-open, доказанный тестом инъекции отказа: когда хранилище заставлено ошибаться или таймаутить, запросы пропускаются (проверено), метрика fail-open растёт, и kill switch отключает лимитер.
  • Короткий разбор: один абзац с количественной оценкой гонки и почему атомарность её убирает, и один абзац про слои (per-IP край против per-key шлюз) и что каждый защищает.
Senior-стретч
  • Добавь fencing для единичной задачи: введи маленький кусок работы, что должен идти ровно на одном инстансе (например, периодическая чистка вёдер), избери лидера через lease и защити общий ресурс монотонно растущим fencing token — затем симулируй застрявшего лидера и покажи, что операция с устаревшим token отвергается.
  • Добавь страж на фильтре Блума: перед лимитером используй фильтр Блума из «известных абьюзных ключей» (или, наоборот, валидных API-ключей), чтобы дёшево отвергать очевидный трафик до касания счётчика — размерь его под целевую долю ложноположительных и объясни, почему ложноположительный здесь безвреден.
  • Сравни алгоритмы эмпирически: реализуй лимитер фиксированного окна рядом с token bucket и продемонстрируй 2× граничный спайк (100 перед сбросом + 100 после), которого нет у token bucket / скользящего окна.
  • Добавь упорядоченный по времени ID запроса (UUIDv7 или Snowflake) в каждую строку лога запросов и покажи, как сортируемые ID позволяют range-scan лога по времени для реконструкции, какие именно запросы лимитер пропустил во время спайка.
Вспомните перед уходом
  1. 01
    Как эмпирически продемонстрировать, а затем починить гонку распределённого лимитера?
  2. 02
    Какие продакшен-поведения отделяют игрушечный лимитер от пригодного к выкатке?
  3. 03
    Как остальные блоки юнита сложились бы в тот же шлюз?
Итог

Этот проект превращает юнит строительных блоков в то, что можно гонять и измерять. Ты строишь token-bucket лимитер (ёмкость B, пополнение R) на общем хранилище, затем намеренно делаешь гонку видимой: наивная read-then-write версия пускает измеримо больше лимита под конкурентностью (потерянное обновление), и перелив растёт с нагрузкой. Затем ты чинишь её одной атомарной операцией — атомарным INCR или Lua-скриптом, делающим refill-and-decrement за один round trip — и перезапускаешь ту же нагрузку, чтобы показать исчезновение перелива. Это центральная истина юнита, сделанная эмпирической: алгоритм тривиален, корректность под конкурентностью — работа. Затем ты добавляешь поведения, делающие его пригодным к выкатке — 429 + Retry-After и самоторможение по заголовкам, и устойчивость fail-open с коротким таймаутом, метрикой и kill switch — и слоишь грубый per-IP и точный per-key лимиты. Наконец ты рассуждаешь о композиции: фильтр Блума для дешёвого предотвержения, упорядоченные по времени ID для range-scan лога, и выбор лидера + fencing token для защиты единичной задачи обслуживания от split-brain. Инженер, построивший это однажды, больше никогда не путает «я написал rate limiter» с «я написал корректный, распределённый rate limiter».

Что-то непонятно?

Задай вопрос по этому уроку. Вопросы анонимны и попадают напрямую автору — урок станет лучше.

хоткеи развернуть
поиск
K
пред. пьеса
k
след. пьеса
j
тиры
t
это меню
?
sources3
expand
  1. 01
  2. 02
  3. 03

Trademarks belong to their respective owners. Editorial reference only.