Оглавление
- Введение
- Теория: перцентили и хвосты
- Измерение и инструментализация
- Расследование инцидентов: от p99 на графике к конкретному запросу
- Паттерны снижения p99 и защиты хвоста
- Чек-лист и FAQ
- Заключение
Введение
Перцентили latency – это не “красивая статистика”, а способ говорить про пользовательский опыт и предсказуемость системы там, где среднее время ответа на запрос (avg) системно врет. Практика крупных распределенных систем показывает: даже редкие (например, 1%) “медленные” ответы на уровне компонентов превращаются в массовые (десятки процентов) “медленные” ответы на уровне сервиса, если запросы имеют fan-out (параллельные подзапросы в БД/кэш/шарды/микросервисы). Это один из центральных выводов работы “The Tail at Scale” и основа того, почему p95/p99 часто важнее среднего.
В аналитике перцентилей важно понимать два технических факта.
- Во-первых, Prometheus-мир различает Summary (streaming квантиль на клиенте) и Histogram (бакеты + вычисление квантиль на сервере). Ключевое следствие Summary-квантили в общем случае не агрегируются между инстансами, а Histogram агрегируются через sum by (le, …) и histogram_квантиль.
- Во-вторых, любые перцентили, полученные из бакетов, имеют оценочную ошибку, зависящую от ширины бакетов и интерполяции внутри них.
Наконец, расследование хвостов без связки “метрики – трейс” почти всегда превращается в гадание. Для этого в экосистеме существуют exemplars (корреляция метрики с trace/span id в формате OpenMetrics) и tail-based sampling в коллекторе, позволяющий сохранять именно медленные/ошибочные трейсы.
Примеры ниже намеренно “не завязаны” на конкретный язык/фреймворк, протоколы – HTTP и/или gRPC.
Типичная прод-картина: средняя задержка “нормальная”, CPU не красный, ошибок мало – но пользователи жалуются на “иногда все висит”. Это не парадокс: среднее значение устойчиво к хвосту, а пользовательский опыт – нет.
В “The Tail at Scale” эта идея показана буквально: если отдельный сервер почти всегда отвечает быстро, но имеет редкие “выбросы”, то при fan-out (запрос зависит от множества параллельных подзапросов) вероятность увидеть медленный ответ на уровне сервиса резко растет с масштабом. На графике в статье показан кейс, где при вероятности “медленного” (например, > 1s) на уровне одного leaf-узла 1/100 и fan-out на ~100 узлов уже около 63% запросов сервиса становятся “медленными” (>1s).

Рис.1 : график вероятности из статьи “The Tail at Scale”
Проблема усиливается тем, что современная backend-система редко “один процесс – одна база”. Обычно это цепочки зависимостей, очереди, пул соединений, кэш, шардирование, повторные запросы и таймауты. “The Tail at Scale” отдельно перечисляет источники вариативности: очереди, фоновые активности (компакции/обслуживание), сборка мусора, глобальное использование среды (сеть/файловые системы), лимиты мощности и т.д.
Поэтому цель статьи – дать не “определения ради определений”, а рабочую инженерную модель: как перцентили устроены, где и как их измерять, как их правильно считать, как расследовать p99-инциденты, и какие паттерны действительно уменьшают “хвост”, а какие лишь “полируют среднее”.
Теория: перцентили и хвосты
Что означают p50/p95/p99/p999
В мониторинге обычно говорят про квантили (0 – 1). Определение в прометеевской практике формулируется так:
квантиль – это наблюдаемое значение, которое занимает ранг примерно N среди N наблюдений (пример: 0.5 – медиана, 0.95 – 95-й перцентиль).
Практическая интерпретация:
- p50 – “типичный” запрос (медиана).
- p95 – 5% запросов хуже (дольше).
- p99 – 1% запросов хуже.
- p999 – 0.1% запросов хуже.
Ключевой нюанс: p99 – это не “редко”. При 10k RPS 1% – это 100 запросов в секунду, которые по определению лежат “в хвосте”.
Почему “хвост” важнее среднего: фан-аут и масштаб
Распределенный сервис часто ждет “последнего” из группы параллельных подзапросов: несколько шардов, несколько зависимостей, несколько реплик, несколько вызовов к БД. Тогда end-to-end latency сильно коррелирует с максимумом/квази-максимумом подзапросов.
В “The Tail at Scale” есть два очень показательных количественных фрагмента:
- Вероятность “service latency > 1s” растет с количеством серверов и частотой outlier’ов, и на графике отмечен кейс ~0.63 при fan-out на 100 серверов и outlier-частоте 1/100.
- Таблица “Individual-leaf-request finishing times” показывает, что если один leaf имеет 99-й перцентиль 10ms, то ожидание “100% leaf’ов завершились” дает уже 99-й перцентиль порядка 140ms – хвост усиливается эффектом “ждем всех”.

Рис. 2: таблица “Individual-leaf-request finishing times”
Это фундаментальная причина, почему “p99 базы 50ms, а p99 сервиса 800ms” может быть не багом мониторинга, а закономерностью архитектуры.
Небольшая математическая модель fan-out (с числами)
Если вероятность, что один подзапрос медленный (>T), равна q, а запрос делает N независимых подзапросов и ждет всех, то вероятность, что хотя бы один будет медленным:
Pslow=1-1-qN
Кейс из “The Tail at Scale” иллюстрирует q = 0.01 (1/100) и N – 100 – 1-0.99 * 100 * 0.63.
В проде независимость часто нарушается (общий диск, общий GC, общий сетевой инцидент), и тогда хвост еще хуже, чем модель независимых событий.
Как выглядит “распределение latency” в жизни: схематичный скетч
Ниже – не “данные”, а визуальная интуиция, почему среднее и p50 могут быть “зелеными”, а p99 – уже боль.
Latency (ms) Count
10- 20 | ##############################
20- 40 | ########################
40- 80 | #############
80-160 | ######
160-320 | ###
320-640 | ##
640-1280 | #
1280-2560 | #
У такого распределения p50 может быть ~20–30ms, p95 ~150–250ms, а p99 – уже секунды (в зависимости от “массы хвоста”). Это и есть предмет управления: tail, а не “среднее”.
Измерение и инструментализация
Где измерять latency: client, edge, server, dependency
Одна и та же “задержка запроса” существует в нескольких “точках”. Ошибка многих команд – выбрать одну (обычно server-side) и считать, что это “пользовательский опыт”.
Практичная декомпозиция:
- Client-observed latency: то, что видит клиент (DNS/TLS/сеть/ретраи/очереди на клиенте).
- Edge latency: ingress/API-gateway (TLS termination, routing, rate limit, retries).
- Server in-process latency: от входа в handler до отправки ответа.
- Dependency latency: DB/cache/outbound RPC, включая ожидание пула.
SLO/SLI методологически рекомендуется строить вокруг измерений, которые отражают качество сервиса для пользователя. В SRE-подходе SLI часто задается как отношение good events / total events, что хорошо ложится на error budget (100% – SLO).
SRE Workbook прямо рекомендует начинать с архитектурной схемы и выбирать SLI, которые релевантны и измеримы, а затем итеративно улучшать.
Практические рекомендации по выбору latency/SLI:
- Для внешнего API часто разумно иметь два SLI:
1) edge/client-latency как прокси пользовательского опыта,
2) server-latency как управляемая “внутренняя” метрика. - Для внутренних RPC полезно мерить и client-side (outbound) и server-side (inbound), чтобы видеть асимметрию сети/балансировки.
- Для dependency обязательно мерить ожидание пула отдельно, иначе вы будете “оптимизировать базу”, когда реальная проблема – “connection pool exhausted”.
В OpenTelemetry HTTP semantic conventions есть полезная привязка: http.server.request.duration и http.client.request.duration должны соответствовать длительности соответствующих спанов, когда метрика репортится рядом со спаном.
Также в HTTP-метриках встречаются отдельные измерения очередей (например, http.client.request.time_in_queue с рекомендуемыми бакетами), что помогает диагностировать именно pool/queue-задержки.
Как системы считают перцентили: подходы и компромиссы
Основные стратегии можно свести к четырем семействам:
- Логирование сырых значений и офлайн-агрегация (ClickHouse/аналитика).
- Streaming quantiles / Summary (квантили считаются на агенте/клиенте).
- Histogram (explicit buckets) (бакеты на клиенте, квантили на сервере мониторинга).
- Экспоненциальные / native histograms (динамические схемы, более компактное хранение).
Prometheus “Histograms and summaries” дает наиболее практичную рамку: summaries считают streaming квантильs на клиенте и отдают готовые значения, histograms отдают бакеты, а квантили вычисляются сервером через histogram_квантиль().
Ключевое следствие из этой же документации: precomputed квантильs у Summary в общем случае не агрегируются между инстансами, попытка “усреднить p99” статистически бессмысленна.
Сравнительная таблица подходов
| Подход | Что хранится | Точность p99/p999 | Агрегация между инстансами | Стоимость | Когда выбирать |
|---|---|---|---|---|---|
| Логи + офлайн | каждое значение latency | высокая (по данным) | да (через SQL/агрегаторы) | высокая по объему и I/O | готовые –квантили (на клиенте) |
| Summary / streaming quantiles | да, как first-class тип | потенциально высокая для одного процесса | обычно нет (не агрегируем) | CPU на клиенте выше | редкие случаи, когда агрегация не нужна |
| Histogram (explicit buckets) | бакеты _bucket + _sum/_count | ограничена шириной бакетов и интерполяцией | да (через суммирование бакетов) | больше time series | дефолт для SLO, дашбордов, алертов |
| ExponentialHistogram (OTel) | экспоненциальные бакеты (масштабируемые) | хорошая относительная точность на wide range (зависит от scale) | да (по модели данных OTel) | обычно дешевле при wide range | latency 1ms…10s+ без ручной настройки бакетов |
| Native histograms (Prometheus) | “структурный” тип гистограммы, динамические бакеты | выше, чем у грубых explicit buckets при схожей стоимости (в типичных кейсах) | требует включения scrape/remote-write опций | требует включения scrape/remote-write опций | когда упираетесь в стоимость классических бакетов или нужна лучшая точность |
Важно не перепутать: в OpenTelemetry “Summary” описан как legacy тип точки, тогда как Histogram и ExponentialHistogram – полноценные современные типы.
Ошибка оценивания percentiles: почему “p99 прыгает” и что с этим делать
Prometheus подробно объясняет, что квантиль из гистограммы – это оценка: истинный квантиль гарантированно лежит в пределах бакета, а одиночное число получается интерполяцией (по умолчанию линейной).
Из этого следуют два инженерных правила:
- бакеты должны быть плотнее вокруг ваших SLO-порогов (например, 200ms/300ms/500ms/1s), иначе p95/p99 могут выглядеть хуже/лучше реальности,
- если распределение “острое” (почти все запросы около одной точки), грубый бакет может давать большую визуальную ошибку (Prometheus приводит мысленные эксперименты, где оценка 95-го резко сдвигается из-за смены бакета).
Практические настройки: RED, семантика метрик, бакеты и low-cardinality
RED и Golden Signals
RED (Rate-Errors-Duration) – практический минимум для request-driven сервисов. Подход описан как микросервис-ориентированная философия мониторинга (Rate, Errors, Duration).
В SRE-подходе близкая рамка – “Four Golden Signals”: latency, traffic, errors, saturation.
Рекомендуемые bucket boundaries для HTTP/RPC
OpenTelemetry semantic conventions для http.server.request.duration и http.client.request.duration рекомендуют explicit бакеты (в секундах):
[0.005, 0.01, 0.025, 0.05, 0.075, 0.1, 0.25, 0.5, 0.75, 1, 2.5, 5, 7.5, 10].
Похожие рекомендации есть и для RPC-метрик.
Таблица (те же границы, но “человечески”):
| Граница (s) | Граница (ms) | Зачем полезно |
|---|---|---|
| 0.005 | 5 | “очень быстрые” cache-hit/локальные операции |
| 0.01 | 10 | быстрые запросы внутри DC |
| 0.025 | 25 | типичный p50 многих API при теплом кэше |
| 0.05 | 50 | “хороший UX” для интерактива |
| 0.075 | 75 | пограничные просадки |
| 0.1 | 100 | психологический порог “уже заметно” |
| 0.25 | 250 | частый SLO-таргет |
| 0.5 | 500 | еще ок для части сценариев |
| 0.75 | 750 | близко к 1s, важно для хвоста |
| 1 | 1000 | серьезная деградация интерактива |
| 2.5 | 2500 | “уже боль” и ретраи/таймауты рядом |
| 5 | 5000 | часто близко к таймаутам клиентов |
| 7.5 | 7500 | диагностика “залипаний” |
| 10 | 10000 | верхняя граница многих интерактивных бюджетов |
Если ваши реальные таймауты 2s, а бакеты идут до 10s – это нормально: верхние бакеты помогают увидеть приближение к таймаутам и “уход” в деградацию.
Low-cardinality labels: где чаще всего “убивают” мониторинг
OpenTelemetry прямо говорит: http.route должен быть low-cardinality, содержать статические сегменты пути, а динамические части – плейсхолдерами.
То же касается ошибок: error.type должен быть предсказуемым и низкокардинальным.
Это критично, потому что:
- histograms и так создают несколько time series на одну метрику,
- добавление “плохих” лейблов (например, url.path=/users/123456) умножает series и приводит к взрывному росту памяти/нагрузки на TSDB.
Полезная параллель из мира логов: Loki строится вокруг индексации лейблов, и выбор лейблов считается ключевым (каждый log stream должен иметь хотя бы один label, а значения должны быть осмысленными).
PromQL: как правильно считать p95/p99 из histogram
Prometheus показывает канонический шаблон: агрегировать бакеты, затем histogram_quantile.
Пример p99 по маршруту (схема):
histogram_quantile(
0.99,
sum by (le, http_route) (
rate(http_request_duration_seconds_bucket[5m])
)
)
Ключевая деталь – sum by (le, …): это сбор “общей гистограммы” из бакетов всех инстансов перед вычислением квантиля.
Также Prometheus подчеркивает: бакеты кумулятивны. Это важно, например, при вычислении долей запросов “быстрее порога” или Apdex-подобных метрик.
ExponentialHistogram и native histograms: когда они становятся “следующим шагом”
OpenTelemetry Metrics Data Model выделяет Histogram и ExponentialHistogram как разные типы точек метрик. ExponentialHistogram предназначен для экспоненциальных бакетов и поддерживает работу на широком диапазоне значений. Модель данных также описывает преобразования (в том числе уменьшение разрешения гистограмм и удаление атрибутов) как часть контроля стоимости сбора метрик.
В Prometheus native histograms идея “гистограмма как first-class sample type” доведена до уровня data model: вместо десятков float-серий (_bucket) появляется структурный тип с динамическими бакетами. Спецификация подчеркивает свойства: sparse representation (почти нулевая стоимость пустых бакетов), отсутствие настройки бакетов на этапе инструментирования, динамическое разрешение и экспоненциальные схемы, обеспечивающие mergeability.
Жизненно важные практические детали: native histograms стали stable в Prometheus v3.8.0, но скрейпинг нужно включать scrape_native_histograms, а remote-write – send_native_histograms.
Инструменты и пайплайн observability
Минимально “промышленный” пайплайн обычно выглядит так:
- приложение формирует signals (metrics/traces/logs) через SDK,
- коллектор принимает, процессит и экспортирует в бэкенды,
- метрики уходят в масштабируемое хранилище (Prometheus-совместимое),
- трейсы – в tracing backend,
- логи – в log backend,
- визуализация/алерты – в Grafana.
OpenTelemetry Collector описывается как исполняемый файл, который принимает телеметрию, обрабатывает и экспортирует в несколько целей, а сама обработка строится из pipeline: receivers/processors/exporters/connectors.
Для масштабного хранения метрик применяются системы класса Mimir (долговременное, горизонтально масштабируемое multi-tenant TSDB для Prometheus-метрик) или Thanos (глобальный query view + объектное хранилище для долгого retention).
Для трейсинга – Tempo (object storage как основа хранения, high-scale backend).
Для логов – Loki (индексирует метаданные labels, а сами логи хранит в чанках).
Для офлайн-аналитики логов/событий и ad-hoc расследований часто используются колоночные OLAP-СУБД вроде ClickHouse.
Jaeger остается широко используемым open-source tracing-решением. Документация отмечает происхождение и CNCF-статус.
Также для крупных установок нередко используется Prometheus Remote Write. Спецификация описывает протокол, а best practices предупреждают про рост памяти (часто порядка ~25% в типичных случаях) и влияние series на память из-за кэша label-значений для WAL.

Рис. 3: контейнерная диаграмма наблюдаемости
Расследование инцидентов: от p99 на графике к конкретному запросу
Почему без exemplars и tail sampling расследование хвоста часто “не сходится”
Метрики отвечают на вопрос “что происходит”: p99 вырос, RPS не упал, ошибки растут, saturations на грани. Но метрики почти никогда не отвечают на вопрос “почему именно эти запросы стали медленными”.
Для ускорения расследования экосистема предлагает exemplars: в OpenMetrics они определяются как ссылки на данные вне MetricSet (типичный пример – trace ID), и должны содержать LabelSet + value (и могут иметь timestamp).
Prometheus хранение exemplars включает через feature flag –enable-feature=exemplar-storage, и прямо связывает exemplars с OpenMetrics.
Grafana описывает exemplars как механизм, позволяющий находить “конкретные трейсы, показывающие высокую latency внутри выбранного интервала”, чтобы быстрее локализовать root cause.
Если вы включили exemplars, естественный workflow расследования становится намного короче: “вижу spike p99 – кликаю exemplar – открываю trace – вижу, где время ушло”. (Сама идея “trace linking” через exemplars опирается на OpenMetrics semantics).
Но даже при наличии трассировок другая частая проблема – sampling: если вы используете head-based sampling, то вы можете просто не сохранить медленные трейсы, потому что решение “сэмплировать или нет” принимается в начале. Tail sampling делает обратное: решение принимается после того, как система успела увидеть весь трейс и может применить политику (например, “сохранять все с ошибкой” или “сохранять медленнее X”).
OpenTelemetry tail_sampling processor формализует важное ограничение: все спаны одного trace_id должны попасть в один и тот же экземпляр коллектора, иначе корректного решения не будет.
Там же описаны типовые политики (latency/status_code/attribute/probabilistic/rate_limiting и др.) и ключевые параметры вроде decision_wait (по умолчанию 30s) и num_traces (по умолчанию 50000).

Рис. 4: диаграмма “p99 spike – exemplar – trace – причина”
Flowchart: практический алгоритм расследования tail latency

Рис.5: практический алгоритм расследования tail latency
Паттерны снижения p99 и защиты хвоста
Ниже – “не философия”, а причины, которые реально встречаются в проде, и практики, которые обычно дают ощутимое снижение p99/p999.
Таблица: типовые причины роста p99 и меры
| Причина хвоста | Как проявляется | Корневая механика | Что делать |
|---|---|---|---|
| Queueing / высокая утилизация | p50 норм, p99 растет, растет time_in_queue/pool wait | несколько слоев очередей усиливают вариативность | лимиты конкуренции (bulkhead), уменьшение фан-аута, запас мощности, измерять очереди явно |
| Head-of-line blocking | “тяжелые” запросы тормозят “легкие”, p99 по route растет | смешение классов работ в одном пуле, в paper предлагается дробить длинные запросы для интерливинга | раздельные пулы, приоритизация, time slicing, ограничение тяжелых эндпоинтов |
| GC / background activity / compaction | редкие “стены” latency, обычно коррелируют с CPU/IO | paper перечисляет garbage collection и фоновые операции как источники спайков | уменьшение аллокаций, контроль фоновых задач, изоляция, планирование (джиттер/батчи) |
| Noisy neighbor / shared resources | хвост появляется “волнами”, сложно воспроизвести | cgroup/квоты, выделенные ноды для latency-critical, I/O isolation, мониторинг iowait/throttling | Fan-out |
| Retries – каскады | p99 сервиса сильно хуже p99 зависимостей | вероятность медленного leaf растет с N, пример ~63% при ~100 leaf и 1/100 outlier | уменьшить N, кэшировать, early return “good enough”, дедлайны на leaf |
| положительная обратная связь: сбой – рост нагрузки – больше сбоев | рост RPS, рост latency, затем ошибки, “лавина” | положительная обратная связь: сбой – рост нагрузки – больше сбоев | retry budgets, “retry only one layer”, backoff/jitter, “don’t retry” ошибки |
| Overload / отсутствие backpressure | “ползучая смерть”: все медленно, но не падает | система пытается обработать все, накапливает очередь, p99 взлетает | load shedding, client-side throttling, квоты по клиентам |
Конкретные паттерны митигации, которые особенно эффективны против хвоста
Дедлайны и “бюджет времени” вместо “таймаута на глаз”
В больших fan-out системах критично иметь строгие дедлайны: leaf-операции должны завершаться в лимит, чтобы end-to-end оставался в бюджете. “The Tail at Scale” подчеркивает, что подоперации должны уложиться в строгий дедлайн, иначе сервис “не ощущается отзывчивым”.
Практически это означает: дедлайн должен быть в метаданных запроса и уменьшаться по мере прохождения слоев (edge – service – dependency).
Bulkheads и лимиты конкуренции как защита p99
Когда один класс запросов отъедает пул потоков/соединений, хвост начинает расти даже при неизменном среднем. В “The Tail at Scale” упоминаются идеи дифференциации service classes и higher-level queueing для сохранения интерактивности.
В SRE-подходе к handling overload показаны практики защиты задач через сигналы “нагрузки процесса” (например, рост активных потоков) и раннее отклонение запросов при превышении порога.
Retry budget и запрет “комбинаторного ретрая”
SRE book прямо описывает retry budgets: per-request (например, максимум 3 попытки) и per-client (например, пока доля ретраев < 10%), а также принцип: ретраи должны происходить только на слое непосредственно над слоем, который отказал, иначе возникает комбинаторный взрыв retries в глубокой зависимости.
Это не “теория”: это типичный механизм, превращающий деградацию в outage.
Hedged requests и tied requests
“The Tail at Scale” предлагает tail-tolerant техники: hedged requests (отправить второй запрос к реплике после небольшой задержки) и tied requests (две копии запроса с координацией отмены). В статье приведен впечатляющий пример: в одном benchmark чтения 1000 ключей, распределенных по 100 серверам, hedging после 10ms уменьшал 99.9-й перцентиль с 1800ms до 74ms при ~2% дополнительных запросов.
Такие техники требуют аккуратности: без приоритизации и контроля бюджета они могут “победить хвост” ценой перегрузки системы.
Load shedding и client-side throttling
Для защиты хвоста иногда правильнее “выкинуть часть запросов быстро”, чем “замедлить все запросы”. В SRE книге описана client-side throttling стратегия: если клиент видит, что значимая доля запросов отклоняется “out of quota”, он начинает сам ограничивать исходящий трафик, чтобы не жечь ресурсы бэкенда и сети.
Тот же раздел разбирает обработку overload-ошибок, правила ретраев и “overloaded, don’t retry” сигналы.
Чек-лист и FAQ
Чек-лист: что сделать в системе, чтобы p99 стал управляемым
- Выбрать “истины latency”: client/edge/server/dependency и зафиксировать, какой слой является SLI для SLO (часто – edge/client).
- Перейти на гистограммы там, где нужна агрегация по инстансам. Избегать “p99 per instance – avg”.
- Привести лейблы к low-cardinality (http.route как template, bounded error.type). Убрать high-cardinality ключи.
- Взять рекомендованные бакеты как baseline и затем уплотнить вокруг SLO-порогов. Помнить про ошибку интерполяции.
- Включить exemplars (OpenMetrics + exemplar storage) и trace linking в UI.
- Включить tail-based sampling (или усилить sampling для медленных/ошибочных) и убедиться, что все спаны одного trace_id попадают в один collector-инстанс.
- Ввести лимиты конкуренции и разделение классов нагрузки, измерять pool/queue wait (иначе вы будете лечить “не то”).
- Настроить retry budgets и запрет многоуровневого ретрая. Внедрить “don’t retry” ошибки для overload.
- Перейти от алертов “p99 > X” к SLO burnFFFFGGGGrate алертингу (multiwindow, multiFFFFGGGGburnFFFFGGGGrate).
Load testing и coordinated omission: почему “бенч показывает p99=50ms”, а прод нет
Coordinated omission – классическая ловушка измерения latency (термин активно продвигался Gil Tene в контексте “как не измерять latency”): измеряющая система “координируется” с измеряемой и пропускает часть хвоста, когда система замедляется.
Если генератор нагрузки работает как “closed loop” (отправил запрос – дождался ответа – отправил следующий), то при росте latency он уменьшает интенсивность, и хвост фиксируется хуже.
wrk2 сделан как constant throughput генератор с моделью измерения, которая учитывает “когда запрос должен был быть отправлен”, чтобы избегать coordinated omission. Это прямо описано в репозитории проекта.
HDRHistogram дает механизмы коррекции coordinated omission: в документации (например, JavaDoc) описан метод copyCorrectedForCoordinatedOmission и логика добавления “промежуточных” значений, когда наблюдаемая задержка превышает ожидаемый интервал между измерениями.
Аналогичные формулировки есть в документации для других реализаций (например, Rust), где описано “record … while correcting for coordinated omission” и принцип автогенерации дополнительных значений.
Exemplars: что нужно, чтобы “кликнуть p99 и открыть trace”
Минимальный набор условий:
- Метрика должна быть экспортирована в формате OpenMetrics с exemplars (семантика exemplars описана в OpenMetrics spec).
- В Prometheus нужно включить exemplar storage feature flag.
- UI должен уметь показывать exemplars и прыгать в tracing backend (это прямо описано в документации Grafana по exemplars).
- Для совместимости важно использовать принятые ключи trace_id и span_id (это фиксируется в OpenTelemetry compatibility документации по Prometheus/OpenMetrics).
Как выбрать p95 vs p99 vs p999
Практическая рамка выбора:
- p95: хороший индикатор “массового UX” и деградаций средней тяжести. Часто подходит как первый шаг для SLO в командах, где observability еще дозревает.
- p99: почти всегда нужен для интерактивных критичных операций (авторизация, платежи, поиск), потому что 1% на большом трафике превращается в постоянный поток плохих опытов. Tail-модель из “The Tail at Scale” показывает, насколько p99 системно усиливается fan-out и масштабом.
- p999: имеет смысл при очень большом трафике и зрелой системе наблюдаемости (иначе становится шумным и “дерганым”). Полезен, когда вы реально оптимизируете tail-tolerant техники (hedging/tied requests) и хотите отслеживать самые редкие outliers. “The Tail at Scale” оперирует 99.9-перцентилями в примерах про hedged requests, что показывает, что p999 – вполне рабочая цель при соответствующем масштабе.
Заключение
Перцентили (p50/p95/p99/p999) – это не “статистика для графиков”, а рабочий инструмент управления предсказуемостью системы. Среднее почти всегда врёт в проде: оно сглаживает редкие, но массово ощущаемые пользователем провалы. В распределённых сервисах хвост усиливается архитектурой (fan-out), очередями и конкуренцией за ресурсы: 1% медленных leaf-вызовов легко превращается в десятки процентов “медленных” end-to-end ответов при ожидании нескольких зависимостей.
Правильное измерение – это не “посчитать p99”, а выбрать где измерять (client/edge/server/dependency), чем мерить (в большинстве случаев – histogram, а не summary), и как делать расследование: метрики должны вести к конкретному trace через exemplars, иначе p99-инциденты превращаются в гадание.
Снижение p99 – это всегда работа с причинами хвоста, а не “оптимизация среднего”: контроль очередей и пулов, ограничения конкуренции (bulkheads), дедлайны и таймауты как единый бюджет, аккуратные ретраи с retry-budget, а при перегрузке – управляемая деградация и load shedding. Именно так вы покупаете стабильность: p99 становится “плоским”, а система – управляемой даже под нагрузкой.
![]()
You must be logged in to post a comment.