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

Архитектура бэкенда

Пропускная способность под нагрузкой: хвостовая задержка и насыщение

Суть Под нагрузкой среднее лжёт. Теория очередей говорит, что задержка плоская до ~70–80% утилизации, затем взрывается нелинейно, и один медленный отрезок в голове задерживает всё за ним. Один цикл это одно ядро, поэтому следи за хвостом и утилизацией event loop, а не за средним.
Высота — путь к senior
НольJuniorMiddleSenior
Ты на senior-высоте — в орбите
◷ 17 min

Сервис работает на 50% CPU со средним временем ответа 20 мс — комфортно. Трафик растёт на 40%, CPU лезет к 75%, и среднее едва двигается до 25 мс. Затем небольшой всплеск толкает его к 82%, и p99 прыгает с 80 мс до 1400 мс. Ничего не сломалось; код не менялся. Система пересекла колено кривой очередей, где задержка перестаёт быть линейной по нагрузке. Среднее прятало это всё время — а среднее это ровно тот не тот показатель, за которым надо следить.

Почему задержка взрывается у насыщения

Сервер — это система очередей: запросы приходят, ждут занятый ресурс, обслуживаются. Теория очередей даёт форму ожидания, и она не линейна. Пока утилизация (ρ) лезет вверх, время ожидания масштабируется примерно как 1 / (1 − ρ) — плоско и дружелюбно до ~70–80%, затем обрыв. При ρ = 0,5 фактор 2; при ρ = 0,8 он 5; при ρ = 0,95 он 20. Поэтому сервер может впитывать нагрузку незаметно долго, а потом упасть со стены: колено кривой, где малые приросты темпа прихода рождают огромные приросты ожидания. Урок — работать с запасом — целиться в 60–70% утилизации связывающего ресурса — именно потому, что последние 20% ёмкости стоят нелинейной задержки и не оставляют ничего на всплески.

Закон Литтла (L = λ × W) связывает всё вместе: число запросов в системе равно темпу прихода, умноженному на время-в-системе. Когда W (задержка) взрывается у насыщения, L (конкурентные запросы в полёте) взрывается с ней — больше памяти, больше открытых соединений, больше давления — что та же спираль неограниченной конкурентности из прошлого урока, теперь движимая самой системой, а не твоим кодом.

Среднее лжёт; следи за хвостом

Среднее складывает медленные запросы в быстрые и прячет их. Реальные пользователи живут в хвосте — p95, p99, p99.9 — и хвост это где насыщение, паузы GC и медленные зависимости проявляются первыми. p50 в 20 мс с p99 в 1400 мс означает, что 1 из 100 запросов в 70× медленнее типичного; для страницы, делающей 100 бэкенд-вызовов, это почти гарантирует, что каждая страница хотя бы раз попадёт в плохой хвост (fan-out усиливает хвосты). Старшие команды ставят SLO на перцентили, а не на средние, и алертят на тренды p99, потому что среднее будет читаться «нормально» прямо до сбоя.

Head-of-line blocking, снова — на масштабе системы

Заморозка из раннего урока была внутри одного процесса; та же форма появляется поперёк очереди. Head-of-line blocking — это когда один медленный элемент впереди задерживает всё за ним: единственный медленный запрос, держащий ресурс, медленная upstream-зависимость, один жирный синхронный отрезок на цикле. Малая доля застрявшей работы каскадирует — задокументированный паттерн: ~3% застряли и задерживают ~30% запросов — потому что всё, что в очереди за застрявшим, наследует его ожидание. Поэтому один невынесенный CPU-отрезок (урок 3) или один неограниченный fan-out (урок 5) не просто вредит себе; он отравляет хвост несвязанному трафику.

Один цикл — это одно ядро — измеряй ELU, выбирай модель

Хребет юнита, заявленный как факт ёмкости: один event loop Node — это одно ядро JavaScript. Он чудесно масштабируется по конкурентному I/O, но не по CPU. Поэтому сигнал насыщения для Node-сервиса — утилизация event loop (ELU) — доля времени, когда цикл занят против простоя — в паре с задержкой event loop. ELU около 1,0 означает, что цикл — узкое место, и единственные фиксы — делать меньше на запрос, выносить CPU или добавлять циклы (cluster / больше инстансов).

Отступив, модель рантайма — это выбор, подогнанный под нагрузку. Event loop блистает на высококонкурентном I/O при малой памяти, но не даёт параллелизма для CPU. Другие модели меняют по-другому: горутины Go (планировщик M:N, начальные стеки ~2 КБ, вытесняющий) и виртуальные потоки Java (~сотни байт оверхеда, смонтированы на потоках-носителях) дают писать блокирующий код, масштабирующийся до миллионов дешёвых «потоков» с реальным многоядерным параллелизмом. Ни одна не лучшая повсюду — старшее суждение в том, чтобы знать свою нагрузку (I/O-bound vs CPU-bound, уровень конкурентности, бюджет памяти) и выбрать модель, чьи компромиссы подходят, затем гонять её с запасом и следить за хвостом.

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

Почему целиться в ~70% утилизации, а не выжимать до 95% ради эффективности? Потому что цена последней доли утилизации платится в валюте, которую ощущают пользователи — хвостовой задержке — и она нелинейна. Переход с 70% на 95% утилизации примерно учетверяет ожидаемое ожидание в очереди (1/(1−0,7) ≈ 3,3 против 1/(1−0,95) = 20), поэтому ты меняешь скромную экономию железа на яростную регрессию задержки и нулевой запас на всплеск: всплеск трафика в 10% при 70% впитывается, тот же всплеск при 95% перекидывает тебя за 100% и очередь растёт без границ. «Эффективность», измеряемая как высокая средняя утилизация, — ловушка, оптимизирующая дешёвый ресурс (циклы CPU) за счёт дорогого (предсказуемая задержка и устойчивость к всплескам). Планирование ёмкости на самом деле планирование хвостовой задержки.

Утилизация ρФактор очереди 1/(1−ρ)Что наблюдаешь
0,5Плоско, комфортно
0,7~3,3×Ещё нормально, у колена
0,8Хвост начинает растягиваться
0,9520×p99 взрывается, нет запаса на всплеск
Викторина

CPU идёт 75% → 82%, и p99 прыгает с 80 мс до 1400 мс, пока среднее едва двигается. Что это объясняет?

Викторина

Почему среднее время ответа — обманчивая цель SLO по сравнению с p99?

Викторина

Для Node-сервиса что самый прямой сигнал насыщения и что он подразумевает около 1,0?

Вспомните перед уходом
  1. 01
    Почему задержка взрывается у насыщения и что это значит для планирования ёмкости?
  2. 02
    Почему следить за хвостом (p99), а не за средним, и как fan-out делает это хуже?
  3. 03
    Что значит «один цикл — это одно ядро» для масштабирования и чем отличаются другие модели рантайма?
Итог

Под нагрузкой среднее — не тот показатель. Сервер — это очередь, и ожидание в очереди масштабируется как 1/(1−ρ): комфортно плоско до колена около 70–80% утилизации, затем нелинейный обрыв, где ρ=0,95 значит двадцатикратное ожидание, поэтому сервис переходит от «нормально» к «горит» без смены кода. Закон Литтла связывает этот взрыв задержки с соответствующим взрывом конкурентного заполнения, поэтому насыщение питает ту же спираль памяти-и-соединений, что и неограниченный fan-out. Поскольку среднее прячет медленные запросы, хвост — p95, p99, p99.9 — это настоящий сигнал, и fan-out делает один-из-ста медленный вызов типичным опытом страницы на сто вызовов. Head-of-line blocking переносит внутрипроцессную заморозку на масштаб системы, где несколько процентов застрявшей работы задерживают треть запросов, поэтому невынесенный CPU-отрезок или неограниченный map отравляет несвязанный трафик. Факт ёмкости под всем этим: один цикл Node — это одно ядро, ELU — его датчик насыщения, а сама модель рантайма — event loop, горутины, виртуальные потоки — это решение под нагрузку, гоняемое с запасом. Это закрывает юнит async-и-блокировки и передаёт следующей заботе, что он постоянно вызывал: пулинг дорогих downstream-соединений, которые защищала ограниченная конкурентность.

Связанные уроки
встречается в185
Продолжить восхождение ↑Async vs blocking: тест с множественным выбором
хоткеи развернуть
поиск
K
пред. пьеса
k
след. пьеса
j
тиры
t
это меню
?
sources3
expand
  1. 01
  2. 02
  3. 03

Trademarks belong to their respective owners. Editorial reference only.