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

В Точке мы используем 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.

Комментарии (3)


  1. MechanicusJr
    05.09.2022 21:19
    +1

    пропущен кусок установки самих сборщиков к прому. А это такой .. адок


    1. Rast1234 Автор
      05.09.2022 22:39
      +1

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


  1. 9982th
    05.09.2022 22:37
    +1

    Если на машине уже стоит node exporter, то для экспорта вывода задач из cron'a можно воспользоваться textfile collector. Собственно, если задача выполняется под рутом, например сбор статистики S.M.A.R.T., то ей совершенно определенно не стоит иметь http-эндпоинт.