Модуль: Observability · Уровень: Middle+/Senior

TL;DR#

Три методологии «что мерить»: RED (Rate / Errors / Duration — для request-driven сервисов, Tom Wilkie), USE (Utilization / Saturation / Errors — для ресурсов: CPU, память, диск, пулы, очереди; Brendan Gregg) и Four Golden Signals (Latency / Traffic / Errors / Saturation — Google SRE). RED отвечает «как сервису», USE — «как ресурсу». Для латенси нельзя использовать average: среднее скрывает хвост, чувствительно к выбросам и не композируется. Нужны перцентили (p50/p99/p99.9) и понимание tail amplification (при fan-out на 100 бэкендов p99 каждого задевает почти каждый запрос). Перцентили считают через histogram (histogram_quantile по агрегированным бакетам), потому что готовые квантили нельзя усреднять между инстансами. И сквозная тема — labels/cardinality: лейблы только bounded, иначе взрыв рядов.

Теория#

Три методологии#

МетодДля чегоСигналыАвтор
REDRequest-driven сервисы (API, HTTP/gRPC)Rate, Errors, DurationTom Wilkie
USEРесурсы (CPU, RAM, диск, сеть, пулы, очереди)Utilization, Saturation, ErrorsBrendan Gregg
Golden SignalsЛюбой пользовательский сервисLatency, Traffic, Errors, SaturationGoogle SRE

Они дополняют друг друга: RED/Golden — взгляд снаружи (как пользователю), USE — изнутри (как ресурсу). На дашборде сервиса — RED, на дашборде инстанса/ноды — USE.

RED (на каждый endpoint/сервис):

# Rate — запросов в секунду
sum by (route) (rate(http_requests_total[$__rate_interval]))
# Errors — доля ошибок
sum by (route) (rate(http_requests_total{code=~"5.."}[5m]))
  / sum by (route) (rate(http_requests_total[5m]))
# Duration — p99 латенси
histogram_quantile(0.99, sum by (le, route) (rate(http_request_duration_seconds_bucket[5m])))

USE (на каждый ресурс):

  • Utilization — доля времени, что ресурс занят (CPU busy %, диск busy %).
  • Saturation — насколько ресурс перегружен сверх возможностей (длина run-queue, depth очереди, swap, ожидание пула).
  • Errors — счётчик ошибок ресурса (ECC, dropped packets, failed allocs).

Тонкость: высокая utilization без saturation — это нормально (ресурс эффективно используется). Опасна saturation — появилась очередь ожидания → латенси растёт нелинейно.

Почему НЕ average для латенси#

Average — главный антипаттерн измерения латенси:

  1. Скрывает хвост: 99 запросов по 10ms + 1 запрос 5s → mean ≈ 60ms, выглядит «нормально», но 1% пользователей ждут 5 секунд.
  2. Чувствителен к выбросам: один GC/таймаут сдвигает среднее.
  3. Не композируется: нельзя усреднить средние двух инстансов с разным трафиком и получить правду без весов.
  4. Бимодальность невидима: cache hit (1ms) + cache miss (200ms). Average = ~100ms — значение, которого не испытывает ни один реальный запрос. Перцентили/heatmap покажут два пика.

Вывод: латенси описывают распределением — перцентили + гистограмма (heatmap).

Перцентили и tail latency#

ПерцентильСмысл
p50 (median)Типичный опыт
p90/p95Большинство
p99Хвост, который замечают
p99.9Самые медленные, важны при высоком RPS и для critical paths

Tail amplification (fan-out) — почему p99 критичен: если запрос пользователя порождает обращения к 100 бэкендам и ждёт всех, вероятность задеть «p99 хотя бы одного» ≈ 1 - 0.99^100 ≈ 63%. То есть p99 отдельного бэкенда становится почти медианным опытом агрегированного запроса. Поэтому в распределённых системах хвост (p99/p99.9) важнее среднего на порядок.

histogram_quantile: механика и точность#

Histogram хранит кумулятивные бакеты (le = «≤ граница»). histogram_quantile(φ, buckets):

  1. Находит бакет, в который попадает φ-й перцентиль по счётчикам.
  2. Линейно интерполирует позицию внутри бакета.

Отсюда точность ограничена шириной бакета:

  • Если p99 попадает в бакет [1s, +Inf], результат интерполируется до бесконечности — мусор.
  • Слишком широкие бакеты → грубая оценка; слишком узкие и много → лишние ряды.

Как выбирать границы бакетов: вокруг ожидаемой латенси и обязательно вокруг порога SLO. Если SLO «99% < 300ms», нужен бакет ровно на le="0.3", чтобы точно считать долю быстрых запросов.

prometheus.HistogramOpts{
    Name:    "http_request_duration_seconds",
    Buckets: []float64{0.005, 0.01, 0.025, 0.05, 0.1, 0.25, 0.3, 0.5, 1, 2.5, 5},
    //                                                   ↑ граница SLO
}

Почему перцентили нельзя усреднять (histogram > summary)#

avg(p99_inst1, p99_inst2) математически бессмысленно: перцентиль — нелинейная функция распределения, его нельзя усреднить. Чтобы получить общий p99 по всем инстансам, нужны сырые бакеты (counters), которые складываются:

histogram_quantile(0.99, sum by (le) (rate(bucket[5m])))   # ✓ верно
avg(http_duration{quantile="0.99"})                         # ✗ summary, неверно

Поэтому histogram (сырьё бакетов, агрегируется на сервере) предпочтительнее summary (готовые per-instance квантили, не агрегируются) везде, где нужна агрегация по подам/инстансам.

Labels и cardinality#

Каждая уникальная комбинация лейблов = отдельный временной ряд. series = ∏ кардинальностей лейблов.

Хорошие лейблы (bounded)Антипаттерны (unbounded)
method, status codeuser_id, email
route-шаблон /orders/:idraw URL /orders/12345
region, datacenterrequest_id, trace_id, session_id
error_type (enum)произвольное сообщение об ошибке

Высококардинальные данные → в трейсы и логи, не в лейблы метрик. Бюджет рядов на сервис + ревью новых лейблов — обязательная senior-практика.

Подводные камни / gotchas#

  • Average latency лжёт — скрывает хвост и бимодальность; не использовать как основной показатель.
  • Усреднение перцентилей (avg готовых квантилей) математически неверно — нужны бакеты гистограммы.
  • histogram_quantile врёт при плохих бакетах — если перцентиль в последнем [X, +Inf] бакете, интерполяция до бесконечности; бакеты должны окружать SLO-порог и ожидаемую латенси.
  • p99 из p99 нельзя — нельзя взять p99 от per-instance p99; считай из агрегированных бакетов.
  • Cardinality взрыв от одного unbounded-лейбла кладёт TSDB.
  • Error rate = errors / total, а не отдельный «errors per second» без знаменателя — иначе рост трафика выглядит как рост ошибок.
  • Missing data ≠ zero: отсутствие точек (рестарт, нет трафика) PromQL трактует не как 0; алерт == 0 может молчать. Учитывай absent()/or vector(0) осознанно.
  • Counter, не gauge, для накопления: счётчик событий — counter (rate() корректно учтёт reset); gauge для «текущего значения». Перепутать = неверные графики.
  • Saturation важнее utilization для предсказания деградации: 100% CPU без очереди ок, а очередь при 70% — уже проблема.

Вопросы на собеседовании#

В: Когда RED, когда USE, когда Golden Signals? О: RED — для request-driven сервисов (Rate/Errors/Duration), взгляд снаружи как у пользователя. USE — для ресурсов (CPU/RAM/диск/пулы/очереди): Utilization/Saturation/Errors, взгляд изнутри. Golden Signals (Latency/Traffic/Errors/Saturation) — обобщение Google SRE для любого пользовательского сервиса. На дашборде сервиса — RED, на ноде/инстансе — USE; вместе они дают и пользовательский, и ресурсный взгляд.

В: Почему нельзя мерить латенси средним? О: Среднее скрывает хвост (1% по 5s утонут в массе быстрых), чувствительно к выбросам, не композируется и невидимо для бимодальных распределений (cache hit/miss) — mean попадает в «пустоту» между пиками, где нет реальных запросов. Нужны перцентили и heatmap, описывающие распределение.

В: Что такое tail amplification и почему p99 важнее, чем кажется? О: При fan-out, когда запрос ждёт N бэкендов, вероятность задеть «p99 хотя бы одного» = 1 - 0.99^N; при N=100 это ~63%. То есть p99 отдельного сервиса становится почти медианным опытом составного запроса. В распределённых системах хвост определяет пользовательскую латенси, поэтому оптимизируют p99/p99.9, а не среднее.

В: Почему histogram, а не summary, для распределённого сервиса? О: Summary считает квантили на клиенте, а их нельзя агрегировать между инстансами (усреднять p99 бессмысленно). Histogram отдаёт сырые кумулятивные бакеты (counters), которые складываются sum by (le), и только потом берётся histogram_quantile — так получается корректный общий перцентиль по всем подам. Цена — зависимость точности от бакетов.

В: Как выбрать границы бакетов гистограммы? О: Вокруг ожидаемой латенси и обязательно ровно на пороге SLO. Если SLO «99% < 300ms», нужен бакет le=0.3, чтобы точно считать долю быстрых запросов и p99. Иначе перцентиль попадёт в широкий бакет и histogram_quantile будет грубо интерполировать (особенно фатально в последнем +Inf бакете).

В: Чем utilization отличается от saturation и что опаснее? О: Utilization — доля времени, что ресурс занят; saturation — степень перегрузки сверх возможностей (длина очереди/run-queue/ожидание пула). Опаснее saturation: высокая utilization без очереди нормальна, а появление очереди означает нелинейный рост латенси. Поэтому saturation — ранний предиктор деградации.

В: Как правильно считать error rate? О: Как долю: rate(errors_total) / rate(requests_total). Абсолютный счётчик ошибок без знаменателя вводит в заблуждение — при росте трафика растут и абсолютные ошибки при неизменной доле. Доля нормализует на объём и сравнима во времени и между сервисами.

На что копают на senior+#

  • Histogram vs summary на уровне агрегируемости и почему перцентили не композируются.
  • Выбор бакетов под SLO и связь гистограммы с threshold-based SLI (доля запросов быстрее порога вместо перцентиля).
  • Tail amplification при fan-out и стратегии борьбы (hedged requests, backup requests, таймауты).
  • Cardinality budgeting: формула рядов, bounded-лейблы, вынос высококардинального в трейсы/логи, экземпляры (exemplars) для связи.
  • Композируемость метрик: что можно складывать (counters, бакеты), что нельзя (готовые квантили, средние без весов).
  • Высокое vs низкое разрешение: trade-off между гранулярностью (короткий scrape, больше бакетов) и стоимостью/кардинальностью.
  • Missing data semantics и алертинг устойчивый к отсутствию точек (staleness, absent).