Prometheus + Grafana с нуля: практичный дашборд для мониторинга серверов небольшой компании
Zabbix стоит у нас почти на каждом клиентском контуре. Триггеры отрабатывают хорошо, инциденты ловятся. Но рано или поздно клиент задаёт вопрос не про «упало или нет», а что-то вроде: «хватит ли нам места на файловом сервере через полгода?» или «почему 1С начинает тормозить именно по вторникам ближе к обеду?». Вот тут лампочка «красный-зелёный» уже не помогает. Нужна история метрик за несколько месяцев — с возможностью покрутить график, провалиться в конкретный день, сравнить недели. Именно для этого мы поднимаем второй слой: Prometheus и Grafana. Ниже — конкретно как: версии, конфиги, пороги.
Зачем директору IT-аутсорсинга ещё один мониторинг, если уже есть Zabbix
Zabbix 7.0 — это наша рабочая лошадка. Стоит на большинстве клиентских стендов, под него выстроены шаблоны, настроены эскалации, SNMP работает со свитчами и ИБП. Всё хорошо — пока не приходит руководитель и не просит показать плавную кривую нагрузки диска за три месяца с разрешением в минуту. Вот тут начинаются проблемы. Item history hold по умолчанию короткий. Тренды хранятся по часам, а не по секундам. Показать «не упало-не упало», а реальную динамику — уже задача со звёздочкой. Приходится выкручиваться, и это неудобно.
Prometheus в этой роли работает иначе: он тянет метрики с интервалом 15 секунд, хранит их как временной ряд с полным разрешением на весь срок retention, и любой график — это не заранее посчитанный тренд, а PromQL-запрос по сырым данным. Для клиента до 50 рабочих мест это означает конкретную вещь: когда мы приходим с разговором «через 4 месяца упрётесь в диск на 1С-сервере», у нас на экране реальная линейная экстраполяция роста node_filesystem_avail_bytes, а не оценка на глаз.
Мы не выкидываем Zabbix и не заменяем его Prometheus-ом — это принципиально. Они решают разные задачи. Zabbix остаётся системой алертинга широкого профиля: SNMP, службы Windows, лог-файлы, эскалации. Prometheus вместе с Grafana — это уже другое. Capacity planning, визуальная диагностика нагрузки, диски, память, сеть, CPU в динамике. На контурах, где серверов больше трёх-четырёх и клиент готов платить за глубокую аналитику, мы ставим оба слоя параллельно. Они не конфликтуют — разные агенты, разные порты, разные задачи.
Архитектура нашего стека: четыре компонента и кто кому что отдаёт
Для небольшого наблюдаемого парка — до 10-15 серверов — нам хватает одной VM-коллектора: 2 vCPU, 4 ГБ RAM, 40 ГБ диска. На ней поднимаем четыре процесса. 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-юниты поверх обычных бинарников, отдельный непривилегированный пользователь для каждого сервиса. Прозрачно, надёжно, понятно любому Linux-администратору без погружения в специфику Docker.
Порядок действий на сервере-коллекторе (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.
