← назад к разделу

Представьте страницу поиска отеля. Она собирает ответ из пяти сервисов: цены, фото, отзывы, карта, рекомендации. Каждый отвечает в среднем за 20 мс — быстро. Но пятая часть страницы ждёт не «среднего», а самого медленного из пяти. И вот тут выясняется, что у сервиса цен каждый сотый запрос идёт 800 мс — и каждые двадцатые запросы пользователя упираются именно в него.

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

Что не так со средним

Среднее арифметическое складывает все значения и делит на число. Оно отвечает на вопрос «сколько времени заняла вся работа», а не «сколько ждёт один запрос». Тысяча запросов по 1 мс и один на 901 мс дают среднее 2 мс — формально честно, по сути обман: один клиент ждал почти секунду, и об этом числе нет ни слова.

Медиана (p50) честнее: она говорит, что половина запросов успела за это время. Половина — но не все. Для страницы из десяти последовательных вызовов вероятность упереться в медленный хотя бы раз близка к единице: хвост каждого вызова становится хвостом всей страницы.

Распределение задержек в реальных системах не похоже на колокол: у него длинный правый хвост из редких, но болезненных событий. Поэтому качество описывают перцентилями — значениями, за которыми лежит заданный процент запросов:

  • p50 (медиана) — половина запросов быстрее;
  • p99 — 99% быстрее, один из ста медленнее;
  • p999 — один из тысячи в хвосте.

Перцентиль p99 = 800 мс читается так: обычный запрос быстрый, но каждый сотый клиент платит почти секундой. Если таким запросом является списание со счёта — каждый сотый списание смотрит на застывший экран.

Откуда берутся хвосты

Хвосты не появляются сами — у каждого есть источник, и почти все они регулярны и предсказуемы.

Сборка мусора. Пока GC останавливает мир, стоят все запросы разом. Пауза приходит раз в минуты, но каждая стопорит всё — и попадает в хвост сразу многих запросов. Чем больше куча и чем реже сборки, тем длиннее единичный хвост.

Блокировки и contention. Два запроса схлестнулись за один лок — один из них ждёт. Пока конкурентных запросов мало, столкновения редки; под нагрузкой их частота растёт нелинейно, и то, что в тестах было невидимо, на проде становится каждосотым запросом.

Ресайзы структур данных. HashMap удвоился, ArrayList скопировался — все, кто в этот момент писали, шагнули в хвост. Ресайз длится миллисекунды, но при частоте операций в тысячи в секунду он обязательно кому-то попадётся.

Деоптимизация JIT. Горячий код пере компилировался на другой уровень оптимизации — пауза в разы больше обычной. Тот же эффект дают холодные ветки, которые впервые прошли после часа работы.

Сеть. Ретраансмит TCP, переполнение буфера коммутатора, «шумный сосед» по гипервизору — всё это не зависит от вашего кода, но попадает в ваши хвосты.

Фан-аут. Запрос ждёт самый медленный из десяти параллельных вызовов. Если каждый вызов имеет 1% хвостов, вероятность хотя бы одного хвостового вызова — примерно 10%. Хвост одного сервиса становится хвостом всей страницы.

Как измерять честно

Первое правило: перцентили нельзя усреднять. Если узел А отдал p99 = 100 мс и узел Б отдал p99 = 100 мс, «средний p99 кластера» не 100 мс: хвосты складываются по вероятности, и настоящий p99 выше. Считать нужно по объединённой выборке — для этого собирают гистограммы задержек:

  • в Prometheus — histogram_quantile() по бакетам, а не avg();
  • в Java — HDR-histogram: он не теряет точности на широких диапазонах.

Второе правило: мерить там, где живёт клиент. Перцентиль, посчитанный внутри сервиса, не видит очередь соединений, TLS-рукопожатие, сетевой путь и браузерные перерисовки. Клиентский p999 видит всё — поэтому хорошие команды смотрят оба и знают разницу между ними.

Третье: смотреть хвосты в разрезе. Общий p99 по всем запросам прячет то, что у одного эндпоинта хвост из-за GC, у другого — из-за фан-аута, у третьего — из-за отсутствия индекса. Разрез по маршруту превращает «что-то медленно» в конкретный список работ.

Что с этим делают

Оптимизация хвостов устроена иначе, чем оптимизация среднего: часто она делает среднее чуть хуже, чтобы убрать редкие провалы.

Изоляция медленных. Разным классам запросов — разные пулы потоков и соединений. Одна тяжёлая выгрузка не должна занимать слоты быстрых поисковых запросов.

Таймауты и fallback. Лучше быстрый приблизительный ответ, чем точный в хвосте: не дождались сервиса отзывов — покажите страницу без отзывов и догрузите. Каждый фан-аут получает бюджет времени.

Снижение частоты хвостов. GC с предсказуемыми паузами (G1/ZGC), отказ от ресайзов на горячем пути (фиксированные размеры, предвыделенные структуры), уменьшение contention (сегментирование локов, sharding счётчиков).

Уборка фан-аутов. Десять последовательных вызовов становятся одним BFF-запросом; данные, без которых можно жить, догружаются отдельно. Вероятность хвоста падает вместе с числом вызовов.

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

p99 p999 1 мс время ответа доля запросов

Масса слева — быстрые запросы, из которых состоит среднее. Хвост справа редкий, но именно он достаётся каждому сотому клиенту.

Коротко

  • Среднее отвечает «сколько заняла вся работа», а не «сколько ждёт один»; медиана молчит о половине.
  • Качество описывают перцентилями: p99 — один из ста в хвосте, p999 — один из тысячи.
  • Хвосты рождаются из GC, локов, ресайзов, JIT, сети и фан-аутов; хвост одного вызова становится хвостом страницы.
  • Перцентили не усредняют: гистограммы и объединённая выборка; мерить там, где живёт клиент.
  • Лечат изоляцией медленных, таймаутами с fallback и уборкой фан-аутов — даже ценой чуть худшего среднего.

Что почитать дальше