Golden Signals deep dive: Latency, Traffic, Errors, Saturation. Order API tier with connection pool, Postgres + Kafka backends, Prometheus + Alertmanager + Grafana observability stack, on-call engineer. Three scenarios: latency p50 vs p99 spike with histogram_quantile PromQL, traffic drop + 5xx cascade correlation, saturation 80% predicting failure 30min before pool exhaustion. Includes ADR-001 on percentiles vs averages.
«Monitor the four golden signals» — фраза знакомая всем по Google SRE Book. Но в большинстве систем golden signals реализованы плохо: latency считается как avg, traffic снимается без cardinality control, errors не классифицируются, saturation сводится к «CPU usage». В итоге дашборд зелёный — а сервис уже лежит. На диаграмме показан минимальный pipeline (api → db/queue → Prometheus → Alertmanager → on-call) и три сценария, где каждый из signals раньше остальных ловит реальный инцидент.
Цель — научиться: какие метрики снимать, как формулировать PromQL, какие пороги ставить, и какие ошибки превращают мониторинг в декорацию.
Latency = «насколько быстро?», Traffic = «насколько активно?», Errors = «насколько правильно?», Saturation = «насколько близко к пределу?». Четыре независимых измерения health. Если все четыре в норме — система здорова. Если хотя бы одно деградирует — у тебя есть конкретное направление, куда копать, до того как пользователи начнут жаловаться.
Каждый signal — это семейство метрик, не одно число. latency без percentile-разбивки, errors без классификации по типу, saturation без application-level метрик — это карго-культ, а не мониторинг.
/metrics у api/db/queue, хранит histogram buckets.Edges: запросы users → api → db/queue. Параллельно — observability-плоскость: prom → api/db/queue (scrape), prom → am → oncall (alert), oncall → grafana → prom (investigate). Две плоскости работают одновременно и не должны мешать друг другу (метрики не должны деградировать вместе с сервисом — иначе мониторинг слепнет ровно когда нужен).
Latency: p50 vs p99 + spike. Базовая нагрузка 1000 RPS, p50=80ms, p99=180ms. Avg=95ms «выглядит нормально». Деплоится новый endpoint без индекса → seqscan 1.5s. Avg уезжает всего на +50ms (медленных запросов мало), а p99 прыгает 180ms → 1500ms. Alert OrderApiHighLatency: p99 > 1s for 5m фаерится, on-call видит в Grafana: avg почти не изменился, p99 вырос в 8×. Это и есть классический tail-проблем: avg маскирует то, что чувствуют 1% пользователей (и часто — самые ценные). Фикс: CREATE INDEX, p99 возвращается на 180ms.
PromQL для drill-down:
histogram_quantile(0.99,
sum by (route, le) (rate(http_request_duration_seconds_bucket[5m]))
)
Traffic vs Errors: drop + 5xx cascade. Сначала traffic падает 1000 → 50 RPS (CDN-провайдер лежит). Это «zero traffic» — самый коварный сигнал, потому что дашборд errors остаётся зелёным (errors считаются от total, а total = 0). Алерт OrderApiTrafficDrop: <50% of 1h ago ловит именно это. Потом CDN восстанавливается, traffic скачет 50 → 1500 RPS (накопленный спрос). DB не успевает, conn timeouts → 5xx jumps 0.01% → 5%. Второй алерт OrderApiHighErrorRate. On-call коррелирует: traffic spike + 5xx + DB latency = downstream saturation. Action — масштабировать read replicas + throttle на ingress.
Ключевой урок: drop in traffic — тоже сигнал. Не только spike. Часто это означает, что сервис не достижим, а errors молчат, потому что некому ошибаться.
Saturation 80% → предсказание отказа. Traffic растёт постепенно: 1000 → 2500 → 4000 → 5500 RPS. Pool: 60/100 → 80/100 → 100/100. На 80% — warn-уровень, тикет (не пейдж), but on-call уже знает: at this rate ~15 min до exhaustion. На 100% — pool пуст, новые requests timeout, 503. Latency и error-сигналы загораются после saturation. Это и есть смысл saturation как «leading indicator»: он предсказывает деградацию, а не констатирует её. Фикс: поднять pool.max + ещё одна реплика приложения, до пейджа.
Saturation на уровне OS (CPU 80%) обычно бесполезен — современные сервисы упираются в пулы, очереди, file descriptors, GC pauses, а не в CPU. Application-level метрики (pool_active/pool_max, queue_depth, worker_busy_ratio) ловят проблему за 10-30 минут до того, как пользователь увидит ошибку.
Контекст. Канонический пример: 99% запросов в 100ms, 1% в 5000ms. avg = 149ms — «всё ок». p99 = 5000ms — правда, которую чувствуют ваши самые активные клиенты. Avg также склеивает бимодальные распределения: success 50ms и error 3000ms смешиваются в одно бессмысленное число. При агрегации по endpoint'ам медленные uploads доминируют над быстрыми login'ами — глобальный avg становится мусором.
Решение. Всегда эмиттить Prometheus histograms с densely-spaced buckets вокруг SLO-таргета (например, 5ms..2s, 12+ buckets). Считать p50/p95/p99/p99.9 через histogram_quantile, с разбивкой по route И status (success-latency != error-latency — это разные распределения). Алертить только на percentile. Avg оставить на дашбордах для sanity-check, но никогда не использовать как SLO.
Последствия.
route × status × method × instance × 12 buckets легко даёт 100K+ series. Контролируется нормализацией route (/users/:id, не /users/12345), убиранием user_id из лейблов, top-N по тенантам.[0.1, 0.2, 0.5, 1, 2, 5], а реальный p99 = 0.7s — quantile-оценка размазана между 0.5 и 1.0, точность ±25%. Подбирайте buckets под expected range.histogram_quantile(0.99, sum(rate(bucket))) — правильно. quantile(0.99, label1) + quantile(0.99, label2) — математическая бессмыслица.Альтернатива (отвергнута). Summary вместо Histogram: считает percentiles на стороне клиента. Проще, но не суммируется по инстансам — нельзя получить cluster-wide p99. Подходит только для single-instance метрик (например, GC pause на конкретной ноде).
status (4xx/5xx), error_type, route.rate > 1% for 5m.user_id в лейблах → Prometheus OOM → мониторинг ослеп ровно когда нужен. Запомнить: каждый лейбл умножает series count.errors / success вместо errors / total — ratio inflated в моменты массового сбоя.Golden signals — это request-response mental model. Они напрямую не подходят для:
consumer lag (saturation в чистом виде); throughput и errors — вторичны. Golden signals здесь — пере-инжиниринг.И — golden signals не заменяют distributed tracing. Они отвечают «что сломалось», но не «почему именно этот запрос». Для root-cause нужен tracing (Jaeger / Tempo / Honeycomb) поверх метрик.
histogram_quantile gotchas.