Перейти к содержимому
ГоМастер

Поиск по урокам

Найдите урок, курс, термин или раздел

Урок 7 из 25 ~20 мин +20 XP

Histogram: латентность и bucket'ы

Гистограмма — главный тип метрик prometheus для латентности: histogram в go-сервисе хранит не среднее, а распределение наблюдений по бакетам, из которого PromQL потом достаёт p99. Разберём Observe, DefBuckets, свои границы сетки и ловушки квантилей, о которых больно узнают на дежурстве.

Содержание урока · 10 разделов

Prometheus histogram: go-пример с Observe#

Представьте два сервиса с одинаковой средней латентностью 120 мс. У первого всё ровно: минимум 80, максимум 180. У второго девять запросов из десяти укладываются в 40 мс, а десятый висит две секунды. Средняя одинаковая — опыт пользователей противоположный. Среднее скрывает форму распределения, а форма и есть главное: медиана показывает типового пользователя, p99 — самого несчастливого из ста, и именно его словами описывают SLO. Обычный Counter здесь бессилен: он умеет только инкременты и не помнит, сколько длился каждый запрос. Histogram решает задачу хитрее: приложение не хранит каждое наблюдение, а раскладывает значения по заранее заданным интервалам — бакетам — и отдаёт Prometheus накопленные счётчики по каждому интервалу. Квантиль считается уже из этих счётчиков, в момент запроса. Компромисс виден сразу: сырые значения теряются, точность ограничена границами бакетов — и границами придётся управлять, это половина урока.

Создаётся гистограмма функцией prometheus.NewHistogram с HistogramOpts, где кроме Name и Help задаётся Buckets — возрастающая сетка границ в секундах. Если её не указать, возьмётся prometheus.DefBuckets: одиннадцать значений от 5 мс до 10 секунд, сетка заточена под типичный веб-сервис. Наблюдение — один вызов hist.Observe(value); передавать принято секунды float64, поэтому у времени ответа пишут time.Since(start).Seconds(). Observe потокобезопасен: его можно звать из любого числа горутин без мьютекса, внутри атомарные операции. Паттерн в хендлере одинаковый всегда: запомнить время в начале, отдать ответ, наблюдать разницу — удобнее всего это делает middleware, про готовые обёртки promhttp будет девятый урок. Здесь пример держим ручным, чтобы была видна механика: замер — это две строки вокруг вашей бизнес-логики, всё остальное берёт на себя client_golang.

Гистограмма латентности с сеткой по умолчанию

GO
package main

import (
        "net/http"
        "time"

        "github.com/prometheus/client_golang/prometheus"
        "github.com/prometheus/client_golang/prometheus/promhttp"
)

var requestDuration = prometheus.NewHistogram(
        prometheus.HistogramOpts{
                Name:    "http_request_duration_seconds",
                Help:    "Время обработки запроса",
                Buckets: prometheus.DefBuckets,
        },
)

func handler(w http.ResponseWriter, r *http.Request) {
        start := time.Now()
        defer func() {
                requestDuration.Observe(time.Since(start).Seconds())
        }()
        w.Write([]byte("ok"))
}

func main() {
        prometheus.MustRegister(requestDuration)
        http.HandleFunc("/api", handler)
        http.Handle("/metrics", promhttp.Handler())
        http.ListenAndServe(":8080", nil)
}

Buckets: prometheus.DefBuckets — сетка по умолчанию; её можно не указывать вовсе, но в коде её обычно пишут явно, чтобы читатель видел, что меряется. Observe в defer гарантирует замер и при панике в хендлере: латентность неудачных ответов тоже попадёт в распределение. Суффикс _seconds в имени — конвенция: единицу стоит называть в имени, иначе PromQL-запросы читать тяжело.

DefBuckets и свои сетки#

DefBuckets — это {0.005, 0.01, 0.025, 0.05, 0.1, 0.25, 0.5, 1, 2.5, 5, 10} секунд. Обратите внимание на пропорцию: сетка логарифмическая, каждый шаг в полтора-два с половиной раза шире предыдущего, потому что латентности живут на порядках. Но она полезна только сервису, у которого время ответа реально лежит от десятков миллисекунд до секунд. Быстрому API, где всё заканчивается за 40 мс, девять из одиннадцати бакетов не пригодятся, а p99 посчитается грубо. Медленной генерации отчётов, наоборот, нужна сетка выше десяти секунд. Свои границы задаются слайсом float64 по возрастанию — либо хелперами: prometheus.LinearBuckets(start, width, count) даёт равномерную сетку, prometheus.ExponentialBuckets(start, factor, count) — экспоненциальную. Учтите: границы зашиты в релиз. Поменять сетку на живом дашборде нельзя, а после смены графики получат разрыв, потому что счётчики стартуют заново. Планируйте сетку под свой SLO, а не копируйте дефолт по привычке.

Инцидент: гистограмма в миллисекундах#

Инцидент, который стоит знать до первого дашборда с p99. В одном сервисе замер писали в миллисекундах: Observe(float64(elapsed.Milliseconds())) при сетке DefBuckets от пяти миллисекунд до десяти секунд. Почти все наблюдения попадали в первый бакет: p99 показывал стабильные пять миллисекунд, график был образцово зелёным, а пользователи жаловались на медленный поиск. Ошибка интерпретации в обратную сторону: график честен — врать стали единицы. Диагностика заняла минуты, когда кто-то посмотрел на сырые бакеты: le=0.005 равнялся le=+Inf, значит, весь трафик быстрее первой границы — либо сервис летает, либо мера не в тех единицах. Противоядие: секунды и time.Since(start).Seconds() по умолчанию; если единица другая — она обязана быть в имени метрики. Проверку le=+Inf равно _count вы уже знаете — она же проверка здравого смысла, и этой истории она бы хватило.

Сетка под свой SLO

GO
var apiDuration = prometheus.NewHistogram(
        prometheus.HistogramOpts{
                Name:    "api_duration_seconds",
                Help:    "Время ответа API",
                Buckets: []float64{0.01, 0.025, 0.05, 0.1, 0.25, 0.5, 1, 2.5},
        },
)

// очередь может ждать и секунды, и минуты — сетка экспоненциальная
var queueWait = prometheus.NewHistogram(
        prometheus.HistogramOpts{
                Name:    "queue_wait_seconds",
                Help:    "Ожидание в очереди",
                Buckets: prometheus.ExponentialBuckets(0.1, 2, 8),
        },
)

Первая сетка построена под SLO «99 процентов быстрее 300 мс»: границы сгущаются вокруг ожидаемого p95 и p99, поэтому квантиль будет точным именно там, где принимаются решения. ExponentialBuckets(0.1, 2, 8) строит 0.1, 0.2, 0.4 и так далее — удобно для очередей, где время ожидания живёт на порядках, от миллисекунд до минут.

Латентность метрики: что уезжает в /metrics#

Гистограмма экспортирует три семейства серий. Первое — _bucket с лейблом le: для каждой границы — накопленное число наблюдений, которые были меньше либо равны ей, включая последний псевдобакет le="+Inf", равный общему числу наблюдений. Кумулятивность важна: это не «сколько попало в интервал», а «сколько не превысило границу с начала процесса»; разность соседних бакетов даёт интервал. Второе — _sum: сумма всех наблюдений. Третье — _count: их количество. Средняя латентность считается делением sum на count без всяких квантилей, а p99 — функцией histogram_quantile в PromQL: она находит бакет, в котором лежит квантиль, и интерполирует внутри него. Всё это считается на стороне сервера Prometheus в момент запроса. Следствие приятное: можно менять запросы, срезать по любым лейблам и агрегировать инстансы, не трогая ни строчки Go-кода — к этому свойству мы ещё вернёмся в следующем уроке, потому что summary ведёт себя ровно наоборот.

Экспозиция гистограммы

GO
api_duration_seconds_bucket{le="0.05"} 318
api_duration_seconds_bucket{le="0.1"} 388
api_duration_seconds_bucket{le="0.25"} 397
api_duration_seconds_bucket{le="+Inf"} 400
api_duration_seconds_sum 6.14
api_duration_seconds_count 400

Четыреста запросов: 318 уложились в 50 мс, ещё 70 — между 50 и 100 мс (388 минус 318), между 100 и 250 мс — девять. Бакет +Inf равен count — встроенная проверка целостности. Средняя: sum на count — 6.14 секунды на 400 запросов, около 15 мс на запрос. Все три семейства серий начинаются с одного имени метрики.

Чем строить сетку бакетов#

СеткаКак задаётсяКогда брать
DefBucketsне указывать Buckets вовсетипичный веб-сервис с латентностью от миллисекунд до секунд
[]float64{...}возрастающий слайс границсвои границы вокруг SLO и реального хвоста распределения
LinearBuckets(start, width, count)равномерный шагвеличины с узким диапазоном: размеры ответов, числа элементов
ExponentialBuckets(start, factor, count)экспоненциальный ростразброс на порядки: очереди, ретраи, тяжёлые файлы

Спорное мнение: одиннадцати бакетов хватает#

Спорное мнение с границей: в большинстве сервисов сетку не надо трогать. Дефолтные одиннадцать границ критикуют за грубость, забывая, зачем квантиль вообще существует: p99 нужен не для физики, а для решений — сравнить релизы, поймать деградацию, проверить цель по надёжности. Для этих задач важно, чтобы граница лежала рядом с целевым значением, а не чтобы квантиль сходился до четвёртого знака. Я сужаю сетку в двух случаях: быстрый внутренний API, где весь трафик живёт в первом бакете, и фоновые задачи с хвостом за десять секунд. И не сужаю «на всякий случай»: тридцать границ утраивают объём серий, а выигрыш достаётся интерполяции, которой никто не доверяет. Граница спора проста: если у вас есть цель в миллисекундах и алерт по ней — сетка под неё; если есть только любопытство — оставьте дефолт и потратьте время на что-то другое.

Вариация: гистограмма размеров, а не времени#

Вариация примера, расширяющая применение: гистограмма не обязана мерить секунды. Размер ответа в байтах — тот же Observe с сеткой LinearBuckets от тысячи до шестидесяти четырёх тысяч: из неё получаются медианный размер страницы и хвост тяжёлых выгрузок, которые раздувают трафик. Число элементов в пачке — ExponentialBuckets от единицы: видно, как часто очередь таскает по одной задаче вместо сотни. Даже возраст данных в кэше влезает в распределение. Правило единственное: единица измерения — в имени метрики, а сетка — по реальному диапазону величины, который проще всего снять с продовых данных за неделю. После этого распределения вроде размерных перестают быть экзотикой и становятся обычным инструментом наравне с латентностью.

Проверка: кумулятивность

Что именно хранит серия _bucket{le="0.5"}?

Проверка: почему p99 приближённый

Почему p99 по гистограмме — всегда приближение?

Средняя из гистограммы

Прочитайте код и выберите, что он выведет:

api_duration_seconds_sum 6.14
api_duration_seconds_count 400

Сколько в интервале?

Прочитайте код и выберите, что он выведет:

api_duration_seconds_bucket{le="0.1"} 388
api_duration_seconds_bucket{le="0.25"} 397
api_duration_seconds_bucket{le="+Inf"} 400

Соберите гистограмму

Допишите создание гистограммы с сеткой по умолчанию и замер латентности.

var requestDuration = prometheus.( prometheus.HistogramOpts{ Name: "http_request_duration_seconds", Help: "Время ответа", Buckets: prometheus., }, ) start := time.Now() defer func() { requestDuration.(time.Since(start).Seconds()) }()

Замер вокруг бизнес-операции

Расставьте строки замера латентности: от старта таймера до Observe.

Строки

Ваша программа

Нажимайте на строки слева, чтобы собрать программу

Карточки: гистограммы

Термин

Histogram

нажмите, чтобы перевернуть
Определение

Метрика-распределение: Observe раскладывает значения по кумулятивным бакетам. Экспортирует _bucket, _sum и _count; квантиль считается на стороне Prometheus.

1 / 6

Гистограмма и лейблы из предыдущего урока работают в паре: гистограмма с лейблом route даёт p99 по каждому маршруту отдельно — типовой дашборд API начинается именно с этого. В следующем уроке сравним гистограмму с Summary — вторым типом для распределений, который считает квантили на стороне приложения, и поймём, почему для HTTP почти всегда берут histogram. Запросы к метрикам этого урока — rate, histogram_quantile, sum by (le) — разобраны в уроке про PromQL, там же появятся готовые формулы латентности. Полная программа — на странице курса Prometheus. Как и раньше, песочница сайта client_golang не исполняет: нужен локальный Go и go get github.com/prometheus/client_golang/prometheus. Домашняя работа урока как раз про это — и про чтение экспозиции руками, которое заменяет половину дашбордов.

Песочница урока

Редактируйте код и запускайте его прямо здесь — интерпретатор работает в браузере, ничего устанавливать не нужно. За каждый запуск — +2 XP.

Вывод

Запустите код, чтобы увидеть вывод.

Коротко: что запомнить

  • Histogram раскладывает наблюдения по кумулятивным бакетам; квантиль считается позже, на стороне Prometheus.
  • Observe принимает секунды float64: time.Since(start).Seconds(); потокобезопасен из любого числа горутин.
  • DefBuckets — 11 границ от 5 мс до 10 с; под быстрый API или медленные отчёты сетку задают своей.
  • _sum и _count дают среднюю без квантилей; _bucket плюс histogram_quantile дают p99.
  • Квантиль не точнее ближайшей границы бакета: границы ставят вокруг целевого SLO.
  • Бакеты умножают серии: 11 границ плюс лейблы — считайте кардинальность заранее.

Домашнее задание

Заведите в учебном сервисе гистограмму с сеткой под ваш целевой SLO, снимите /metrics curl-ом и посчитайте руками: среднюю из sum и count, число наблюдений в одном интервале разностью бакетов. Затем определите, между какими границами лежит ваш p99, и оцените, насколько грубым будет его приближение. Если поднят Prometheus — проверьте себя запросом histogram_quantile. Нужен локальный Go: песочница client_golang не исполняет.

Личные заметкиПусто
Хранятся только в вашем браузере (Prometheus · Histogram: латентность и bucket'ы)

Вопросы и ответы по уроку

Почему Observe принимает секунды, а не миллисекунды?

Такова конвенция client_golang: DefBuckets задан в секундах, и смешивать единицы в одном графе мониторинга опасно. Если нужны миллисекунды, назовите метрику с суффиксом _milliseconds и постройте свою сетку — но сначала попробуйте просто секунды.

Можно ли менять сетку бакетов без релиза?

Нет: границы зашиты в HistogramOpts и меняются вместе с кодом. После смены сетки счётчики стартуют с нуля, и графики получат разрыв — поэтому сетку планируют под SLO заранее и меняют осознанно.

Гистограмма или summary для латентности HTTP?

По умолчанию гистограмма: её бакеты агрегируются по инстансам и любым лейблам, p99 по ферме считается запросом. Summary берут за точный квантиль одиночного процесса — полный разбор в следующем уроке.

Термины урока

Ключевые понятия из этого урока — короткие определения в глоссарии. Наведите на термин, чтобы вспомнить, откройте карточку для подробностей и ссылки на практику.

Следующая ступень учебного пути

WebSocket25 уроков~13 чпрактика25 уроков уже доступны

WebSocket в Go: real-time чаты и уведомления

Курс WebSocket на Go: gorilla/websocket, эхо-сервер, hub-паттерн, комнаты, авторизация соединений, ping/pong, reconnect и масштабирование. 25 уроков.

  • Поднимать WS-сервер и апгрейдить HTTP-соединения
  • Писать readPump/writePump без гонок на каждое соединение
  • Строить hub: регистрацию, рассылку и комнаты
  • Авторизовывать соединения и ограничивать сообщения