Prometheus + Grafana с нуля: практичный дашборд для мониторинга серверов небольшой компании
Zabbix у нас стоит почти на всех клиентских контурах и хорошо ловит инциденты по триггерам. Но когда клиент спрашивает «а хватит ли нам диска на файловом сервере через полгода» или «почему сервер 1С подтормаживает по вторникам к обеду», нужна не лампочка «красный-зелёный», а история метрик за месяцы с возможностью покрутить график руками. Для этого мы поднимаем второй, лёгкий слой — Prometheus и Grafana. Рассказываю, как именно мы это делаем: с какими версиями, конфигами и порогами.
Зачем директору IT-аутсорсинга ещё один мониторинг, если уже есть Zabbix
У нас в компании Zabbix 7.0 — рабочая лошадка на большинстве клиентских стендов: он умеет триггеры, эскалации, SNMP по свитчам и коммутируемым ИБП, и мы давно выстроили под него шаблоны. Но у Zabbix есть слабое место, которое проявляется именно в разговоре с руководителем клиента: агрегация истории. Item history hold по умолчанию короткий, тренды хранятся по часам, а не по секундам, и когда нужно показать не «упало-не упало», а плавную кривую нагрузки диска за три месяца с разрешением в минуту — приходится выкручиваться.
Prometheus в этой роли работает иначе: он тянет метрики с интервалом 15 секунд, хранит их как временной ряд с полным разрешением на весь срок retention, и любой график — это не заранее посчитанный тренд, а PromQL-запрос по сырым данным. Для клиента до 50 рабочих мест это означает конкретную вещь: когда мы приходим с разговором «через 4 месяца упрётесь в диск на 1С-сервере», у нас на экране реальная линейная экстраполяция роста node_filesystem_avail_bytes, а не оценка на глаз.
Мы не заменяем Zabbix Prometheus-ом — они решают разные задачи. Zabbix остаётся системой алертинга широкого профиля (SNMP, службы Windows, лог-файлы), а Prometheus плюс Grafana — это наш инструмент для capacity planning и визуальной диагностики нагрузки: диски, память, сеть, CPU. На части клиентских контуров (там, где серверов больше 3-4 и клиент готов платить за более глубокую аналитику) мы ставим оба слоя параллельно, они друг другу не мешают — используют разные агенты и разные порты.
Архитектура нашего стека: четыре компонента и кто кому что отдаёт
Минимальный рабочий стек, который мы разворачиваем на одном небольшом сервере-«коллекторе» (обычно это отдельная VM на 2 vCPU / 4 ГБ RAM / 40 ГБ диска, если серверов под наблюдением до 10-15), состоит из четырёх процессов. Собираем их не в Docker, а нативными бинарниками под systemd — на этом отдельно остановлюсь ниже.
| Компонент | Роль | Порт | systemd-юнит | Версия на июль 2026 |
|---|---|---|---|---|
| Prometheus | Тянет (scrape) метрики по HTTP, хранит TSDB, считает alerting rules | 9090 | prometheus.service | 3.x |
| node_exporter | Отдаёт метрики ОС хоста (CPU, память, диски, сеть, файловые системы) | 9100 | node_exporter.service | 1.x |
| Grafana | Дашборды, панели, шаблонные переменные, алерт-визуализация | 3000 | grafana-server.service | 12.x |
| Alertmanager | Группировка, дедупликация и маршрутизация алертов от Prometheus в Telegram/почту | 9093 | alertmanager.service | 0.2x |
Точные минорные версии мы фиксируем на дату внедрения по официальным release-страницам — здесь важнее показать состав стека, чем номер сборки. Модель работы простая и однонаправленная: node_exporter стоит на каждом наблюдаемом сервере и пассивно отдаёт метрики в текстовом формате Prometheus по адресу /metrics. Сам Prometheus сидит на сервере-коллекторе и с заданным интервалом ходит («scrape») ко всем node_exporter-ам, забирая срез метрик. Он же вычисляет alerting rules и, если условие сработало, отправляет алерт в Alertmanager, который решает — сгруппировать, подождать, отправить в Telegram-бота или на почту. Grafana ничего не собирает сама — она ходит в Prometheus как data source и рисует то, что запрошено через PromQL.
Установка: почему мы ставим бинарники, а не тянем готовый Docker-compose
На чужом железе клиента лишний слой контейнеризации — это ещё один процесс, который надо мониторить, обновлять и объяснять клиентскому админу при передаче на сопровождение. Поэтому наш стандарт — systemd-юниты поверх обычных бинарников с отдельным непривилегированным пользователем.
Коротко порядок действий на сервере-коллекторе (Debian 12 / Ubuntu 22.04-24.04):
- Создаём системного пользователя без шелла:
useradd --no-create-home --shell /usr/sbin/nologin prometheus. - Разворачиваем бинарник в
/usr/local/bin/prometheus, конфиг — в/etc/prometheus/prometheus.yml, данные TSDB — в/var/lib/prometheus, права всюду на пользователяprometheus. - Флаги запуска, которые мы всегда прописываем явно в systemd-юните:
--config.file=/etc/prometheus/prometheus.yml --storage.tsdb.path=/var/lib/prometheus --storage.tsdb.retention.time=90d --web.listen-address=127.0.0.1:9090. Слушать наружу без обратного прокси и аутентификации мы Prometheus не даём принципиально — у него из коробки нет built-in аутентификации, поэтому порт 9090 либо только на loopback, либо за nginx с basic_auth и TLS. - На каждом наблюдаемом сервере — тот же паттерн для node_exporter, но слушает он на
0.0.0.0:9100(или на внутреннем VLAN-адресе, если сеть сегментирована), потому что к нему приходит Prometheus снаружи этого хоста.
Базовый prometheus.yml, с которого мы стартуем на любом новом клиентском стенде, выглядит так:
global:
scrape_interval: 15s
evaluation_interval: 15s
rule_files:
- "/etc/prometheus/rules/*.yml"
alerting:
alertmanagers:
- static_configs:
- targets: ["127.0.0.1:9093"]
scrape_configs:
- job_name: "prometheus"
static_configs:
- targets: ["127.0.0.1:9090"]
- job_name: "node"
scrape_interval: 15s
static_configs:
- targets:
- "10.20.0.11:9100"
- "10.20.0.12:9100"
- "10.20.0.13:9100"
labels:
env: "prod"
Интервал в 15 секунд — не догма из документации, а наш практический выбор: для дашборда нагрузки сервера этого достаточно, а более частый опрос (5-10 с) на слабом коллекторе создаёт лишнюю нагрузку на диск TSDB без ощутимой пользы в визуализации. Если серверов под наблюдением больше 20-25, мы разносим их по нескольким job_name с метками env и role, чтобы потом фильтровать в Grafana по одной переменной, а не городить десяток отдельных дашбордов.
node_exporter: какие метрики мы реально выводим на дашборд, а какие оставляем в тени
node_exporter отдаёт несколько сотен метрик из коробки — по CPU, памяти, дискам, сети, файловым системам, systemd-юнитам, температуре, если доступна. На практике для дашборда «здоровье сервера небольшой компании» нам хватает десятка. Вот с чем мы реально работаем каждый день:
| Метрика node_exporter | Что показывает | Наш практический порог |
|---|---|---|
| node_filesystem_avail_bytes / node_filesystem_size_bytes | Свободное место на файловой системе (по label mountpoint) | Тревога при заполнении > 85%, критично > 92% |
| node_memory_MemAvailable_bytes / node_memory_MemTotal_bytes | Реально доступная память (учитывает кэш и буферы, а не «свободную» в лоб) | Тревога при доступности < 10% в течение 10 минут |
| node_cpu_seconds_total{mode=...} | Счётчик времени CPU по режимам (idle, user, system, iowait, steal) | Средняя загрузка > 85% на протяжении 15 минут — повод разбираться |
| node_load1 / node_load5 / node_load15 | Load average в привычном виде из top | Сравниваем с числом ядер; load1 > 1.5×nproc — тревога |
| node_disk_io_time_seconds_total | Сколько времени диск был занят операциями ввода-вывода (счётчик, «занятость» диска) | rate > 0.9 (то есть диск занят 90%+ времени) — узкое место |
| node_network_receive_bytes_total / transmit_bytes_total | Счётчики трафика по интерфейсу (label device) | Смотрим тренд, жёсткого порога нет — зависит от канала клиента |
| node_boot_time_seconds | Время последней загрузки — считаем аптайм как time() минус эта метрика | Неожиданный сброс — повод проверить, не было ли аварийной перезагрузки |
| node_systemd_unit_state | Состояние systemd-юнита (label state: active/failed/...) | state="failed" для критичных юнитов (nginx, postgresql, srv1cv8) — мгновенная тревога |
Отдельно скажу про node_disk_io_time_seconds_total — эта метрика недооценена в большинстве шаблонных дашбордов, а у нас как раз она чаще всего первой показывает деградацию: если у клиента 1С-сервер на медленном RAID и вечером бухгалтерия жалуется на тормоза, именно занятость диска (а не CPU и не память) первой уходит в полку. Отдельно держим на дашборде i/o wait из node_cpu_seconds_total{mode="iowait"} — рост этой доли при невысоком CPU usage почти всегда означает упирание в диск, а не в процессор.
Настроим мониторинг серверов вашей компании под ключ
Разворачиваем Prometheus, node_exporter, Grafana и Alertmanager на вашей инфраструктуре: пороги алертов под ваши реальные сервисы, дашборд с историей нагрузки за месяцы и уведомления в Telegram вместо разбора логов постфактум. Подходит компаниям до 50 рабочих мест с 1-2 серверами и больше.
PromQL: как считать нагрузку правильно, а не приблизительно
Ключевая ошибка, которую мы видели у клиентов, пытавшихся сами настроить Prometheus «по гайду из интернета» — рисовать графики напрямую по счётчикам (counter) без функции rate(). Счётчик в Prometheus только растёт (и сбрасывается в 0 при рестарте процесса), поэтому голое число ни о чём не говорит — важна скорость роста за окно времени. Вот запросы, которые реально стоят у нас в панелях:
Загрузка CPU в процентах (по инстансу):
100 - (avg by (instance) (rate(node_cpu_seconds_total{mode="idle"}[5m])) * 100)
Заполненность диска в процентах (исключая псевдо-ФС):
100 - (node_filesystem_avail_bytes{fstype!~"tmpfs|overlay|squashfs"} / node_filesystem_size_bytes{fstype!~"tmpfs|overlay|squashfs"} * 100)
Использование памяти в процентах (через MemAvailable, а не MemFree):
(1 - (node_memory_MemAvailable_bytes / node_memory_MemTotal_bytes)) * 100
Сетевой трафик на интерфейсе в мегабитах в секунду:
rate(node_network_receive_bytes_total{device="eth0"}[5m]) * 8 / 1000000
Окно [5m] в rate() мы выбрали не случайно: при scrape_interval 15 секунд в окно попадает 20 точек, этого достаточно, чтобы функция не «дёргалась» на единичном пропущенном скрейпе, но при этом график остаётся достаточно отзывчивым для дашборда реального времени. Для панелей, где важна именно мгновенная реакция (например, всплеск сетевого трафика при бэкапе), используем irate() — она берёт только две последние точки в окне, более резкая, но более шумная; на постоянно обновляемом дашборде мы её не ставим, только в разовых расследованиях через Prometheus Explore.
Ещё один момент: MemAvailable в ядре Linux уже учитывает то, что кэш страниц и буферы можно освободить под нужды приложений — это принципиально отличается от простого MemFree, которое почти всегда выглядит пугающе низким на любом сервере с приличным аптаймом просто потому, что ОС активно кэширует диск. Мы через это объясняем клиентам, почему «памяти мало» на графике top — это неправда, если считать честно.
Дашборд в Grafana: от provisioning датасорса до готовых панелей
Датасорс мы никогда не добавляем руками через UI на боевом стенде — заводим через provisioning-файл, чтобы при пересоздании VM или переносе на другой сервер конфигурация поднималась автоматически вместе с systemd-юнитом Grafana. Файл лежит в /etc/grafana/provisioning/datasources/prometheus.yml:
apiVersion: 1
datasources:
- name: Prometheus
type: prometheus
access: proxy
url: http://127.0.0.1:9090
isDefault: true
jsonData:
timeInterval: 15s
httpMethod: POST
Режим access: proxy означает, что запросы к Prometheus идут через backend самой Grafana, а не напрямую из браузера клиента — это важно, потому что порт 9090 у нас и так закрыт наружу, а Grafana проксирует всё через себя на своём же 3000-м порту, за которым уже стоит nginx с TLS и базовой авторизацией (или SSO, если у клиента есть).
Сам дашборд мы собираем не с нуля каждый раз, а держим свой JSON-шаблон («ITfresh Base Server Dashboard»), который накатываем через provisioning дашбордов, а потом донастраиваем под конкретного клиента. Ключевая деталь — шаблонная переменная $instance, которую строим запросом label_values(node_uname_info, instance): она даёт выпадающий список всех серверов под наблюдением, и весь дашборд переключается на нужный сервер без копирования панелей.
| Панель | Тип визуализации | Что показывает |
|---|---|---|
| CPU / Load | Time series (две оси) | Занятость CPU в % и load average рядом с числом ядер |
| Memory | Time series + Stat (текущее значение) | Используемая память в % и в абсолютных ГБ |
| Disk usage | Bar gauge по mountpoint | Заполненность каждой смонтированной файловой системы |
| Disk I/O | Time series | Занятость диска (io_time rate) и iowait CPU рядом |
| Network | Time series (receive/transmit разными цветами) | Трафик по интерфейсам в Мбит/с |
| Uptime / Services | Stat + таблица | Аптайм и состояние критичных systemd-юнитов |
На каждой панели мы включаем threshold-заливку (жёлтый на 70%, красный на 90% для дисков и памяти) — это визуальный аналог порогов из alerting rules, чтобы клиент, зашедший в дашборд сам, без объяснений понимал, где горит.
Alertmanager: пороги, которые мы реально готовы получать ночью
Правила тревог мы храним отдельным файлом /etc/prometheus/rules/node.yml, который Prometheus подхватывает через rule_files из основного конфига и переоценивает каждые evaluation_interval — у нас это те же 15 секунд.
groups:
- name: node-alerts
rules:
- alert: HighDiskUsage
expr: 100 - (node_filesystem_avail_bytes{fstype!~"tmpfs|overlay"} / node_filesystem_size_bytes{fstype!~"tmpfs|overlay"} * 100) > 85
for: 10m
labels:
severity: warning
annotations:
summary: "Диск {{ $labels.mountpoint }} на {{ $labels.instance }} заполнен на {{ $value | printf \"%.1f\" }}%"
- alert: HighMemoryUsage
expr: (1 - (node_memory_MemAvailable_bytes / node_memory_MemTotal_bytes)) * 100 > 90
for: 10m
labels:
severity: page
annotations:
summary: "Память на {{ $labels.instance }} заполнена свыше 90% дольше 10 минут"
- alert: ServiceFailed
expr: node_systemd_unit_state{state="failed"} == 1
for: 2m
labels:
severity: page
annotations:
summary: "Юнит {{ $labels.name }} в состоянии failed на {{ $labels.instance }}"
Обратите внимание на for: 10m у дисков и памяти — мы сознательно не будим никого на единичном всплеске: у 1С и SQL Server бывают легитимные кратковременные пики потребления памяти при построении отчётов, и алерт без выдержки времени превращается в шум, на который через неделю никто не реагирует. А вот ServiceFailed с for: 2m — это уже про упавшую службу, здесь долго ждать нельзя.
Дальше Prometheus шлёт сработавший алерт в Alertmanager, а тот решает — как сгруппировать и куда доставить. Наш alertmanager.yml:
route:
receiver: default-telegram
group_by: ["alertname", "instance"]
group_wait: 30s
group_interval: 5m
repeat_interval: 4h
routes:
- matchers:
- severity="page"
receiver: urgent-telegram
repeat_interval: 1h
receivers:
- name: default-telegram
webhook_configs:
- url: "http://127.0.0.1:9095/hook/warning"
- name: urgent-telegram
webhook_configs:
- url: "http://127.0.0.1:9095/hook/urgent"
group_wait: 30s — сколько Alertmanager ждёт перед первой отправкой, чтобы собрать в одно уведомление сразу несколько связанных алертов (например, если легло сразу три файловые системы на одном сервере — придёт одно сообщение, а не три). group_interval: 5m — минимальный интервал между уведомлениями по уже существующей группе, если появились новые алерты внутри неё. repeat_interval — как часто повторять напоминание, если проблема не разрешилась; для critical («page») мы срезаем его до часа, чтобы не потерять «висящую» аварию в потоке обычных уведомлений. За доставку в Telegram отвечает у нас небольшой self-hosted webhook-бридж — Alertmanager нативно шлёт email, PagerDuty, Slack, OpsGenie, Webex и generic webhook, поэтому телеграм-нотификации всегда идут именно через webhook_configs, а не «из коробки».
Грабли, на которых мы уже успели споткнуться сами
Несколько практических моментов, до которых лучше додуматься заранее, а не после инцидента:
- Retention и место на диске. По умолчанию Prometheus хранит данные 15 суток (
--storage.tsdb.retention.time). Для дашборда, который должен показывать тренд за квартал, мы сразу ставим 90 суток и заранее считаем диск: чем больше активных временных рядов (метрика + уникальный набор label) и чем чаще скрейп, тем быстрее растёт TSDB. Для 10-15 серверов с нашим набором метрик 90 суток укладываются в 15-20 ГБ — но это наша оценка по практике, не цифра из документации, и её стоит перепроверять на конкретном клиенте после недели работы. - Взрыв кардинальности (cardinality). Самая частая ошибка новичков — добавить в label что-то с высокой уникальностью, например ID процесса или временную метку. Каждая уникальная комбинация меток — это отдельный ряд в TSDB, и если переусердствовать, Prometheus начинает есть память быстрее, чем растёт польза от графиков. Мы жёстко ограничиваем набор меток на node_exporter стандартным: instance, device, mountpoint, fstype, mode — и не добавляем ничего своего без крайней необходимости.
- node_exporter слушает 0.0.0.0 — закрывайте это фаерволом. На метриках node_exporter нет built-in аутентификации в базовой поставке, поэтому мы всегда ограничиваем доступ к порту 9100 либо через файрвол (только с IP сервера-коллектора), либо через внутренний VLAN, никогда не открываем его наружу.
- textfile collector для своих проверок. Когда клиенту важна метрика, которой нет из коробки (например, возраст последнего бэкапа), мы не пишем свой exporter, а используем встроенный
--collector.textfile.directory: обычный cron-скрипт раз в 5 минут кладёт файл*.promс нужными строками метрик, а node_exporter сам их подхватывает и отдаёт вместе с остальными. - Не дублируйте job_name с одинаковыми target-ами. Если один и тот же адрес попадает в два job одновременно, в Grafana начинают дублироваться серии на графике, и легенда «расползается» — типичная причина, почему свежий дашборд выглядит криво на старте.
Куда двигаем эту схему дальше
На стендах с внешними веб-сервисами клиента (сайт, личный кабинет, API) мы добавляем к этому стеку Blackbox Exporter — он умеет проверять HTTP/HTTPS/TCP/DNS снаружи и отдаёт те же Prometheus-метрики о доступности и времени ответа, что логично встраивается в тот же дашборд рядом с нагрузкой сервера. Для клиентов с несколькими площадками рассматриваем remote_write в центральный Prometheus или Mimir, чтобы видеть все объекты в одной Grafana без захода в отдельные инстансы. Но это уже следующий уровень зрелости — фундамент, который описан выше (Prometheus плюс node_exporter плюс Grafana плюс Alertmanager на честных systemd-юнитах, с продуманными PromQL-запросами и порогами с выдержкой времени), закрывает 90% практических задач мониторинга сервера небольшой компании и остаётся у нас базовым шаблоном для новых клиентских стендов.
Частые вопросы
- Prometheus и Grafana заменяют Zabbix или дополняют его?
- В нашей практике — дополняют. Zabbix остаётся системой алертинга широкого профиля (SNMP по сетевому оборудованию, службы Windows, лог-мониторинг), а Prometheus плюс Grafana мы разворачиваем как отдельный слой для истории нагрузки с высоким разрешением и capacity planning. Они работают на разных портах и не конфликтуют.
- Сколько ресурсов съедает такой стек на сервере-коллекторе?
- Для 10-15 наблюдаемых серверов с описанным набором метрик и retention 90 суток по нашей практике хватает 2 vCPU, 4 ГБ RAM и 20-30 ГБ диска под TSDB — это оценка по опыту внедрений, а не гарантированная цифра из документации, и её стоит уточнять после первой недели работы на конкретном стенде.
- На сколько по умолчанию хранятся метрики в Prometheus?
- По умолчанию флаг storage.tsdb.retention.time равен 15 суткам. Мы почти всегда увеличиваем его до 90 суток при установке, чтобы дашборд показывал сезонность и тренд роста нагрузки, а не только последние две недели.
- Нужно ли закрывать порт node_exporter (9100) от внешнего мира?
- Обязательно. У node_exporter нет встроенной аутентификации в базовой поставке, поэтому мы всегда ограничиваем доступ к порту 9100 файрволом — только с адреса сервера-коллектора Prometheus, либо изолируем его во внутреннем VLAN без выхода наружу.
- Можно ли получать алерты в Telegram, а не только по почте?
- Да, но напрямую Alertmanager этого не умеет — у него из коробки есть email, Slack, PagerDuty, OpsGenie, Webex и generic webhook. Мы ставим небольшой self-hosted webhook-бридж, который принимает webhook от Alertmanager и пересылает сообщение ботом в нужный чат Telegram.