Привет, Хабр! Меня зовут Сева, я работаю backend-разработчиком в Doubletapp, а также занимаюсь некоторыми devops-задачами. В этой статье я расскажу о мониторинге наших backend-приложений: сборе метрик, их визуализации и отправке уведомлений. Покажу примеры конфигов с подробными комментами и дам ссылки на гитхаб.
В Doubletapp мы занимаемся аутсорс-разработкой, к нам часто приходят новые проекты, а часть старых уходит в режим поддержки. Из-за этого у нас много сервисов, где активная разработка уже не ведется и которые развернуты на серверах клиентов. Мы не хотим узнавать о поломках от пользователей спустя несколько дней, поэтому нам нужна обзорная система, которая будет собирать метрики со всех проектов и отвечать на вопросы:
жив ли сервер/сервис;
как меняется RPS;
какое время ответа;
хватает ли памяти и CPU;
сколько места осталось на диске.
Для этого мы выбрали популярное решение: собираем метрики с помощью Prometheus, отображаем их на дашбордах в Grafana и отправляем алерты в Телеграм через Prometheus Alert Manager.
Prometheus
Приложения обычно развернуты в облачных сервисах, у каждого клиента своя инфраструктура, поэтому мы не можем воспользоваться service discovery, который позволяет Prometheus автоматически обнаруживать новые объекты: Docker-контейнеры на хосте или поды Kubernetes-кластера.
Вместо этого мы используем статическую конфигурацию. Редактировать конфиг приходится вручную, зато можно настроить каждый источник: указать интервал сбора метрик, эндпоинт, данные для авторизации и прочее.
Конфигурация Prometheus
prometheus/prometheus.yml:
# секция global указывает параметры для всех конфигураций сбора метрик
global:
# как часто запрашивать метрики по умолчанию
scrape_interval: 10s
# как часто пересчитывать правила для алертов (подробнее — в alerts.yml)
evaluation_interval: 10s
# список конфигураций сбора метрик
scrape_configs:
# название процесса, который собирает метрики
- job_name: example-service
static_configs:
# список доменов/ip, с которых будут собираться метрики
- targets:
- example.url.com
# дополнительные данные, которые добавятся к записи
labels:
# название проекта
instance_group: example_project
# окружение приложения
instance_env: test
# тип источника метрик
instance_type: service
# команда, ответственная за проект
team: backend
. . .
Чтобы добавить новый сервис, нужно изменить конфиг и перезапустить Prometheus. Можно включить перезагрузку конфигурации в рантайме, но мы просто перезапускаем Prometheus во время выкатки нового конфига через CI/CD.
Сбор метрик
В отличие от Graphite или InfluxDB, Prometheus сам ходит в сервисы и забирает из них телеметрию. Для того, чтобы это работало, сервис должен предоставить эндпоинт с информацией о своем состоянии в формате, понятном Prometheus, — это или их собственный формат, или OpenMetrics.
Обычно для этого используется exporter — демон, который смотрит в логи системы, готовит телеметрию и предоставляет ее по HTTP/S. Для большинства существующих сервисов уже написаны готовые экспортеры. В простом случае можно реализовать /metrics в самом приложении, например, если мы хотим проверить только то, что приложение запущено и готово обрабатывать запросы.
По умолчанию экспортеры работают по HTTP без авторизации, что позволяет любому желающему получить к ним доступ. Стоит озаботиться настройкой TLS и авторизации. Это можно сделать как с помощью настроек самого экспортера, так и прокси вроде Nginx.
Метрики приложений
На всех наших проектах мы используем Nginx для проксирования запросов к API, поэтому из его логов мы можем строить основные метрики. Есть официальный экспортер от самой Nginx Inc. Для стандартной версии Nginx можно получить информацию об активных соединениях и количестве обработанных запросов, но нам нужно больше информации, как минимум HTTP-коды ответов и время обработки запросов. Официальный экспортер может собирать эти данные из API Nginx, но она доступна только в версии Nginx Plus.
Для бесплатного Nginx также существует альтернативный экспортер — martin-helmich/prometheus-nginxlog-exporter. Чтобы его настроить, нужно указать путь и формат лога, который он будет парсить. Также нужно указать границы бакетов для гистограмм, чтобы можно было посмотреть, сколько времени заняла обработка определенной доли запросов. В дефолтном формате лога Nginx нет информации о времени ответа, поэтому его тоже поменяем.
Конфигурация Nginx
nginx/default.conf:
# формат лога
log_format timed_combined
'$remote_addr - $remote_user [$time_local] '
'"$request" $status $body_bytes_sent '
'"$http_referer" "$http_user_agent" '
'rt=$request_time uct="$upstream_connect_time" uht="$upstream_header_time" urt="$upstream_response_time"';
server {
# порт, который будет слушать Nginx
listen 80;
# переотправка запросов к API
location / {
# файл логов
access_log /var/log/nginx/app.log timed_combined;
proxy_pass http://app:80/;
. . .
}
# переотправка запросов на /metrics к экспортеру
location /metrics {
# логирование запросов отключено, чтобы не портить метрики
access_log off;
proxy_pass http://nginx-exporter:4040/metrics;
. . .
}
}
Конфигурация Nginx-exporter
nginx-exporter/config.hcl:
listen {
# порт, который будет слушать экспортер
port = 4040
# эндпоинт для получения метрик
metrics_endpoint = "/metrics"
}
namespace "nginx" {
# формат лога Nginx для парсинга
format = "$remote_addr - $remote_user [$time_local] \"$request\" $status $body_bytes_sent \"$http_referer\" \"$http_user_agent\" rt=$request_time uct=\"$upstream_connect_time\" uht=\"$upstream_header_time\" urt=\"$upstream_response_time\""
# границы бакетов для гистограмм
histogram_buckets = [.005, .01, .025, .05, .1, .25, .5, 1, 2.5, 5, 10]
# источник логов
source {
files = ["/var/log/nginx/app.log"]
}
}
По ссылке — пример приложения с Nginx и экспортером.
Метрики серверов
Также важно собирать метрики с серверов, на которых приложения запущены, — следить за нагрузкой на ЦПУ, расходованием оперативной памяти и диска. Для сбора этих метрик есть официальный экспортер от Prometheus — prometheus/node_exporter. Он запускается как демон и периодически собирает метрики из системных файлов и интерфейсов.
Ссылка на проект с экспортером.
Дашборды
Дашборд в Grafana — это инструмент для визуализации метрик в реальном времени. Дашборд состоит из панелей, каждая из которых отображает определенный набор данных, например в виде графика или таблицы. Можно использовать готовые дашборды или собрать их с нуля в веб-интерфейсе. Для создания панели нужно написать запрос данных на PromQL и настроить их представление.
В Grafana мы сделали 3 дашборда: главный, на котором собраны все приложения и серверы, дашборд конкретного приложения и дашборд конкретного сервера.
Главный дашборд
На главном находятся таблицы серверов и приложений с основными метриками: состояние (up или down), использование процессора, памяти и диска для сервера и RPS, статус, коды и время ответа для приложения. Он позволяет быстро оценить состояние всех наших проектов. Таблица состоит из отдельных панелей для серверов и приложений, они разделены на продовые и тестовые. Можно перейти на дашборд конкретного сервера/приложения, если кликнуть на его название или одну из метрик.
Таблица серверов
PromQL запросы, использующиеся в таблице серверов
State
up{instance_type="node", instance_env="prod"}
CPU Used
100 * (1 - avg by(instance)(irate(node_cpu_seconds_total{mode='idle', instance_env="prod"}[1m])))
Memory Available
avg by(instance)(node_memory_MemAvailable_bytes{instance_env="prod"})
Memory Total
avg by(instance)(node_memory_MemTotal_bytes{instance_env="prod"})
Disk Available
avg by(instance)(node_filesystem_avail_bytes{mountpoint='/', instance_env="prod"})
Disk Total
avg by(instance)(node_filesystem_size_bytes{mountpoint='/', instance_env="prod"})
Таблица приложений
PromQL запросы, использующиеся в таблице приложений
State
up{instance_type="service"}
RPS
sum by(instance)(rate(nginx_http_response_count_total{status=~'...'}[24h]))
RP24H
floor(sum by(instance)(increase(nginx_http_response_count_total{status=~'...'}[24h])))
2хх
floor(sum by(instance)(increase(nginx_http_response_count_total{status=~'2..'}[24h])))
95th
histogram_quantile(0.95, sum by (instance,le) (rate(nginx_http_upstream_time_seconds_hist_bucket{status='200'}[10m]))) * 1000
Дашборд приложения
На странице приложений — состояние, RPS, количество и коды ответов, время ответа.
PromQL запросы, использующиеся для построения панелей
Панель состояния
up{instance=~'$instance:.+'}
Панель RPS
sum(rate(nginx_http_response_count_total{instance=~'$instance:.+'}[1m]))
Метрика
nginx_http_response_count_total{instance=~'$instance:.+',status='200'}
содержит информацию о количестве обработанных HTTP-запросовЗапрашиваются средняя скорость роста на отрезках по 1 минуте для этой метрики
(rate(...)[1m])
Считается сумма по всем HTTP-методам и статусам ответов
sum(...)
Панель запросов за 24 часа
floor(sum(increase(nginx_http_response_count_total{instance=~'$instance:.+'}[24h])))
Метрика
nginx_http_response_count_total{instance=~'$instance:.+',status='200'}
содержит информацию о количестве обработанных HTTP-запросовДля количества обработанных HTTP-запросов считается их количественное увеличение за 24 часа
(increase(...)[24h])
Считается сумма по всем HTTP-методам и статусам ответов
(sum(...))
и округляется до целогоfloor(...)
Панель запросов по статусам ответа за 24 часа
floor(sum(increase(nginx_http_response_count_total{instance=~'$instance:.+',status=~'2..'}[24h])))
Запрос аналогичен предыдущему, но данные запрашиваются по каждому типу ответов.
Панель времени ответов на разных перцентилях
histogram_quantile(0.95, sum by (le) (rate(nginx_http_upstream_time_seconds_hist_bucket{instance=~'$instance:.+',status='200'}[10m]))) * 1000
Панель графика запросов в секунду
sum(rate(nginx_http_response_count_total{instance=~'$instance:.+',status=~'...'}[1m]))
Панель графика запросов по статусам ответа
floor(sum(increase(nginx_http_response_count_total{instance=~'$instance:.+',status=~'2..'}[1m])))
Панель графика 4хх запросов
floor(sum by (status) (increase(nginx_http_response_count_total{instance=~'$instance:.+',status=~'4..'}[1m])))
Панель графика 5хх запросов
floor(sum by (status) (increase(nginx_http_response_count_total{instance=~'$instance:.+',status=~'5..'}[1m])))
Панель графика времени ответов на разных перцентилях
histogram_quantile(0.99, sum by (le) (rate(nginx_http_upstream_time_seconds_hist_bucket{instance=~'$instance:.+',status='200'}[10m]))) * 1000
Метрика
nginx_http_upstream_time_seconds_hist_bucket{instance=~'$instance:.+',status='200'} содержит
информацию о распределении времени, необходимом приложению для обработки запросов с успешными ответами, по бакетам гистограммы.Запрашиваются средняя скорость роста на отрезках по 10 минут для этой метрики
(rate(...)[10m])
Строится несколько графиков на разных перцентилях (0.5, 0.75, 0.9, 0.95, 0.99)
histogram_quantile(n, sum by (le)(...)
Дашборд сервера
На странице сервера — графики нагрузки на ЦПУ, расходования памяти и диска сервера.
PromQL запросы, использующиеся для построения панелей
Обзорная панель
CPU Used
100 * (1 - avg by(instance)(irate(node_cpu_seconds_total{mode='idle',instance=~'$instance.*'}[1m])))
Метрика
node_cpu_seconds_total{mode='idle',instance=~'$instance.'}
содержит информацию о распределении времени, которое процессор проводит состоянии бездействияВычисляется посекундная мгновенная скорость увеличения временного ряда на отрезках по 10 минут для этой метрики
(irate(...)[10m])
Из этого значения вычисляется процентное соотношение времени, которое процессор проводит в любом состоянии, кроме бездействия
(100(1 - …))
Memory Available
(node_memory_MemAvailable_bytes{instance=~'$instance.*'}) / (node_memory_MemTotal_bytes{instance=~'$instance.*'}) * 100
Процентное соотношение свободной памяти к общей.
Disk Available
(node_filesystem_avail_bytes{instance=~'$instance.*', mountpoint='/'}) / (node_filesystem_size_bytes{instance=~'$instance.*', mountpoint='/'}) * 100
Процентное соотношение свободного дискового пространства к общему.
Панель памяти
График свободной памяти
node_memory_MemAvailable_bytes{instance=~'$instance.*'}
График использованной памяти
node_memory_MemTotal_bytes{instance=~'$instance.*'} - node_memory_MemAvailable_bytes{instance=~'$instance.*'}
Панель настроена так, что графики отображены друг над другом.
Панель дискового пространства
График свободного дискового пространства
node_filesystem_avail_bytes{instance=~'$instance.*', mountpoint='/'}
График использованного дискового пространства
node_filesystem_size_bytes{instance=~'$instance.*', mountpoint='/'} - node_filesystem_avail_bytes{instance=~'$instance.*', mountpoint='/'}
Панель настроена аналогично панели памяти.
Панель CPU
100 * (1 - avg by(instance)(irate(node_cpu_seconds_total{mode='idle',instance=~'$instance.*'}[1m])))
Запрос PromQL аналогичен запросу CPU Used в обзорной панели, только данные визуализируются как график.
Панель CPU Cores
100 * (1 - (irate(node_cpu_seconds_total{mode='idle',instance=~'$instance.*'}[1m])))
Запрос PromQL похож на запрос CPU Used в обзорной панели, но отсутствует агрегация по instance. Это дает данные о нагрузке на каждое ядро ЦПУ.
Панель CPU Cores Usage
100 * (1 - (irate(node_cpu_seconds_total{mode='idle',instance=~'$instance.*'}[1m])))
Запрос PromQL аналогичен предыдущему, только данные визуализируются как график.
Данные, которые мы собираем со всех серверов и приложений, одинаковые, поэтому дашборды для их отображения мы используем одни и те же, только меняем переменную Instance, значения которой подтягиваются из Prometheus.
В качестве локальной базы данных для хранения конфигурации (дашборды, пользователи и т. д.) в Grafana поднят sqlite3. Но дашборды удобно хранить в формате JSON в репозитории проекта для развертывания Grafana на новом сервере. Экспортировать их можно на странице самого дашборда, а импортировать — в меню Grafana.
Ссылка на проект с Prometheus+Grafana — тут.
Уведомления
Без оповещений о критических ситуациях можно узнать, только если весь день смотреть в монитор, а это крайне неэффективный способ обнаружения проблем и довольно скучное времяпрепровождение. Узнать о проблемах в момент их возникновения поможет механизм алертов.
Правила уведомлений настраиваются в Prometheus. Правило — это PromQL выражение, которое должно стать истинным, чтобы активировать алерт. Prometheus вычисляет эти выражения в цикле с настроенной периодичностью.
У нас уведомления настроены на основные критические ситуации:
Приложение/сервер не работает. В течение 5 минут метрика up находится в значении 0.
На сервере мало свободной памяти/диска. Отношение значений занятого объема к общему больше 95 процентов.
На сервере высокое потребление ЦПУ. Время, которое процессор проводит в любом состоянии, кроме бездействия, составляет больше 80 процентов.
Приложение недоступно. Число ответов 502 Bad Gateway прокси-сервера возрастает в течение 3 минут.
Приложение возвращает 5хх-е коды ответов. За последнюю минуту вернулся 5хх-й ответ.
Активные алерты (то есть те, для которых условие стало истинным) шлются в Alertmanager, где они перенаправляются в каналы связи. Он отправляет уведомления в различные каналы связи, такие как электронная почта или мессенджеры, есть много интеграций, которые поддерживаются из коробки. Также можно сделать переотправку на вебхуки. Они могут помочь в интегрировании каналов связи, для которых нет готовых интеграций. Например, если вы хотите узнавать о высокой нагрузке на ваш сервер в своем паблике во «ВКонтакте».
Конфигурация алертов Prometheus
prometheus/alerts.yml:
groups:
- name: default
rules:
# название алерта
- alert: NodeLowMemory
# выражение PromQL для вычисления
expr: node_memory_MemAvailable_bytes / node_memory_MemTotal_bytes <= 0.05
# алерт считается активным, если выражение expr верно в течение этого времени
for: 5m
# дополнительные данные для алерта
labels:
severity: critical
annotations:
# текстовое описание
summary: "Node {{ $labels.instance }} is low on RAM for more than 5 minutes"
Настройка алертов и их отправки в основном конфиге Prometheus
prometheus/prometheus.yml:
. . .
# список файлов с описанием алертов
rule_files:
- alerts.yml
# опции, связанные с Alertmanger. Протокол, префикс URL, адрес.
alerting:
alertmanagers:
- scheme: http
path_prefix: /alertmanager
static_configs:
- targets: [ "alertmanager:9093" ]
Для оповещения мы используем Телеграм. Хотя у Alertmanager есть свой конфиг специально для Телеграма, для более гибкого форматирования сообщений у нас запущено приложение-вебхук. Alertmanager отправляет POST-запросы с данными об активных алертах в формате JSON по URL типа /alert/{id} (id — идентификатор телеграм-канала) в webhook-receiver, тот их форматирует и отправляет сообщения в Телеграм.
Для каждого проекта мы создаем отдельные телеграм-каналы. Кроме того, есть канал об инфраструктурных серверах и приложениях. Ответственные за проект люди — разработчики, devops-инженеры и менеджеры — подписываются на соответствующий канал, чтобы получать уведомления о проблемах и оперативно на них реагировать.
Конфигурация Alertmanager
alertmanager/alertmanager.yml:
# параметры, которые будут унаследованы всеми определенными в routes нодами маршрутизации
route:
group_wait: 0s
group_interval: 1s
repeat_interval: 4h
group_by: [...]
# ноды маршрутизации алертов
routes:
# параметры, по которым выбирается уведомление
- match:
instance_group: example_group
# название обработчика, куда будет отправлено уведомление
receiver: example-receiver
# список обработчиков уведомлений
receivers:
# название обработчика
- name: example-receiver
webhook_configs:
# уведомлять ли о разрешенных уведомлениях
- send_resolved: True
# куда будет отправлен POST-запрос с уведомлением
url: http://webhook-receiver:3000/alert/<tg-channel-id>
В этой статье я рассказал о том, как мы в Doubletapp собираем метрики с помощью Prometheus, визуализируем их в Grafana и отправляем уведомления через Alertmanager в Телеграм.
Буду рад ответить на ваши вопросы и подискутировать в комментариях.
Ссылки на репозитории с проектами — Prometheus+Grafana, экспортер для серверов, шаблон приложения с Nginx-экспортером.