Чем на самом деле занято ваше приложение. Prometheus и его PromQL. Часть 2

💡 Усі статті, обговорення, новини про Java — в одному місці. Приєднуйтесь до Java спільноти!

Ещё раз привет, читатель. Я всё ещё Денис, и я продолжаю делиться опытом работы с мониторингом, который я получил в AB Soft. В прошлый раз мы оставались на уровне Java-кода, где я рассказал о Micrometer, о том, зачем он нужен и как им пользоваться. Если вы не читали первую часть, то советую таки пойти и почитать хотя бы до раздела Overview :)

А сейчас я предлагаю отдалиться от кода, представить своё приложение как чёрный ящик и сконцентрироваться на том, как оно взаимодействует с чёрным ящиком Prometheus. После чего попробуем визуализировать данные из Prometheus.

Поднимаем локальное окружение

Предлагаю сразу поднять локально весь зоопарк, чтобы можно было самостоятельно поиграться с Prometheus параллельно с прочтением статьи. У меня уже есть тестовый пример. Возможно, это не самое красивое в мире приложение для примера, но со своей задачей оно должно справиться. Вам остаётся только выполнить инструкции ниже и подождать, пока соберётся немного статистики. Для корректной работы вам понадобится только docker-compose.

  1. Склонируйте/скачайте себе этот проект на компьютер.
  2. Зайдите в него в папку metrics-demo-env.
  3. Запустите команду docker-compose up -d.
  4. Зайдите на http://localhost:8080 и нажмите кнопку GENERATE. Готово! Можете поставить ваш чай поближе к процессору, там он теперь не должен остывать.
  5. Когда понадобится выключить зоопарк — запустите команду docker-compose down в той же папке metrics-demo-env.

Серьезно! Запустите, пожалуйста, этот пример. Я на него потратил больше времени, чем на саму статью 🥲

Что такое Prometheus

Общая информация

Prometheus — это open-source продукт для сбора метрик и нотификаций. Изначально его создали в компании Sound Cloud, но с тех пор Prometheus прошёл длинный путь и уже развивается и поддерживается силами собственного комьюнити.

Логика работы Prometheus

Как я уже сказал, Prometheus отвечает за сбор и хранение метрик. И строго говоря, метрики, как и любую информацию, можно либо откуда-то тянуть (pull), либо ждать что тебе запихают (push). Так вот, конкретно Prometheus самостоятельно тянет метрики из приложений, которые он мониторит. Внутри Prometheus буквально конфигурируется один или более web-scrapper, который с заданным интервалом ходит по заданному адресу и просто сохраняет текущее состояние метрик в свою базу данных. Ожидается, что по этому адресу будут находиться метрики в специальном Prometheus-like формате, как по урлу ​​/actuator/prometheus из предыдущей статьи. А за то, чтобы в мониторящихся приложениях метрики были и были в нужном формате, отвечают exporters. Они являются частью этих самых приложений. Клиентские библиотеки Prometheus, в том числе, являются экспортерами.

Т. е. основной workflow взаимодействия вашего приложения и Prometheus происходит следующим образом.

  1. Вы напрямую либо через Micrometer вызываете API Prometheus (exporter) в своём приложении и создаёте/изменяете ваши метрики.
  2. Prometheus exporter выставляет все метрики наружу в специальном формате. В Spring Boot приложении по-умолчанию они будут по пути ​​/actuator/prometheus.
  3. Prometheus «как-то» узнаёт адрес вашего приложения (target). Под «как-то» я имею в виду, что либо адрес задан статически прямо в конфиге, либо Prometheus узнаёт адреса всех инстансов сервиса через вашу тулу, которая знает адреса всех рабочих POD, будь то Eureka или k8s.
  4. Prometheus периодически ходит по полученному адресу в ваше приложение и сохраняет все метрики в свою базу данных временных рядов. Периодичность тоже конфигурируется.

В целом, это достаточно последовательная логика, не правда ли?

Благодаря pull стратегии получения метрик, вряд ли получится заDDoSить Prometheus. Но я хочу обратить внимание, что поскольку Prometheus сам ходит в приложения за метриками, то сохранённые в результате метрики будут немного не точными. Т. е. если Prometheus ходит за метриками раз в секунду, то в базе данных они будут сохранены именно с точностью до секунды. И если с какой-нибудь gauge метрикой за условные полсекунды будет происходить что-то странное, то это может просто не попасть в базу данных. Вряд ли это станет существенным ограничением для вашего приложения, однако об этом стоит помнить при выборе системы для сбора метрик.

Дополнительные компоненты Prometheus

На самом деле, Prometheus — это не только один сервер для сбора метрик, а целая экосистема с кучей компонентов, в том числе и неофициальных (prometheus server, набор exporters для разных языков программирования, софтин и железяк — от видюх до чатиков, alert manager и прочее). Предлагаю взглянуть на схематическую картину устройства Prometheus, чтобы ещё раз уложить всё в голове и познакомиться ещё с парой компонентов Prometheus.

Из нового тут Pushgateway и Alertmanager. Первое — это мост для приложений, которые сами «пихают» (push) метрики. Это полезно, например, для короткоживущих приложений. А второе — это оповещалка о том, что, судя по метрикам, какому-то приложению поплохело. Например, нагрузка на CPU перевалила за допустимый порог.

Пробуем PromQL

Основные понятия

И так, PromQL — это декларативный язык запросов к Prometheus. Он представляет метрики как временные ряды, и при помощи него можно эти самые метрики вытаскивать, сортировать, группировать и применять к ним преобразования. В общем, PromQL это такой SQL для Prometheus. Есть отличная документация с объяснением основных концепций языка вот тут. А моя краткая выжимка будет ниже.

В языке есть два основных типа данных: Instant vector и Rage vector. Первый — это самый обычный временной ряд, набор значений, изменяющийся во времени. А вот второй тип — это чуть более хитрая вещь. Это что-то вроде двухмерного временного ряда. Если взять Instant vector и заменить значение в каждой точке на временной ряд от этой самой точки на n секунд в прошлое, то получится Range vector. Попробую ниже пояснить на примере. И чтобы закрыть тему с типами данных, в PromQL ещё есть числа с плавающей точкой и строки.

Вот так выглядит системная метрика jvm_memory_used_bytes, если запустить на полчаса мой пример выше. В моём примере на Java-приложение отправляется очень много запросов с фронта для вычисления факториала от 20000. А приложение, в свою очередь, каждый раз честно вычисляет факториал при помощи длинной арифметики, плодя в процессе огромное количество мусора.

Напомню, что метрики могут иметь таги, а конкретно jvm_memory_used_bytes имеет два тага: area и id. Поэтому за одним названием метрики мы фактически увидели много графиков, каждый с конкретным уникальным сочетанием значений тагов. Жёлтый график — это количество выделенной памяти в одной из частей хипа, в которой создаются новые объекты (area="heap", id="G1 Eden Space"). Если вы запустили пример у себя локально, то можете посмотреть на свои данные по этой ссылке.

Окей, а если дописать сразу за именем метрики что-то вроде jvm_memory_used_bytes[5m], то мы получим Range vector. В этом векторе будут содержаться все возможные промежутки времени от любой точки jvm_memory_used_bytes на 5 минут в прошлое. А если 5-минутный отрезок выходит за пределы фактически существующих данных, то данные для них будут экстраполироваться из того, что есть. т.е. если jvm_memory_used_bytes содержит данные с 22:30 до 23:00, то jvm_memory_used_bytes[5m] будет условное содержать отрезки 22:25:00-22:30:00, 22:25:01-22:30:01, 22:25:02-22:30:02... Ещё одна аналогия — Range vector это скользящее окно по временному окну.

Основные операции в PromQL

Кстати, Range vector никак не получится нарисовать в виде графика, потому что это не имеет смысла. Зачем тогда всё это нужно? А затем, чтобы применить к Range vector какую-нибудь функцию. Например, есть функция delta, которая принимает Range vector, вычисляет разность первым и последним элементом каждого из отрезков этого вектора и возвращает все эти разности в виде Instant vector. И записывается это следующим образом delta(jvm_memory_used_bytes[5m]).

Важно, delta можно использовать только с метриками типа gauge. Для неубывающих метрик вроде counter можно использовать функцию rate, которая в вычисляет скорость изменения метрики в секунду, или increase, которая эту скорость ещё и нормирует на размер окна Range vector.

И ещё одно замечание. Вероятно, есть смысл заменить delta на idelta, которая вычисляет разность между двумя последними записями. Мне кажется, это будет иметь больше смысла для жёлтого графика, а остальные всё равно не особо интересны.

И я бы предложил отфильтровать часть данных, оставив только метрики по памяти из хипа, написав следующее idelta(jvm_memory_used_bytes{area="heap"}[5m]). В фигурных скобках указано условие, которые будет применяться к тагам метрики из прошлой статьи. Проверки могут быть не только на равенство, но и на неравенство, и даже регулярка. И наконец переведу байты в мегабаты, дважды разделив метрику на 1024. Вот так это всё будет выглядеть idelta(jvm_memory_used_bytes [5m]) / 1024 / 1024.

А если хочется ещё и агрегировать данные, то для этого есть операторы агрегации. Они принимают Instant vector, что-то с ним делают и возвращают Instant vector. Их смысл в том, чтобы понизить размерность вектора. Вот например, если есть желание просто посмотреть на скорость изменения всей занятой памяти, то можно отправить запрос sum (idelta(jvm_memory_used_bytes[5m])) / 1024 / 1024.

Помните, что по запросу delta(jvm_memory_used_bytes[5m]) / 1024 / 1024 UI рисовал много графиков для всех существующих уникальных значений тагов метрики jvm_memory_used_bytes?. Написав sum, мы просто суммировали все эти графики в один.

А если есть желание, например, суммировать изменения в хипе и вне хипа отдельно, то можно дополнить запрос, указав по каким тагам нужно агрегировать sum by (area) (delta(jvm_memory_used_bytes[5m])) / 1024 / 1024. Так, выйдет всего два графика. Первый будет результатом суммы всех графиков, у которых area="heap", а второй будет результатом суммы всех, у кого area="nonheap".

Кстати, раз уж сильно скачет заполненность той части хипа, в которой появляются молодые объекты, то почему бы не посчитать меру разброса значений относительно среднего значения? Можно посчитать среднеквадратичное отклонение stddev_over_time(jvm_memory_used_bytes{id="G1 Eden Space«}[5m]) / 1024 / 1024. График ниже — это график изменения среднеквадратичного отклонения в 5-минутных отрезках. Я специально подождал немного больше получаса, чтобы сделать скриншот и увидеть, что происходило с Eden Space не только во время нагрузки (где-то с 22:25 до 22:55), но и после того, как я её выключил. Второй пик — это сборка мусора.

Выходит, что PromQL позволяет выбирать метрики, фильтровать, создавать «скользящие окна» благодаря Rage vector, применять к ним разные функции вроде rate и агрегировать результат. И конечно, в PromQL можно совершать математические и логические операции над Instant vector и числами в любой комбинации. Но когда в одной математической/логической операции два Instant vector, то неявно включается алгоритм группировки временных рядов. Мне кажется, это достаточно запутанная тема, главное, что при необходимости можно явно переопределять, как именно будут группироваться ряды.

Развлекаемся с PromQL

Среднее время время ответа

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

Например, можно посчитать среднюю скорость обработки HTTP запросов каждым из эндпоинтов. Если у вас запущено приложение, то вы можете открыть localhost:8080/actuator/prometheus и посмотреть, какие вообще метрики вам доступны из коробки. Там, например, есть http_server_requests_seconds_count, которая хранит в себе сумму количества всех запросов к серверу. При каждом запросе эта метрика инкрементируется на 1.

А ещё есть http_server_requests_seconds_sum, которая тоже инкрементируется при каждом запросе, но не на 1, а на количество миллисекунд, которое было затрачено на обработку запроса. Я предлагаю взять окно длинной в 5 минут и посчитать количество запросов в секунду и «количество секунд обработки запроса в секунду», применив уже знакомую функцию rate. А если разделить второе на первое, то выйдет скорость времени обработки запросов на единицу роста количества запросов. И агрегировать это всё по method и uri для удобства.

sum by (method, uri) (rate(http_server_requests_seconds_sum[5m]) / rate(http_server_requests_seconds_count[5m]))

Красная линия — это эндпоинт, в котором вычисляется факториал. Ещё я решил добавить эндпоинт с нагрузкой на сеть и создал один, который ходит на google.com и при помощи RegEx вытаскивает погоду в вашем месте и название вашей текущей локации, по мнению гугла. Запросы на второй эндпоинт отправлялись раз в пару секунд, чтобы читателя не забанил Гугл :)

Результаты для этого эндпоинта нарисованы залёным.

Кстати, с точки зрения синтаксиса можно сразу делить сумму времени запросов на их количество без преобразований Instant vector в Range vector и обратно. Запрос вроде sum by (method, uri) (http_server_requests_seconds_sum / http_server_requests_seconds_count) абсолютно валиден, однако он в результате выдаст график, где каждая точка будет являться средним временем обработки запросов с момента запуска приложения. В то время как в результате запроса выше получается среднее время обработки запросов за последние 5 минут.

Назад в первую часть цикла :C

Чтобы двигаться дальше, мне придется закрыть свой долг из первой части статьи. Я тогда не рассказал о таком типе метрик как DistributionSummary. А он по сути строит кумулятивную гистограмму, замеряя, какое количество раз «исходная метрика» была меньше или равна заданным пороговым значениям.

Сразу пример: в проекте с примером добавляется DistributionSummary для времени ответа сервера на запрос. Я решил быть щедрым и создал 7 пороговых значений от 63 мс до 4000 мс с шагом х2. И ещё 8е пороговое значение со значением +бесконечность создалось автоматически.

Выглядит это вот так.

@Bean
public MeterFilter meterFilter() {
   return new MeterFilter() {
       @Override
       public DistributionStatisticConfig configure(Meter.Id id, DistributionStatisticConfig config) {
           if (id.getName().startsWith("http.server.requests")) {
               return DistributionStatisticConfig.builder()
                       .serviceLevelObjectives(
                               Duration.ofMillis(63).toNanos(),
                               Duration.ofMillis(125).toNanos(),
                               Duration.ofMillis(250).toNanos(),
                               Duration.ofMillis(500).toNanos(),
                               Duration.ofMillis(1000).toNanos(),
                               Duration.ofMillis(2000).toNanos(),
                               Duration.ofMillis(4000).toNanos())
                       .build()
                       .merge(config);
           }
           return config;
       }
   };
}

В экспортере создалась новая метрика http_server_requests_seconds_bucket, которая содержит все те же таги, что и http_server_requests*, и дополнительный таг le, в котором записано пороговое значение. Вот, как это выглядит в экспортере.

Квантили

Можно воспользоваться встроенной функцией histogram_quantile, которая принимает вероятность в виде числа от 0 до 1 и Instant vector и возвращает Instant vector с изменением значения заданного перцентиля. Сложное предложение, но на примере обычно понятнее. Вот пример вычисления 90-го перцентиля времени ответа сервера с группировкой по URI и имени HTTP метода. Это вполне может быть полезно в продакшене, потому что физический смысл вычисления этого в том, что в 90% случаев время вычисления ответа не выше чем значение 90-го перцентиля.

histogram_quantile(0.9, sum by (method, uri, le) (rate(http_server_requests_seconds_bucket[5m])))

Агрегировать по le нужно обязательно, чтобы у histogram_quantile были данные, на которых можно строить гистограмму. И я думаю понятно, что histogram_quantile строит приближение к реальной гистограмме. Чем больше пороговых значений сконфигурировано, тем точнее выходит гистограмма. Кстати, таким же образом можно построить приближение к медиане времени ответа.

Ускорение вычисления факториала было из-за того, что я закрыл дебаг консоль в хроме на странице, где генерировался трафик к приложению. А потом снова открыл консоль. Почему так странно себя вёл эндпоинт, который слал запросы в гугл, я не знаю.

Apdex

Где-то в документации Prometheus я нашёл этот индекс. Это более-менее стандартный способ определять удовлетворенность пользователей скоростью работы сайта. Заявляется, что есть какое-то «хорошее время» ответа, есть «приемлемое», которое в 4 раза больше хорошего и «плохое». Так вот, Apdex складывает количество запросов отработавших за «хорошее» время, половину запросов за «приемлемое» и делит на общее количество запросов. Если в результате у вас всегда получается 1 или около того, то вы лапочки. Я решил, что 250 мс для меня — это хорошее время и попробовал вывести, как изменялось значение индекса в 5-минутных интервалах за полчаса работы примера.

(sum by (method, uri) (rate(http_server_requests_seconds_bucket{le="0.25"}[5m])) + sum by (method, uri) (rate(http_server_requests_seconds_bucket{le="1.0"}[5m]))) / sum by (method, uri) (rate(http_server_requests_seconds_count{}[5m])) / 2

Резюме

Prometheus — навороченная вещь. В нём есть есть много различных компонентов, закрывающих множество потребностей. Есть целая встроенная база данных, есть богатый язык запросов PromQL, есть внешнее API, к которому могут подключаться третьесторонние приложения. Есть возможность горизонтального масштабирования, есть возможность нотификации в случае, если какие-то метрики ведут себя странно. Даже есть возможность предсказания будущих значений метрик прямо на уровне языка PromQL, хоть и при помощи линейной регресси. Но вот графический интерфейс не то, чтобы сильно впечатлял, он скорее похож на MVP. Всё, что он позволяет — это построить заранее заданный тип графика, заранее заданного размера и не более чем одна метрика на одной диаграмме. Наверно, именно поэтому прямо на сайте проекта Prometheus в качестве визуализатора предлагают использовать Grafana.

Спасибо, что дочитали или хотя бы доскролили этот лонгрид до конца. Задавайте вопросы в комментарии. Всем хорошего дня!

👍ПодобаєтьсяСподобалось14
До обраногоВ обраному6
LinkedIn
Дозволені теги: blockquote, a, pre, code, ul, ol, li, b, i, del.
Ctrl + Enter
Дозволені теги: blockquote, a, pre, code, ul, ol, li, b, i, del.
Ctrl + Enter

Большое спасибо за хороший материал !

Prometheus — это open-source продукт для сбора метрик

из-за единственного retention периода, в качестве хранилища метрик, он откровенно — так себе

да, retention policy у Prometheus может быть только глобальным, в отличие от какого-нибудь InfluxDB. Но я не готов подписаться под утверждением, что из-за этого Prometheus совершенно не пригоден для использования в качестве хранилища метрик по двум причинам.

Во-первых, вряд ли найдётся решение без компромиссов. Смысл же выбора в том, чтобы больные места инструмента не были критичны для текущей задачи.
Во-вторых, если внезапно всё-таки возникнет потребность какие-то метрики хранить дольше обычного, то Prometheus позволяет подключать внешние хранилища, параллельно со своим TSDB. Тогда кастомную retention polity можно будет настроить уже силами внешней системы.

если внезапно всё-таки возникнет потребность какие-то метрики хранить дольше обычного

как раз невнезапно нормально хранить метрики за все время жизненного цикла системы (естественно уже с прореживанием/округлением), чтобы видеть и понимать тренды

Prometheus позволяет подключать внешние хранилища

ну и на кой он сам в роли TSDB тогда вообще нужен

Так метрики в реальном времени и агрегированные метрики за предыдущие годы работы это же разные вещи с разным смыслом. Прометей закрывает только первый пункт, давая возможность закрыть второй третьесторонними системами. Таков дизайн Prometheus-а.
А решать хороший ли он или плохой без контекста конкретной задачи и без сравнения с другими продуктами для мониторинга (в чём я не силён) мне кажется пустым занятием.

Та то надо было еще дописать в статье что прометеус это очень дёшево )
Честная 100% не семплированная трассировка со складыванием результатов куда-нибудь в эластик на долгий срок это конские деньги на инфру.

Читать метрики с логов — тоже очень дорого.
в 99,99% случаев информация о расходе памяти сервиса, нампример, или лейтенси не нужна больше чем на 1-2 дня (расследование инцидентов).

Підписатись на коментарі