Это вторая статья из цикла. В первой, вводной, я рассказывал, как устроены метрики для сервисов, чем отличаются от логов, и какую задачу вообще решают. Теперь подробнее про то, как их готовить.
В Точке мы используем Prometheus для работы с метриками. Он включает в себя:
сервер — хранилка и сборщик метрик;
формат данных;
язык запросов — еще его называют PromQL.
Можно поначалу запутаться, но обычно из контекста понятно, о чем речь. Есть еще проект по стандартизации формата данных и запросов — OpenMetrics, но нам это не важно — это, по сути, тот же Prometheus.
Сначала разберемся, как работает и хранит данные сервер, потом посмотрим на формат экспорта метрик из приложений и после этого, уже в следующих частях, научимся писать запросы. Prometheus так устроен, что формат метрик и язык запросов очень похожи друг на друга, поэтому не придется страдать слишком много.
Оглавление цикла:
Prometheus — сервер и клиенты
Scrape и подсчет метрик
Нам как пользователям нужно знать, что:
Задача приложения – выставить HTTP-страницу со своими метриками в определенном формате.
Сервер периодически делает HTTP-запрос
GET /metrics
к нашему приложению — это называется scrape. Интервал запроса может быть любым, можно настроить endpoint и заменить HTTP на что-то другое. В примерах дальше будем считать, что scrape делается раз в 30 секунд.Ответ приложения сервер сохраняет в БД с текущим timestamp.
Метрики приложения остаются в памяти приложения. Мы их дополняем и агрегируем дальше.
Идея в том, что Prometheus собирает срез во времени, а потом его средствами мы уже вычисляем изменения.
То есть мы могли за 30 секунд насчитать, что пришло 10 HTTP-запросов. Пришел scraper, и мы отдали ему эти данные. Что происходит дальше:
Правильно | Неправильно |
---|---|
Продолжаем дальше инкрементить этот же счетчик | Сбрасываем счетчик |
Да, технически счетчик можно сбросить, но делать этого не нужно! У приложения будет вечно копиться увеличивающийся счетчик (монотонно возрастающий). Это свойство нам пригодится потом: грубо говоря, чтобы узнать, «сколько пришло запросов за последнюю минуту», Prometheus будет брать производную. Кстати, производные — это не страшно: подробное и понятное объяснение будет в одной из следующих частей.
Безопасность
Prometheus по умолчанию приходит за метриками без аутентификации. Возникает проблема: а как закрыть к ним доступ, чтобы нельзя было что-то подсмотреть? Это особенно важно, если приложение доступно снаружи, из интернета. Есть разные варианты, кому как удобнее:
Настроить аутентификацию в самом Prometheus: он будет присылать запросы с нужными вам заголовками.
Хостить endpoint
/metrics
на отдельном порту, который не выставлен наружу.Настроить файерволл.
Сделать whitelist на уровне приложения.
Альтернативные способы скрапинга
Не всегда код живет как приложение с HTTP-сервером. Бывает, что это сервис без HTTP, какой-нибудь обработчик очередей RabbitMQ или вообще cronjob, который запускается по таймеру, отрабатывает и умирает.
Простой способ собрать метрики в таких случаях – решить, что накладные расходы на добавление HTTP-сервера только ради /metrics
вас не пугают. Это нормально, но не поможет cronjob-ам, которые не живут как постоянный процесс и не могут хранить и отдавать метрики каждые 30 секунд. Поэтому есть варианты, как можно обустроить сбор метрик в обход pull-модели. Придется поднять вспомогательный сервис на выбор:
Pushgateway – компонент Prometheus, который, по сути, живет как промежуточное приложение: в него можно отправить свои метрики, а Pushgateway уже будет их раздавать Prometheus’у.
Telegraf – универсальный конвертер и агрегатор метрик, который тоже придется держать постоянно запущенным. Можно настроить его на сбор и получение метрик любым удобным способом. Он умеет фильтровать и конвертировать полученные данные, а результат с него уже заберет Prometheus.
StatsD Exporter – приложение должно отправить туда метрики в формате statsd, а он их выставит для Prometheus. Концептуальное отличие только в формате, его точно так же придется держать постоянно запущенным.
Со стороны нашего кода обычно нужно подключить и настроить библиотеку для сбора метрик. Она уже будет агрегировать, форматировать и отдавать страницу с метриками. В каком-то стеке библиотеки сами подружатся с вашим веб-сервером, где-то придется поколдовать. Основная идея в том, что библиотеки для работы с метриками предоставляют API, через которое нужно зарегистрировать и описать метрику, и потом ее обновлять из любого места в приложении. Например, при получении HTTP-запроса увеличиваем метрику «количество запросов на этот endpoint», при отправке ответа – увеличиваем метрику «время обработки запросов». Теперь пора разобраться с тем, что такое метрики с точки зрения Prometheus, и как они обновляются из кода. Так мы поймем, какие методы использовать и какие метрики лучше подходят для определенных задач.
Prometheus – формат данных
Формат, в котором метрики пишутся приложением и отдаются Prometheus-ом из БД, достаточно простой и сделан, чтобы легко читаться глазами. Подсчет и форматирование метрик из приложения не нужно делать вручную — для этого есть библиотеки. Вот так выглядит страничка, которую приложение должно отдать на GET /metrics
:
# HELP http_requests_total Requests made to public API
# TYPE http_requests_total counter
http_requests_total{method="POST", url="/messages"} 1
http_requests_total{method="GET", url="/messages"} 3
http_requests_total{method="POST", url="/login"} 2
Что здесь есть:
HELP
— описание для помощи человекам;TYPE
— тип метрики;http_requests_total
— имя метрики;набор key-value лейблов (можно еще называть их тегами);
значение метрики (64-bit float aka double);
после сбора в БД добавляется еще timestamp.
Хранение работает так: имя метрики – на самом деле тоже лейбл с именем __name__
. Все лейблы вместе описывают собой time series (временной ряд), т.е. это как бы имя таблицы, составленное из всех key-value. В этом ряду лежат значения [(timestamp1, double1), (timestamp2, double2), ...]
. Из примера выше, у нас одна метрика, но в базе есть три таблицы: для GET /messages
, POST /messages
и POST /login
. В каждую таблицу раз в 30 секунд пишется очередное число, которое момент scrape-а показало приложение.
Хранятся double-ы в разрезе времени. Никаких int-ов. Никаких строк. Никакой дополнительной инфы. Только числа!
Кстати, полезно посмотреть практики именования в документации. Лейблы используются для поиска и для агрегации, но есть одна особенность: серверу поплохеет, если для лейблов часто использовать уникальные или редко повторяющиеся значения. Потому что...
Кардинальность
Каждое новое значение лейбла – это уже новый временной ряд. То есть новая таблица. Поэтому не надо ими злоупотреблять. Хороший лейбл ограничен в возможных значениях. То есть целиком User-Agent
туда писать плохо, а вот название и мажорную версию браузера – ОК. Юзернейм, если их сотни – сомнительно, а если их десятки – ОК (api-клиенты внутреннего сервиса, например).
Например, мы пишем метрики о HTTP-запросах. Перемножаем все возможные значения всех лейблов: 2 HTTP глагола, 7 урлов, 5 реплик сервиса, 3 вида ответов (2xx, 3xx, 4xx), 4 браузера. 840 временных рядов! Ну то есть это как 840 таблиц в sql. Prometheus справляется с десятками миллионов рядов, но комбинаторный взрыв можно устроить очень быстро. Подробнее можно еще почитать тут: Cardinality is key.
В целом, не стесняясь пишите то, что вам действительно надо, но не злодействуйте. Следите за потреблением ресурсов Prometheus и чтобы в лейблах не было произвольного текста.
Прежде чем писать метрику, подумайте, в каком виде она будет отображаться? Вряд ли вам нужен график, на котором пляшут десятки разноцветных линий, поэтому записывать точный user-agent бесполезно. Вы все равно захотите его сгруппировать во что-то осмысленное. С другой стороны, одна и та же метрика может быть сгруппирована по разным лейблам и нарисована на разных графиках. Если вы, например, считаете HTTP-запросы и сохраняете в лейблы method, id клиента и код ответа, эту метрику уже можно вывести разными способами: HTTP requests by client
, HTTP requests by method and response code
.
Типы метрик
Несмотря на то что у метрик есть поле TYPE
— под капотом разницы нет. Это, как и HELP
, только для людей, чтобы было проще работать. Но библиотеки, которыми мы пишем метрики, построены вокруг этих типов. И некоторые функции в запросах корректно работают только для определенных типов. То есть можно считать тип соглашением о том, как себя ведет значение этой метрики.
Дальше в тексте, API
– это усредненное название методов из prometheus-библиотек под разные языки. Просто для иллюстрации. Есть, конечно, варианты. Например, библиотека для dotnet App Metrics имеет немного другие названия и методы, но суть не меняется.
Counter
Счетчик – монотонно возрастающее число. Никогда не убывает! Может быть сброшен в ноль, например, при рестартах сервиса, который пишет метрику. Это важно, т.к. у Prometheus есть специальные функции, которые это учитывают. API: increase()
, add(x)
# Примеры метрик: количество обработаных запросов, ошибок, задач
http_requests_total{url="/login"} 10
http_requests_total{url="/"} 100
http_errors{status="500", url="/"} 3
http_errors{status="401", url="/"} 26
http_errors{status="400", url="/login"} 11
http_errors{status="404", url="/admin"} 298
jobs{type="cleanup", status="completed"} 42
jobs{type="cleanup", status="failed"} 38
Как узнать, сколько запросов было за единицу времени, когда у нас всего одно число? Посмотреть на дельту, т.к. Prometheus сохраняет снимки этого числа каждые 30 секунд. Ну и понадобится дополнительный костыль, если приложение перезапустилось, и счетчик вдруг сбросился в ноль – это уже учтено в функциях, которые работают со счетчиками.
Gauge
"Стрелка" — число, которое может гулять вверх-вниз. API: setValue(x)
, increase()
, decrease()
# Примеры метрик: количество обрабатываемых запросов прямо сейчас, занятая память, свободное место на диске
http_active_requests{app="web"} 5
http_active_requests{app="internal"} 1
memory_swap{host="test"} 0
memory_swap{host="prod"} 102400
memory_usage_bytes{host="test"} 1295007744
memory_usage_bytes{host="prod"} 5476434545
disk_free_bytes{path="/var/www/"} 29298077696
disk_free_bytes{path="/tmp/"} 37359484928
Поскольку эта штука не монотонная, для нее не сработают некоторые математические фокусы, то есть она чуть больше ограничена в использовании. Что за фокусы? Об этом в четвертой части цикла.
Histogram
Гистограмма — агрегация чего-то самим приложением, когда нам интересно знать распределение величин по заранее определенным группам (buckets). API: observe(x)
Например, мы хотим знать длительность HTTP-запросов. Определимся, какое время считать хорошим, какое плохим, и насколько детально мы хотим это знать. Можно сказать, качественное распределение:
<= 0.1
сек. — хороший запрос, ожидаем, что таких будет большинство;<= 1
— сойдет, но лучше бы знать, что такие встречаются;<= 5
— подозрительно, пойдем смотреть код, если таких окажется много;больше 5
— вообще плохо, для однообразия можно сказать, что это<= infinity
.
Как это работает: пришел запрос, померяли время обработки X
и обновили гистограмму: добавили +1
в соответствующие группы и добавили +X
к суммарному времени. Вот несколько примеров попадания запросов с разным временем в бакеты:
0.01
попадет во все бакеты:<= 0.1
,<= 1
,<= 5
,<= infinity
;0.3
попадет в бакеты кроме первого:<= 1
,<= 5
,<= infinity
. В первый не попадает, т.к. время больше0.1
;4
попадет в бакеты:<= 5
,<= infinity
. В первый и второй не попадает, т.к. время больше0.1
и1
;10
попадет только в бакет<= infinity
. В остальные не попадает, т.к. время больше0.1
,1
и5
.
# Пример метрики: распределение времени обработки HTTP-запросов по 4 бакетам
http_duration_bucket{url="/", le="0.1"} 100
http_duration_bucket{url="/", le="1"} 130
http_duration_bucket{url="/", le="5"} 140
http_duration_bucket{url="/", le="+Inf"} 141
http_duration_sum{url="/"} 152.7625769 # это бонусом идет сумма всех значений, которые мы записали
http_duration_count{url="/"} 141 # это количество значений, т.е. counter который всегда делает +1 на каждое обновление гистограммы
le
— просто лейбл, который генерируется из наших бакетов. Никакой магии. Означает "less than or equal", то есть<=
Гистограмма считает количество попаданий в какую-то группу, то есть запоминает счетчики, а не сами значения! Мы ведь ограничены тем, что метрика сама по себе — это только одно число. Каждый бакет — как бы отдельная метрика.
Как этим пользоваться? Можно просто вывести на график нужный бакет, поделив его на count
: получим соотношение этого бакета ко всем запросам, т.е. долю «хороших» или «плохих» запросов в общей массе, смотря что мы хотим наблюдать. Но лучше делать это не руками, а одной функцией агрегировать в квантили (см. в следующей части). Это удобно, просто и будет обсчитываться на Prometheus-сервере, хоть и с потерей точности (меньше бакетов — меньше точность). Если вы хотите считать квантили самостоятельно или не знаете заранее, какие бакеты нужны, есть другой тип — Summary.
Summary
Сводка - готовьтесь, сейчас будет сложно. На первый взгляд, похожа на гистограмму, но на самом деле — это результат агрегации гистограммы. Она выдает сразу квантили, можно сказать, количественное распределение, когда мы заранее не можем определить бакеты. API: observe(x)
Читайте про квантили в следующей части и смело возвращайтесь — станет гораздо понятнее!
Проще всего объяснить на практике: обычно мы заранее не знаем, что считать хорошим временем для запроса, а что плохим. Поэтому просто закинем измеренное время в Summary, и потом посмотрим, во что впишутся 95% запросов. Ну и 50%, и 99% тоже. Итак, пришел запрос, померяли время обработки X
, записали в Summary:
+1 в счетчик количества запросов;
само время
X
закинули во множество значений в памяти приложения;пересчитали квантили;
периодически придется выкидывать из памяти старые значения, чтобы не расходовать ее бесконечно.
# Пример метрики: распределение времени обработки HTTP-запросов по 5 квантилям
http_duration_summary{quantile="1"} 100
http_duration_summary{quantile="0.99"} 4.300226799
http_duration_summary{quantile="0.95"} 2.204090024
http_duration_summary{quantile="0.5"} 0.073790038
http_duration_summary{quantile="0.1"} 0.018127115
http_duration_summary_sum 152.7625769 # как у гистограммы, сумма всех значений
http_duration_summary_count 141 # и количество значений
Как это интерпретировать? Здесь тоже что-то вроде бакетов, как в гистограмме, но с другим смыслом. Если вкратце, метрика с quantile="0.95"
говорит нам, что 95% запросов выполнялись быстрее, чем за 2.2
секунды. Аналогично, 99% запросов выполнялись быстрее, чем 4.3
секунды, и так далее. Как это работает и зачем нужно, станет понятно только после объяснения квантилей, поэтому вернемся к Summary в последней части.
Сводки нельзя просто так агрегировать в лоб, но вообще можно, если вы думаете головой, с потерей точности (и об этом тоже в следующей части, ага). А еще они висят в памяти приложения, так как нужно запоминать набор значений за какой-то промежуток времени. Из-за этого сводки считают квантили с потерями: старые данные постепенно вытесняются, поэтому они оказывают меньшее влияние на значение, которое получается в данный момент. Можно применять разные подходы: например, «сдвигать окно» – выбрасывать самые старые значения. Или выкидывать случайные. Зависит от того, что мы больше хотим видеть в метрике: статистику по вообще всем запросам, или только по недавним.
В примерах используется одна и та же метрика –
http_duration
. Так сделано только для наглядности в статье. Одну и ту же метрику не нужно писать сразу в двух видах, это избыточно. Выбирайте либо histogram, либо summary.
С типами и их особенностями закончили, самое время поднять приложение на своем стеке, подключить библиотеку для экспорта метрик и попробовать что-то вывести. Дальше будем разбирать язык запросов PromQL.