Чем на самом деле занято ваше приложение. Micrometer. Часть 1

Підписуйтеся на Telegram-канал «DOU #tech», щоб не пропустити нові технічні статті.

Привет, читатель! Меня зовут Денис и так сложилось, что за 6+ лет в Java-разработке я так и не столкнулся с необходимостью залезть в кишки разработанного моей же командой приложения и вытащить оттуда хоть какую-нибудь статистику. Точнее, такая необходимость внезапно появилась только этим летом, когда я присоединился к компании AB Soft.

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

Ниже будет первая статья из цикла о том, как затащить к себе в джаву мониторинг на Micrometer, Prometheus и Grafana. Это полезно, потому что, как правило, информация о том, как работает конкретный кусок вашего сервиса во времени, помогает найти бутылочное горлышко в системе и даёт возможность трезво оценивать ограничения приложения. Если вы ищете инструмент, чтобы понять, как именно вы можете измерить ваше приложение, то этот цикл как раз для вас. Усаживайтесь поудобнее и приятного прочтения.

Зачем это вообще нужно

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

Overview

Давайте сразу обозначу, что есть что. Для начала, мониторинг — это про наблюдение за какими-нибудь метриками вашего приложения во времени. Например, объём хипа приложения в конкретной точке во времени — это и есть метрика, и имея значения размера хипа в нескольких разных точках, можно построить график потребления памяти приложением. То есть метрика — это такая пара «ключ-значение», которая может меняться со временем.

Micrometer — это Java-библиотека с API для создания метрик. Причём Micrometer — это фасад, за которым можно сконфигурировать одну из многих систем для сбора метрик, включая Prometheus. Вот официальная дока.

Как вы уже поняли, Prometheus — это решение для сбора метрик. Но он не только даёт возможность собирать метрики через свой Java-клиент, но и хранит метрики в time-series базе данных и имеет кучу всяких полезностей вроде email-оповещений. Подробнее тут.

А Grafana — это красивый UI-визуализатор данных. Она умеет брать данные из множества мест, включая Prometheus. В Grafana есть понятие дашборда — это множество графиков, которые каким-то образом размещены вместе на одной странице. Каждый дашборд имеет уникальное имя, может экспортироваться/импортироваться в конфиг-файл.

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

Micrometer

Как я уже сказал, Micromeret — это фасад для клиента мониторинга. Для нас эта библиотека выглядит как выставленный наружу минималистичный бин MeterRegistry, в котором находится почти всё, что нужно для мониторинга.

Оговорюсь, что Micrometer даёт возможность не только создавать метрики, но и читать их прямо в Java-коде и делать минимальную агрегацию. В статье я буду говорить только о создании метрик через Micrometer.

У нас есть возможность создавать три разных вида метрик: counter, timer и gauge. У всех трёх имя задаётся строкой, а значение — числом. Причём для таймера значение — это long, для счётчика — double, а для gauge — это вообще объект Number. И ведут себя эти типы метрик немного по-разному, предлагаю сразу взглянуть на примеры.

Counter

Пример использования counter-ов:

meterRegistry.counter("my.counter1").increment();
meterRegistry.counter("my.counter2").increment(49.5);

Счётчик умеет только увеличиваться. По умолчанию он увеличивается на 1, но есть возможность передать кастомный инкремент в параметрах. Кто первый напишет в комментах, что будет, если в метод инкремента передать отрицательное число и почему — тот будет молодец :)

То есть если вспомнить, что все метрики сохраняются где-то в time-serias базе данных, то получается картина, когда на выходе мы будем иметь восходящий график значения метрики от времени.

Timer

Пример использования timer-а:

meterRegistry.timer("my.timer1").record(20, TimeUnit.MILLISECONDS);
meterRegistry.timer("my.timer2").record(() -> {});
meterRegistry.timer("my.timer3").wrap(() -> {}).run();

Я думаю, суть таймера предельно ясна, как и в целом понятно, что в первой строке из примера таймеру устанавливается значение в 20 мс. Ожидается, что у метрики типа «таймер» могут быть любые целые неотрицательные значения.

Но ещё у таймера есть интересные возможности для использования. Во втором примере в record передаётся Runnable. Переданный Runnable будет вызван самим Micrometer-ом и время работы Runnable-а будет записано в качестве значения метрики.

В третьем же примере метод wrap тоже замеряет время работы коллбека, но делает это иначе чем record. Он не запускает коллбек самостоятельно, а лишь оборачивает в свой, который будет засекать время работы при вызове. Кроме Runnable методы wrap и record ещё принимают Callable и Supplier.

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

Gauge

Пример использования gauge:

AtomicLong gauge1 = gaugeMap.computeIfAbsent("my.gauge1",
       metricName -> meterRegistry.gauge(metricName, new AtomicLong()));
gauge1.incrementAndGet();

AtomicLong gauge2 = gaugeMap.computeIfAbsent("my.gauge2",
       metricName -> meterRegistry.gauge(metricName, new MyStateObject(), this::statObjectToDouble));
gauge2.incrementAndGet();

Этот тип метрик используется чуть иначе. Во-первых, gauge может иметь любое численное значение в любой момент времени. Также gauge заставляет разработчика самостоятельно передавать объект, в котором будет храниться значение метрики. И объект должен быть либо наследником Number как в первом примере (Integer, Long, Double, AtomicLong, AtomicInteger, etc), либо вообще любым объектом. Но во втором случае вместе с объектом нужно передавать функцию, которая умеет преобразовывать объект в double (второй пример).

И есть не очень очевидный подводный камень, из-за которого я использовал в примере с gauge словарь. Смысл в том, что guage оставляет необхоимость хранить последнее значение метрики на нас. Поэтому я каждый раз перед использованием метрики проверяю, не создана ли она уже, а если создана, то использую её вместе с её предыдущим значением. Можно, конечно, и не усложнять себе жизнь со словарём, но в таком случае придётся смириться, что не получится узнать «предыдущее» значение метрики, а значит, и не получится делать инкремент и декремент. Хотя если необходимо только устанавливать какое-то конкретное значение в gauge метрику, то словарь, действительно, не нужен.

Этим типом метрик удобно, например, мониторить количество свободных/занятых соединений к базе данных. Наверное, такого рода вещи логичнее всего мониторить именно при помощи gauge. А вещи вроде мониторинга количества запросов к приложению в конкретный промежуток времени лучше мониторить при помощи counter-a. Возможно, это не очень очевидно на уровне Java кода, но на уровне дашбордов Grafana-ы можно легко преобразовать неубывающий график counter-метрики в график количества метрик за час/минуту/секунду с пиками и просадками.

Другие типы метрик

На самом деле, это не все типы метрик, а только те, которые можно использовать напрямую из MeterRegistry. Есть ещё DistributionSummary для создания гистограм, LongTaskTimer и загадочный тип Other, который даже не имеет своей имплементации. Но описанные три типа мертик должны покрыть бОльшую часть вашей необходимости в мониторинге, поэтому остановимся только на них. Но если кому-то интересно, то вот ссылка на все типы метрик в Micrometer.

Таги

Выше я приводил пример с добавлением таймера в контроллер. Представим, что вместе с таймером у вас есть желание видеть и counter, чтобы знать, сколько запросов и каким контроллером сейчас обрабатывается. Но как быть, если у вас много эндпоинтов и вы хотите, с одной стороны, иметь возможность смотреть значения метрик отдельно для каждого эндпоинта, а с другой — видеть суммарное значение метрики для всех точек входа? Можно для каждого эндпоинта создавать метрику со своим уникальным именем, а суммарное значение считать в Grafana-е. Это решение не ок, потому что, во-первых, суммирование множества метрик грозит быть громоздким, а во-вторых, при добавлении нового эндпоинта нужно будет не забыть довать метрику в общий график. А если забудете, то мониторинг просто будет работать не корректно, но без каких-либо ошибок в логе.

Эту проблему можно решить при помощи тагов. Таг — это пара строк ключ-значение, по сути лейбл метрики. Ниже приведу пример создания тагов.

List<Tag> tagList = asList(Tag.of("method", "GET"), Tag.of("URL", "/api/v1/users/{id}"));
meterRegistry.counter("my.tagged.counter", tagList).increment();

Есть важное замечание: все метрики всегда должны иметь одно и то же множество тагов. Метрику нельзя сначала создать с тагами method и URL, а потом с тагами method, URL и error_code, это приведёт к ошибке во время выполнения.

Интеграция со SpringBoot-ом и конфигурация

SpringBoot не был бы собой, если бы не было возможности простым добавлением зависимости сконфигурировать мониторинг или вроде того. Для того, чтобы сконфигугрировать мониторинг, нужно чтобы, во-первых, у вас вообще был actuator. Во-вторых, чтобы была зависимость на автоконфиг мониторинга Micrometer поверх Prometheus. В третьих, в application.* нужно включить Prometheus. Вот и всё! Более конкретно на примере ниже (зависимости для Prometheus+Micrometer в build.gradle):

implementation 'org.springframework.boot:spring-boot-starter-actuator:2.6.0'
runtimeOnly 'io.micrometer:micrometer-registry-prometheus:1.8.0'

Конфиг для Prometheus в application.yml:

management:
 endpoints:
   web:
     exposure:
       include: health,prometheus

Применяя изменения выше, вы добавляете всё, что необходимо, чтобы подружить Spring, Micrometer и Prometheus. И в качестве бонуса вы получаете набор уже готовых и работающих метрик, в том числе, метрики для входящих/исходщих HTTP соединений для данных о JVM, сборщике мусора, etc.

Да как же всё это выглядит?

Я понимаю, что статья уже далеко не маленькая, а получить какой-то фидбек от всего описанного выше до сих пор не вышло. К счастью, без труда увидеть какие-то данные мониторинга без визуализаторов вроде Grafana возможно, хотя результат будет куда менее информативен. По умолчанию у вашего сервиса появится эндпоинт /actuator/prometheus, на котором можно посмотреть текущее значение всех метрик. Каждая метрика выглядит примерно как имя{таги} значение. Вы можете скачать мой пример на GitHub, чтобы поиграться с метриками. Ниже приведу пример метрик из этого приложения.

Пример метрик

Как видите, каждый «блок метрик» начинается с небольшого описания о том, что он из себя представляет. Далее идут сами метрики с тагами и их значения. Всё метрики на скриншоте «появились автоматически». Например, http_server_requests_seconds* содержат в себе метрики о запросах к эндпоинтам приложения. И конечно, у нас есть возможность очень сильно конфигурировать список дефолтных метрик и даже их названий, подробно с этим можно разобраться в документации Micrometer, ссылку на которую я оставлял выше.

Итог

В этой главе я рассказал вам о метриках, оставаясь на уровне вашего приложения. Micrometer — это достаточно простая штука, чтобы создавать метрики и как-то скармливать их дальше, например в Prometheus. И конечно, это только часть инфраструктуры, которая позволит вам удобно наблюдать за тем, что происходит в вашем приложении. Строго говоря, страница /actuator/prometheus не то, чтобы была особо информативной для понимания, что происходит внутри приложения, поэтому в следующей части посмотрим поближе на Prometheus и Grafana. И наконец построми-таки графики.

Это всё, что я хотел сказать на сегодня. Спасибо, что дочитали, надеюсь, что было интересно и полезно. Всем бобра, ни бага ни лага и иммунитета от ковида, пока!

👍ПодобаєтьсяСподобалось13
До обраногоВ обраному11
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
SpringBoot не был бы собой, если бы не было возможности простым добавлением зависимости сконфигурировать мониторинг или вроде того. Для того, чтобы сконфигугрировать мониторинг, нужно чтобы, во-первых, у вас вообще был actuator.

Дякую за пост, але, на жаль, тут не вказано, що Spring Boot Actuator не тільки включає Micrometer, але й містить багато вбудованих метрик. Понад те, він дозволяє декларативно створювати метрики, наприклад, для HTTP endpoints.
Тому цікаво, а як автор використовував Micrometer без Actuator? Сам створював метрики виду Memory Usage, CPU Utilization і т.д. ?

Micrometer и Prometheus endpoint подключаются как часть Spring Boot Actuator. Пример

Вы правы, возможно я кого-то ввёл в заблуждение, сказав что micrometer подключается к spring boot, а не к actuator. Выделил жирным уточнение.

Но при этом описание Actruator выходит за рамки статьи.

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