Содержание урока · 11 разделов
- Prometheus gauge в Go: Set, Inc и Dec
- Инцидент: очередь, которая никогда не пустела
- Gauge метрика пример: что мерят датчиками на практике
- Counter против Gauge: сводная таблица
- Почему растущее нельзя мерить gauge-ом
- Яма после рестарта: как не читать график датчика
- Гибрид: события и сверка в одном датчике
- Скрейп-интервал и потерянные пики
- Песочница урока
- Коротко: что запомнить
- Вопросы и ответы
Prometheus gauge в Go: Set, Inc и Dec#
После счётчиков мир выглядит неудобно: не всё в системе растёт. Очередь задач то пустеет, то переполняется; память то освобождается, то кончается; пользователи заходят и выходят. Для таких величин у Prometheus есть Gauge — датчик текущего значения. Он обязан быть скучным на фоне драмы с рестартами счётчиков: gauge просто хранит число, которое вы записали последним, и отдаёт его при каждом скрейпе. Сложность gauge не в механике, а в дисциплине: нужно вовремя писать актуальное значение и не пытаться мерить им то, что только растёт. В уроке — NewGauge и все методы, два живых примера: очередь, которая обновляется событиями, и число задач из базы, которое синхронизируется таймером.
Создание зеркально счётчику: prometheus.NewGauge со структурой GaugeOpts из Name и Help. Name — snake_case без суффикса _total: он зарезервирован за счётчиками, и его отсутствие на датчиках — часть конвенции. Регистрация всё та же prometheus.MustRegister, один раз при старте. Дальше четыре метода, и у каждого своя семантика. Set записывает абсолютное значение — это главный метод gauge, им вы говорите «сейчас значение равно вот этому». Inc и Dec — плюс и минус единица для событий: задачу взяли, задачу отдали. Add и Sub меняют значение на произвольную величину с любым знаком: в отличие от счётчика убывание здесь законный основной сценарий. Все методы потокобезопасны — под нагрузкой из горутин значение не порвётся, хотя логику «кто и когда обновляет» библиотека за вас не придумает.
Очередь заказов: gauge, который живёт событиями
GOpackage main
import (
"net/http"
"github.com/prometheus/client_golang/prometheus"
"github.com/prometheus/client_golang/prometheus/promhttp"
)
var queueSize = prometheus.NewGauge(prometheus.GaugeOpts{
Name: "shop_queue_size",
Help: "Текущая длина очереди заказов.",
})
func init() {
prometheus.MustRegister(queueSize)
}
func main() {
http.HandleFunc("/enqueue", func(w http.ResponseWriter, r *http.Request) {
queueSize.Inc()
w.Write([]byte("добавлено"))
})
http.HandleFunc("/done", func(w http.ResponseWriter, r *http.Request) {
queueSize.Dec()
w.Write([]byte("обработано"))
})
http.Handle("/metrics", promhttp.Handler())
http.ListenAndServe(":8080", nil)
}Очередь — идеальный житель gauge: каждый новый заказ делает Inc, каждый обработанный — Dec, а /metrics в любой момент показывает актуальную длину. Имя без _total — конвенция датчиков. Модель честная: значение всегда соответствует состоянию процесса, потому что обновляется в момент события.
Событийная модель работает, пока события в ваших руках. Для величин, которые считает не процесс, а внешний мир — число строк в таблице, размер каталога, температура в серверной, — есть второй паттерн: синхронизация. Значение периодически читается из источника правды и записывается Set-ом, а gauge остаётся витриной последнего прочитанного. Так меряют всё, до чего хендлеры не дотягиваются: глубину отстающей очереди, размер кэша в объектах, число соединений, которыми управляет отдельный компонент. Смотрите на код ниже — он станет шаблоном для половины ваших датчиков.
Синхронизация датчика из источника правды
GOvar pendingJobs = prometheus.NewGauge(prometheus.GaugeOpts{
Name: "worker_pending_jobs",
Help: "Задачи, ожидающие обработки, по данным базы.",
})
func watchPendingJobs() {
ticker := time.NewTicker(10 * time.Second)
for range ticker.C {
n := countPendingJobs() // SELECT count(*) ...
pendingJobs.Set(float64(n))
}
}
go watchPendingJobs()Фоновая горутина раз в десять секунд читает актуальное значение и пишет его Set-ом — это и есть синхронизация датчика. Между обновлениями gauge хранит прошлое значение, и скрейп поймает именно его: разрешение метрики задаёт ваш таймер, а не только scrape-интервал. Слишком частые тяжёлые запросы к базе — самоповреждение, подберите интервал по цене запроса.
Инцидент: очередь, которая никогда не пустела#
Инцидент, после которого в нашей команде появилось правило «проверяй ноль». В обработчике платежей датчик shop_payments_pending инкрементился на входе и уменьшался на выходе — всё сходилось, пока не появился ранний выход: при невалидной карте функция возвращалась до Dec, и значение навсегда оставалось завышенным на единицу. За три месяца таких выходов набралось сорок: график «в очереди сорок платежей» превратился в константу, на которую никто не смотрел, а настоящий застой очереди заметили по жалобам, а не по метрике. Ловушка в том, что gauge честен ровно настолько, насколько симметричны пары Inc и Dec. Противоядия три: уменьшение в defer, как учит урок про отложенные вызовы; сборка значения из источника правды Set-ом вместо парных вызовов; периодическая сверка с базой. С третьим способом график иногда рисует ступеньку вниз — это не баг, это правда о том, что датчик раньше врал.
Gauge метрика пример: что мерят датчиками на практике#
Разберём типовую коллекцию. Встроенные метрики, которые вы видели в третьем уроке, по большей части — gauge: go_goroutines — число горутин сейчас, go_memstats_heap_inuse_bytes — занятая куча, process_open_fds — открытые дескрипторы. Это подсказка: если метрика отвечает на вопрос «сколько сейчас», в вашем коде, вероятно, тоже нужен gauge. Собственные датчики типового веб-сервиса: in-flight запросы — сколько обрабатывается прямо сейчас; размер кэша в объектах и в байтах; число активных сессий или WebSocket-соединений — для real-time у нас есть отдельный курс; глубина пула соединений к базе; заполненность дисковой партиции в процентах. Общий признак: значение имеет смысл только в моменте, а ноль — не пустая история, а честное «пусто сейчас».
Counter против Gauge: сводная таблица#
| Свойство | Counter | Gauge |
|---|---|---|
| Закон изменения | только вверх | в обе стороны |
| Рестарт процесса | обнуляется, rate склеивает сбросы | начинается с первого Set или с нуля |
| Ключевые методы | Inc, Add с неотрицательным | Set, Inc, Dec, Add, Sub |
| Типичные примеры | запросы, ошибки, байты, деньги | очередь, память, горутины, сессии |
| PromQL на дашбордах | rate() и производные | сырое значение, avg_over_time |
Почему растущее нельзя мерить gauge-ом#
Соблазн велик: «заказов всего» растёт, значит поставлю gauge и буду Set-ить накопленное. График будет выглядеть правдоподобно ровно до первого вопроса о скорости: у gauge нет понятия сброса при рестарте, поэтому после деплоя линия резко упадёт с сорока тысяч до пяти и так же резко ляжет в новый рост — график честно нарисует гору, которая ничего не означает. rate по gauge не считают: этот запрос рассчитан на монотонность счётчика, и алерты по скорости тоже не построить. Растущее значение — counter, и это не вкусовщина: тип определяет, на какие вопросы метрика умеет отвечать. Обратная ошибка мягче: убывающая величина в counter либо паникует при попытке уменьшения, либо заводится как разность двух счётчиков на уровне запросов — работает, но зачем, если есть gauge.
На одном проекте команду месяц вводил в заблуждение график «всего пользователей» в gauge: после каждого деплоя линия падала почти в ноль, и казалось, что сервис теряет аудиторию. Причина банальна: значение копилось в памяти, рестарт его стирал, а Set при старте никто не поставил. Два лекарства: накопления мерять counter-ом, а для честных датчиков сразу при старте выставлять актуальное значение из источника правды — тогда перезапуск не рисует ямы, которых не было.
Яма после рестарта: как не читать график датчика#
Типичная ошибка интерпретации датчика начинается после деплоя. График активных сессий падает почти в ноль и снова растёт: половина команды читает «пользователи ушли и вернулись», хотя правдивое чтение — процесс перезапустился, память очистилась, а Set при старте никто не поставил. Отличить одно от другого просто: след рестарта совпадает с моментом выката на графике выкладок, а поведение пользователей так синхронно не ходит. Второе чтение той же ямы — «сервис потерял данные»: если датчик отвечает за очередь, яма выглядит как «очередь разобрана», и все радуются, хотя очередь просто перестала быть видимой. Лечится двумя строками: при старте выставлять актуальное значение из источника правды и держать в дашборде вторую серию — время жизни процесса. Когда обе линии прыгают одновременно, вы смотрите на рестарт, а не на бизнес; лечится это не перезапуском, а инициализацией.
Гибрид: события и сверка в одном датчике#
Вариация примера, которую я использую чаще всего: гибрид двух моделей. События двигают датчик Inc и Dec для мгновенной реакции, а фоновая сверка раз в минуту выставляет Set из источника правды, исправляя накопленный дрейф: пропущенный Dec в редкой ветке перестаёт быть вечной ошибкой и превращается в минутную. Спорная сторона гибрида — двойная логика: читатель метрики должен знать, что значение может прыгнуть на сверке, и не искать в этот момент аварию. Моё правило: для короткоживущих сущностей с чёткими входами и выходами хватит чистых Inc и Dec; для долгоживущих состояний с редкими нештатными ветками гибрид честнее. А если источник правды сам дешевле события — например, длина очереди уже хранится в Redis — берите чистую синхронизацию и не поддерживайте два механизма одновременно.
Скрейп-интервал и потерянные пики#
Gauge отдаёт значение в момент опроса: если пик длился две секунды между скрейпами по пятнадцать — Prometheus его никогда не увидит. Для большинства метрик это нормально: очередь длиной десять тысяч, а через две секунды пустая, — событие, а не состояние, и для событий есть счётчики и логи. Но знать про слепые окна надо, иначе вы будете искать «пик, который точно был» по графику, где его физически нет. Вторая особенность — усреднения: avg_over_time по датчику показывает среднее по точкам скрейпа, и результат зависит от интервала. Спорное мнение из практики: in-flight запросы я предпочитаю мерить gauge-ом в middleware, а не вычислять из счётчиков разностью — датчик честен в моменте и не требует бережного обращения с рестартами.
Прежде чем заводить датчик руками, посмотрите на /metrics: горутины, память кучи, открытые дескрипторы — это уже gauge из коробки, и урок про встроенные метрики процесса разбирает их поимённо. Свой gauge оправдан, когда величина ваша по смыслу: длина бизнес-очереди, активные сессии, размер кэша. А для утечек горутин пригодится и теория — как деградирует профиль горутин и где искать виновника в продвинутом курсе по Go.
Проверка: Set против Inc
Чем Set принципиально отличается от Inc?
Проверка: gauge после рестарта
Сервис с датчиком «активные сессии» перезапустили. Что покажет метрика до первого Set?
Какое значение увидит скрейп
Прочитайте код и выберите, что он выведет:
queue.Set(42)
queue.Inc()
queue.Add(3)
queue.Dec()
queue.Set(10)Соберите датчик очереди
Восстановите создание gauge и два способа обновления: абсолютным значением и уменьшением на единицу.
Объявление и первое обновление датчика
Расставьте строки в правильном порядке: от создания gauge до первого обновления.
Строки
Ваша программа
Нажимайте на строки слева, чтобы собрать программу
Карточки: Gauge
Датчики закрывают вторую половину бытовых метрик — дальше метрики станут многомерными. Следующий урок — лейблы: одна метрика shop_orders_total и бесконечность срезов по маршрутам и статусам через CounterVec; там же первая встреча с кардинальностью, главным тихим убийцей Prometheus. После лейблов — гистограмма и её бакеты для латентности. Перечитайте перед практикой урок про счётчик: пара counter плюс gauge закрывает девять задач из десяти, и путаница между ними — самый частый вопрос на ревью. Практика по сегодняшнему материалу — только локальный Go: песочница сайта не поднимает HTTP-сервер. Карта всех двадцати пяти тем, включая будущие PromQL, Grafana и итоговый проект, — на странице курса.
Песочница урока
Редактируйте код и запускайте его прямо здесь — интерпретатор работает в браузере, ничего устанавливать не нужно. За каждый запуск — +2 XP.
Запустите код, чтобы увидеть вывод.
Коротко: что запомнить
- Gauge — текущее состояние: Set, Inc, Dec, Add и Sub с любым знаком, потокобезопасно, значение — последнее записанное.
- NewGauge с GaugeOpts без _total; MustRegister при старте; суффикс _total зарезервирован за счётчиками.
- Два паттерна обновления: события — Inc и Dec в коде, синхронизация — Set из источника правды по таймеру.
- Скрейп ловит момент: пики между опросами теряются, усреднения зависят от интервала.
- Растущее нельзя мерить gauge-ом: нет сбросов и rate — график рисует ложные ямы и горы после рестартов.
- Готовые датчики уже в /metrics: go_goroutines, память кучи, дескрипторы; свои — только для вашего домена.
Домашнее задание
Соберите сервис с очередью из урока: нагоните Inc-ов, потом Dec-ов и посмотрите, как значение ходит туда-сюда на /metrics. Затем добавьте второй датчик worker_pending_jobs с синхронизацией из источника правды и попробуйте «сломать» график: ставьте Set-ом растущую сумму и сравните картину с counter-версией из прошлого урока. Песочница не подойдёт — нужен локальный Go.
Личные заметкиПусто
Вопросы и ответы по уроку
Чем Gauge отличается от Counter в одном предложении?
Counter копит и только растёт, gauge показывает, сколько сейчас, и ходит в обе стороны. Растущее — счётчик, текущее состояние — датчик; перепутанные типы дают бессмысленные графики.
Как часто обновлять gauge из базы?
По цене запроса: тяжёлый подсчёт раз в десять секунд — нормально, раз в секунду — самоповреждение. Между обновлениями Prometheus видит прошлое значение: разрешение метрики ограничено и таймером, и scrape-интервалом.
Нужно ли защищать gauge от гонок?
Методы потокобезопасны: значение не порвётся под конкурентными Set и Inc. Гонки вашей логики библиотека не ловит: две горутины, считающие «актуальное» значение из разных источников, — уже ваша ошибка согласованности.
Термины урока
Ключевые понятия из этого урока — короткие определения в глоссарии. Наведите на термин, чтобы вспомнить, откройте карточку для подробностей и ссылки на практику.
Изучайте дальше
- Лейблы: срезы по маршрутам и статусам
- Histogram: латентность и bucket'ы
- Summary против Histogram
- Метрики HTTP: InstrumentHandler
- Counter: считаем то, что только растёт
- client_golang: первый /metrics endpoint
- Четыре типа метрик: Counter, Gauge, Histogram, Summary
- Знакомство: почему сервис молчит, а падает
- Все курсы по Go
Следующая ступень учебного пути
WebSocket в Go: real-time чаты и уведомления
Курс WebSocket на Go: gorilla/websocket, эхо-сервер, hub-паттерн, комнаты, авторизация соединений, ping/pong, reconnect и масштабирование. 25 уроков.
- Поднимать WS-сервер и апгрейдить HTTP-соединения
- Писать readPump/writePump без гонок на каждое соединение
- Строить hub: регистрацию, рассылку и комнаты
- Авторизовывать соединения и ограничивать сообщения