Как стать автором
Обновить
Точка
Как мы делаем онлайн-сервисы для бизнеса

Человеческим языком про метрики 2: Prometheus

Время на прочтение10 мин
Количество просмотров95K

Это вторая статья из цикла. В первой, вводной, я рассказывал, как устроены метрики для сервисов, чем отличаются от логов, и какую задачу вообще решают. Теперь подробнее про то, как их готовить.

В Точке мы используем Prometheus для работы с метриками. Он включает в себя:

  • сервер — хранилка и сборщик метрик;

  • формат данных;

  • язык запросов — еще его называют PromQL.

Можно поначалу запутаться, но обычно из контекста понятно, о чем речь. Есть еще проект по стандартизации формата данных и запросов — OpenMetrics, но нам это не важно — это, по сути, тот же Prometheus.

Сначала разберемся, как работает и хранит данные сервер, потом посмотрим на формат экспорта метрик из приложений и после этого, уже в следующих частях, научимся писать запросы. Prometheus так устроен, что формат метрик и язык запросов очень похожи друг на друга, поэтому не придется страдать слишком много.

Оглавление цикла:

  1. Потерянное введение

  2. Prometheus

  3. Перцентили для чайников

  4. PromQL

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.

Теги:
Хабы:
Всего голосов 16: ↑16 и ↓0+16
Комментарии5

Публикации

Информация

Сайт
tochka.com
Дата регистрации
Дата основания
Численность
1 001–5 000 человек
Местоположение
Россия
Представитель
Сулейманова Евгения