PROM 09: Создание и настройка алертов

Описание:

Всем привет!

В первой части нашей программы по Prometheus мы разобрали много немаловажных концептуальных вещей. Рассмотрели, как собираются метрики с ваших систем и сервисов, научились пушить данные через pushgateway, изучили различные типы метрик и научились анализировать их с помощью языка promql. Но настал следующий этап и нам предстоит обсудить, наверное, одну из важнейших тем данного практикума - настройка алертов. Ведь согласитесь - собирать метрики - это здорово, но нужно сразу узнавать о том, что что-то работает не так, как нужно. И, желательно, не от разъяренных пользователей. Итак, сегодня мы поговорим о том, как prometheus работает с алертами, расскажем про то, как вы можете писать и проверять свои правила оповещений, а так же дадим пример стандартных алертов, которые вы будете использовать постоянно.

По поводу реального кейса дополнить даже и нечего - вы и сами все прекрасно понимаете. Вам нужны нотификации для того, чтобы узнавать о проблемах не от начальства или пользователей, а заранее. Плюс вы сможете предотвращать некоторых из них проактивно - то есть до их реального наступления. Например, можно будет расширить вашу ферму веб серверов при увеличении нагрузки или заменить сбойный диск еще до момента его выхода из строя. Итак, поехали!

Как Prometheus работает с алертами

Если рассматривать основную концепцию, то авторы prometheus поступили очень мудро - они разделили правила алертинга (условия при котором должен сработать алерт) и методы его доставки (email/slack/telegram/etc, как группировать алерты и так далее). Сами правила алертинга настраиваются в prometheus с помощью alert_rules, которые пишутся на языке promql и позволяют добавить условия при которых должен сработать алерт.

Второй же частью этой картины - доставкой оповещений об алертах занимается другой компонент - alertmanager. Он позволяет группировать алерты, направлять их в различные каналы (например оповещения с dev отправлять в slack, а с prod - на телефон), а так же использовать различные сервисы для отправки уведомлений (slack, twilio, pagerduty и другие). Про alertmanager мы поговорим подробнее в следующем задании, а сейчас остановимся на настройках алертов в prometheus.

Итак, для того чтобы добавить правила алертинга в prometheus, вам придется использовать promql. Правила алертинга работают очень простым образом. Если указанный запрос возвращает какие-то timeseries данные, то правило срабатывает. То есть если мы возьмем наш пример с метрикой up:

root@prom-test:/opt/prometheus# ./promtool query instant http://localhost:9090/ "up{} == 0"

Сейчас данный запрос ничего не выводит, поскольку все метрики up равны единице:

root@prom-test:/opt/prometheus# ./promtool query instant http://localhost:9090/ "up{}"
up{instance="localhost:8080", job="app"} => 1 @[1622740363.323]
up{instance="localhost:9090", job="prometheus"} => 1 @[1622740363.323]

Как только одна из метрик up станет равна нулю, то правило сработает и отобразит эту метрику. Таким образом вы можете описывать абсолютно любые кейсы и настраивать на них правила оповещений - персентиль 90% времени ответа начал расти, место на жестком диске закончится через 2 часа - абсолютно любые условия могут стать алертом! Давайте попробуем посмотреть вживую на то, где именно вы будете описывать правила алертинга.

Настройка алертов в Prometheus

Для настройки алертов в promethues вам потребуется в основном конфигурационном файле указать параметр rule_files, который отвечает за то в каком файле prometheus будет искать правила алертинга. Давайте отредактируем конфиг и укажем наш файл:

rule_files:
- "alerts.yml"

После этого можем создать файл alerts.yml (он должен находиться в той же директории что и prometheus.yml) с нашим алертом по доступности хостов:

groups:
- name: instances
interval: 10s
rules:
- alert: InstanceDown
expr: up == 0
for: 1m
labels:
severity: critical
annotations:
summary: Instance is down!

В данном случае в самом начале мы определяем группу, в которую будут входить наши правила. Имя группы должно быть уникальным в рамках одного файла. Для каждой группы мы можем определить свой собственный интервал выполнения (interval: 10s). Это интервал с которым будут выполняться правила алертинга. В нашем случае это запрос up == 0 - именно этот запрос будет периодичеки выполняться. Если же interval для группы не задан, то будет использован глобальный параметр evaluation_interval. Помните в самом начале prometheus.yaml мы задавали парамер evaluation_interval - это именно он и есть.

global:
scrape_interval: 15s # Set the scrape interval to every 15 seconds. Default is every 1 minute.
evaluation_interval: 15s # Evaluate rules every 15 seconds. The default is every 1 minute.

Итак, prometheus будет выполнять наш запрос up == 0 каждые 10 секунд (в нашем примере) и если обнаружит какой-то ответ - то есть найдутся какие-то метрики, которые подходят под данный запрос, то далее prometheus начнет ждать время, определенное в параметре for - в нашем случае 1 минуту. Если за эту минуту данный запрос так и продолжит возвращать ту же самую метрику, то prometheus начнет отправлять информацию об этом алерте в alertmanager (как именно рассмотрим в следующем задании).

Давайте еще раз проговорим весь flow нашего алерта. Изначально созданный алерт неактивен - находится в стадии inactive. Через один и тот же интервал времени, prometheus выполняет указанный нами запрос. Если запрос возвращает какие-то метрики, которые попали под его условие, то алерт переходит в состояние pending и находится в нем в течение времени, указанного в парамтре for. Пока алерт находится в состоянии pending, prometheus все так же как и раньше выполняет наш запрос через заданный интервал и проверяет не пропала ли данная метрика, подходящая под условия. Если не пропала, то по истечении времени указанного в параметре for, алерт переходит в статус firing и начинает отправляться в alertmanager. А далее alertmanager уже сам начнет оповещать всех заинтересованных по заранее заданным каналам.

Стоит обратить внимание, что prometheus будет периодически (раз в 30 секунд) отправлять алерт в alertmanager, пока он находится в состоянии firing. Таким образом alertmanager точно будет знать, что алерт все еще активен - ведь информация о нем приходит каждые 30 секунд.

Важно: Очень важно отметить, что понимать схему работы алертов крайне важно для того чтобы вы смогли правильно настроить тайминги для срабатывания. Представьте себе что вы собираете информацию с узлов каждые 2 минуты, а интервал проверки правил алертинга установлен в 5 секунд. И значение for выставлено в 1 минуту. Тут получается некоторая рассинхронизация. Допустим что алерт сработал. В этот момент он переходит в статус pending и prometheus начинает ждать 1 минуту, выполняя данный запрос каждые 5 секунд. Но так как информацию с узлов вы собираете раз в 2 минуты, то очевидно что значение вашего запроса не сможет измениться за эту минуту, сколько бы раз вы его не запустили - новых данных-то не появится. В данном случае логичным было бы увеличить время обработки правил и значение for. Ну или если позволяют ресурсы вашего сервера - стоит уменьшить сбор метрик до 5 секунд, чтобы при проверке запроса он каждый раз обрабатывал новые данные.

Как работают алерты мы разобрались, давайте теперь перейдем к последней части нашей группы - к настройкам labels и annotations. Как понятно из названия labels используются точно так же, как и в prometheus - это теги. Благодаря им, alertmanager сможет раскидывать наши сообщения по различным каналам и группам. Annotations же - это просто расширенный текст алерта, который alertmanager сможет отправить вам в мессенджер или на телефон - чтобы вы сразу могли увидеть что произошло. Annotations предназначены для хранения длинного текста. Оба этих параметра могут быть параметризированы. Давайте выключим наше приложение, которое опрашивал prometheus и посмотрим на текущие алерты:

root@prom-test:/opt/prometheus# ./promtool query instant http://localhost:9090/ "ALERTS{}"
ALERTS{alertname="InstanceDown", alertstate="firing", instance="localhost:8080", job="app", severity="critical"} => 1 @[1622757949.277]

Да, вы поняли все правильно - в prometheus существует специальная метрика ALERTS, которая позволяет посмотреть все текущие алерты. В нашем случае мы видим алерт alertname=InstanceDown и его теги. Прошу обратить внимание, что алерту добавляются все теги сработавшей метрики - например instance и job, а так же добавляются новые теги - alertname, alertstate и наш тег, заданный в конфигурации - severity. Все эти данные так же будут переданы в alertmanager.

Давайте теперь подробнее остановимся на параметризации. В labels & annotations мы можем использовать go templating для подстановки переменных в описание алерта. Давайте расширим наш предыдущий пример:

groups:
- name: instances
interval: 10s
rules:
- alert: InstanceDown
expr: up == 0
for: 1m
labels:
severity: critical
labelscount: "{{ len $labels }}"
annotations:
summary: "Instance {{ $labels.instance }} down"
description: "{{ $labels.instance }} of job {{ $labels.job }} has been down for 1 minute."

Во-первых, мы добавили новый тег labelscount, который будет содержать количество лейблов в исходной метрике. Происходит это благодаря встроенной функции len. Помимо этого мы указали в annotations два параметра - summary (заголовок алерта), в котором обращаемся к тегу instance и добавляем его в текст, а так же параметр desctiption (описание алерта) - в нем мы обращаемся к тегу instance и job. Работает вся эта магия благодаря go-templating.

Таким образом, когда под наше условие попадает любое количество метрик (ведь у нас может попасть под наш запрос несколько метрик с разными тегами), то prometheus, используя go-templating создаст несколько алертов с разными тегами и описанием. Давайте посмотрим на результат такой настройки:

ALERTS{alertname="InstanceDown", alertstate="firing", instance="localhost:8080", job="app", labelscount="3", severity="critical"} => 1 @[1622758792.223]

Как видно из вывода у нас появился тег labelscount, который имеет значение 3 - поскольку в исходной метрике у нас было три тега:

root@prom-test:/opt/prometheus# ./promtool query instant http://localhost:9090/ "up"
up{instance="localhost:8080", job="app"} => 0 @[1622758824.147]

Как видно из вывода тега всего лишь два - instance и job. А функция len выдала значение 3 (labelsname=3). Почему тега два, а len считает что их 3? Кто разгадает данную загадку и подтвердит ее, выведя все три тега в annotations.description - пишите на [email-masked] с темой письма: LABELSCOUNT:3 и примером настройки алерта с выводом всех тегов в аннотации - если вы окажетесь первым, то отправлю вам подарок :)

Возвращаясь к шаблонам стоит сказать что вы можете использовать всю мощь go templating и писать условия через {{ if }} и перебирать все теги через {{ range }} оператор. Ну и собственно все что захочется. Используйте данный инструмент - он очень гибкий и мощный.

Кстати, в выводе мы так и не увидели annotations - они передаются в алертменеджер и используются им для составления текста уведомлений - поэтому после такой настройки вы получите алерт примерно такого вида:

Instance localhost:8080 down
localhost:8080 of job app has been down for 1 minute

Подробнее об этом в следующем задании.

Итак, с алертами разобрались - осталось научиться их тестировать!

Тестирование правил алертинга

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

Для тестирования алертов необходимо создать отдельный файл с описанием тестовых кейсов. Давайте попробуем настроить тесты для нашего предыдущего примера:

# Путь к файлу с алертами
rule_files:
- alerts.yml

# Как часто будут выполняться запросы из тестовых правил
evaluation_interval: 10s

tests:
- interval: 15s # Как часто собираются данные по нашей метрике
input_series: # Тестовая метрика
- series: 'up{job="prometheus", instance="localhost:9090"}' # Название и теги тестовой метрики
values: '0 0 0 0 0 0 0 0 0 0 0 0 0 0 0' # Значения тестовой метрики - каждое значение по факту будет с
# обираться каждые interval секунд. То есть в нашем примере
# interval 15 sec, значит при тестах первое значение - ноль
# - будет собрано в самом начале, когда прошло 0 секунд,
# второе значение - ноль - будет собрано в 15 секунд,
# третье значение - ноль - появится в 30 секунд и так
# далее. Это важно, поскольку вам так же захочется
# тестировать крайние случаи - когда for и evaluation_interval
# и scrape_interval отличаются и алерт может не сработать.

# Тестовые правила
alert_rule_test:
- eval_time: 5m # Время прошедшее с момента запуска теста - за это
# время тесты соберут все значения метрики
# (раз в 15 секунд, а так же будут раз в 10 секунд
# выполнять наши правила алертинга)
alertname: InstanceDown # Названия алерта, которое тестируем
exp_alerts: # что ожидаем получить на выходе
- exp_labels: # Какие лейблы должны быть у алерта
severity: critical
instance: localhost:9090
job: prometheus
exp_annotations: # Какие аннотации должны быть у алерта
summary: "Instance localhost:9090 down"
description: "localhost:9090 of job prometheus has been down for 1 minute."

Файл достаточно хорошо прокомментирован - так что думаю тут не должно возникнуть больших проблем. Давайте попробуем запустить наши тесты:

root@prom-test:/opt/prometheus# ./promtool test rules test.yaml
Unit Testing: test.yaml
FAILED:
alertname:InstanceDown, time:5m,
exp:"[Labels:{alertname=\"InstanceDown\", instance=\"localhost:9090\", job=\"prometheus\", severity=\"critical\"} Annotations:{description=\"localhost:9090 of job prometh
eus has been down for 1 minute.\", summary=\"Instance localhost:9090 down\"}]",
got:"[Labels:{alertname=\"InstanceDown\", instance=\"localhost:9090\", job=\"prometheus\", labelscount=\"3\", severity=\"critical\"} Annotations:{desc
ription=\"localhost:9090 of job prometheus has been down for 1 minute.\", summary=\"Instance localhost:9090 down\"}]"

Как видно из вывода тест не прошел. А не прошел он по причине того, что в полученном алерте находится неизвестный тег labelscount=3, который мы добавляли для демонстрации go-templating. Давайте удалим его из alerts.yaml и повторим наш тест:

groups:
- name: instances
interval: 10s
rules:
- alert: InstanceDown
expr: up == 0
labels:
severity: critical
annotations:
summary: "Instance {{ $labels.instance }} down"
description: "{{ $labels.instance }} of job {{ $labels.job }} has been down for 1 minute."

Запускаем тесты:

root@prom-test:/opt/prometheus# ./promtool test rules test.yaml
Unit Testing: test.yaml
SUCCESS

Отлично! Тесты пройдены!

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

Стандартные алерты для систем

Итак, пришла пора показать вам абсолютно стандартные алерты для Linux, которые вы постоянно будете использовать в своей работе. на самом деле мониторить обычно надо 4 ресурса, которые являются основными - это CPU, RAM, Disk, Network.

Первый пример будет про место на диске:

- alert: HostOutOfDiskSpace
expr: (node_filesystem_avail_bytes * 100) / node_filesystem_size_bytes < 10 and ON (instance, device, mountpoint) node_filesystem_readonly == 0
for: 2m
labels:
severity: warning
annotations:
summary: Host out of disk space (instance {{ $labels.instance }})
description: "Disk is almost full (< 10% left)\n VALUE = {{ $value }}\n LABELS = {{ $labels }}"

В данном примере используется вся сила promql и go templating. В данном выражении используется два запроса: (node_filesystem_avail_bytes * 100) / node_filesystem_size_bytes < 10 и ON (instance, device, mountpoint) node_filesystem_readonly == 0

Первый запрос рассчитывает процент занятого места и сравнивает его с 10 процентами. Второй запрос проверяет что файловая система не находится в режиме readonly, что характерно для всяких системных ФС. Два запроса соеденены логической операцией and и составляют одно выражение (expression) алерта.

Тот же пример с местом на диске, но только, используя функцию predict_linear, которая предсказывает значение через 24 часа:

- alert: HostDiskWillFillIn24Hours
expr: (node_filesystem_avail_bytes * 100) / node_filesystem_size_bytes < 10 and ON (instance, device, mountpoint) predict_linear(node_filesystem_avail_bytes{fstype!~"tmpfs"}[1h], 24 * 3600) < 0 and ON (instance, device, mountpoint) node_filesystem_readonly == 0
for: 2m
labels:
severity: warning
annotations:
summary: Host disk will fill in 24 hours (instance {{ $labels.instance }})
description: "Filesystem is predicted to run out of space within the next 24 hours at current write rate\n VALUE = {{ $value }}\n LABELS = {{ $labels }}"

Этот алерт поможет вам точно не пропустить переполненное место на диске :)

Следующий алерт поможет вам быстро обнаружить сильно загруженные сервера:

- alert: HostHighCpuLoad
expr: 100 - (avg by(instance) (rate(node_cpu_seconds_total{mode="idle"}[2m])) * 100) > 80
for: 0m
labels:
severity: warning
annotations:
summary: Host high CPU load (instance {{ $labels.instance }})
description: "CPU load is > 80%\n VALUE = {{ $value }}\n LABELS = {{ $labels }}"

В данном случае пример достаточно интересный. Сначала мы берем rate от node_cpu_seconds_total за последние 2 минуты. Метрика node_cpu_seconds_total - это counter, который показывает использованное процессорное время. С помощью функции rate мы вытаскиваем значение загруженности процессора в единицу времени - в секунду. Так же мы берем только свободное процессорное время - на что указывает тег idle. От получившейся метрики мы берем среднее значение по тегу instance - это делается для того, чтобы вытащить среднее свободное процессорное время между ядрами. Если на одном процессоре много ядер - то мы просто берем среднее значение. Дальше мы умножаем его на 100 и получаем процент свободного ресурса процессора. Чтобы получить на сколько процессор загружен нам нужно из 100 вычесть это значение. Таким образом мы получаем процент загруженности процессора и сравниваем его с 80%. На самом деле вы так же можете посмотреть на метрику node_load - возможно с ней вам понравится работать больше.

Следующий алерт в списке - количество свободной памяти:

- alert: HostOutOfMemory
expr: node_memory_MemAvailable_bytes / node_memory_MemTotal_bytes * 100 < 10
for: 2m
labels:
severity: warning
annotations:
summary: Host out of memory (instance {{ $labels.instance }})
description: "Node memory is filling up (< 10% left)\n VALUE = {{ $value }}\n LABELS = {{ $labels }}"

Ну тут я думаю должно быть все понятно :)

И последний алерт в нашем списке - по сети:

- alert: HostUnusualNetworkThroughputIn
expr: sum by (instance) (rate(node_network_receive_bytes_total[2m])) / 1024 / 1024 > 100
for: 5m
labels:
severity: warning
annotations:
summary: Host unusual network throughput in (instance {{ $labels.instance }})
description: "Host network interfaces are probably receiving too much data (> 100 MB/s)\n VALUE = {{ $value }}\n LABELS = {{ $labels }}"

В данном примере алерт срабатывает если сетевой интерфейс принимает более 100 mb трафика в секунду. Вы всегда можете подкорректировать данное значение под свои требования.

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

Итоги

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

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

Полезные ссылки:

awesome prometheus alerts

go templating

alerts unit testing

alerting rules

understanding delays on alerts

Задание:

В этом задании вам будет необходимо настроить алерт на персентиль времени выполнения compaction внутри самого prometheus. На подготовленной виртуальной машине уже установлен Prometheus.

Критерии оценки выполнения задания:

Prometheus собирает метрики с самого себя (настроено по умолчанию).

В Prometheus подключены правила алертинга из файла /opt/prometheus/alerts.yml

В файле /opt/prometheus/alerts.yml описан алерт:

3.1. Название алерта - CompactionTimeTooLong.

3.2. Алерт должен проверять 95 персентиль для метрики prometheus_tsdb_compaction_duration_seconds_bucket и если он больше или равен 1 секунде - должен срабатывать.

3.3. Алерт должен срабатывать если он активен более 5 минут

3.4. Алерт должен иметь лейблы: severity=warning и env=dev

3.5. Алерт должен иметь summary вида: Compaction time on $INSTANCE equals $DATA. Вы должны заменить $INSTANCE на имя инстанса, а $DATA на значение функции из пункта 3.2 с помощью go-template.