Модуль: 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, иначе взрыв рядов.
Теория#
Три методологии#
| Метод | Для чего | Сигналы | Автор |
|---|---|---|---|
| RED | Request-driven сервисы (API, HTTP/gRPC) | Rate, Errors, Duration | Tom Wilkie |
| USE | Ресурсы (CPU, RAM, диск, сеть, пулы, очереди) | Utilization, Saturation, Errors | Brendan Gregg |
| Golden Signals | Любой пользовательский сервис | Latency, Traffic, Errors, Saturation | Google 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 — главный антипаттерн измерения латенси:
- Скрывает хвост: 99 запросов по 10ms + 1 запрос 5s → mean ≈ 60ms, выглядит «нормально», но 1% пользователей ждут 5 секунд.
- Чувствителен к выбросам: один GC/таймаут сдвигает среднее.
- Не композируется: нельзя усреднить средние двух инстансов с разным трафиком и получить правду без весов.
- Бимодальность невидима: 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):
- Находит бакет, в который попадает φ-й перцентиль по счётчикам.
- Линейно интерполирует позицию внутри бакета.
Отсюда точность ограничена шириной бакета:
- Если 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 code | user_id, email |
route-шаблон /orders/:id | raw URL /orders/12345 |
| region, datacenter | request_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).