Docker-мониторинг без Prometheus: простой подход для команд до 10 человек
Docker-мониторинг без Prometheus для малых команд.
Пару месяцев назад ко мне пришёл руководитель небольшой веб-студии — восемь разработчиков, двадцать с лишним проектов на продакшне, три сервера. Спрашивает: "Как нам нормально мониторить Docker? Всё говорят — Prometheus и Grafana, мы попробовали, три дня потратили на настройку, половину не поняли, в итоге всё снесли." Я его прекрасно понимаю, потому что сам через это проходил. Prometheus — это мощно, это индустриальный стандарт, это то, что используют Uber и Netflix. Только вот Uber и Netflix — это не восемь человек с тремя серверами. И вот тут начинается разговор, который я, честно говоря, веду уже не первый раз: а что если для вашего масштаба существует решение проще? Не хуже — просто проще. И при этом закрывающее девяносто процентов реальных потребностей.
Я работаю с небольшими и средними командами уже лет восемь. Видел много проектов, где инфраструктура была выстроена "как в Google" — только без команды Google и без бюджета Google. И почти везде это заканчивалось одинаково: красивая архитектура, которую никто не понимает, мониторинг, который никто не смотрит, и алерты, которые всё время шумят и которые все давно научились игнорировать. А потом сервер падает, и никто не знает, что произошло — потому что в Grafana последний раз заходили три недели назад.
Поэтому сегодня я хочу рассказать про другой подход. Не революционный, не хайповый, не "а вы слышали про eBPF-трейсинг?" — а простой и рабочий. SSH на сервер, docker stats, парсинг JSON, сохранение в PostgreSQL, отображение в дашборде. Всё. Работает на трёх серверах и пятидесяти контейнерах без отдельного сервера для мониторинга, без агентов на каждой машине, без YAML-конфигураций на тысячу строк.
Я понимаю, что сейчас скажу вещь, за которую меня могут осудить технические люди. Но Prometheus с Grafana для команды до десяти человек и до пятидесяти контейнеров — это избыточное решение. Не плохое — избыточное. Разница важная.
Смотрите, что происходит, когда небольшая команда берётся за Prometheus. Сначала надо развернуть сам Prometheus-сервер — это отдельный процесс, который требует ресурсов, места на диске, настройки retention и компактации данных. Потом нужен Node Exporter на каждом сервере — ещё один агент, ещё один процесс, ещё одна точка отказа. Потом нужен cAdvisor для Docker-метрик — потому что Node Exporter сам по себе контейнеры не понимает. Потом нужен Grafana с dashboards — и это не "кликнул три кнопки", это часы настройки, выбора panels, написания PromQL-запросов. Потом Alert Manager, потому что без него алерты не придут. Потом кто-то уходит в отпуск, и оказывается, что из восьми человек только он понимал, как всё это устроено.
Я не преувеличиваю. Я видел это своими глазами минимум на пяти разных проектах. И каждый раз история одинаковая: настраивали с энтузиазмом, первые две недели смотрели, потом перестали понимать, что смотреть, потом один из dashboards сломался и никто не починил, потом Prometheus начал жрать 4 GB RAM на мониторинге десяти контейнеров и его "временно" отключили. Временно — это навсегда.
Проблема не в Prometheus. Prometheus — отличный инструмент. Проблема в том, что его операционная сложность не соответствует размеру задачи. Это как купить промышленный кофемашину за двести тысяч рублей для домашнего использования — кофе она сварит отличный, но обслуживать её, чистить и разбираться в её настройках вы будете столько же времени, сколько пьёте кофе.
Но давайте разберёмся, что нам реально нужно от мониторинга Docker. Когда я задаю этот вопрос командам, ответы почти всегда одинаковые: знать, когда контейнер упал; знать, когда контейнер ест слишком много памяти; видеть логи, когда что-то пошло не так; получить алерт в мессенджер, а не узнавать о проблеме от пользователей. Всё. Никто не говорит "нам нужны персентили latency по percentile 99.9" или "нам нужен cardinality exploration в PromQL". Это нужно SRE-командам в больших компаниях, а не ребятам, которые держат двадцать сайтов на трёх серверах.
И когда понимаешь это — становится очевидно, что задача гораздо проще, чем кажется. И что для её решения не нужен Prometheus.
Есть ещё один фактор, о котором редко говорят открыто: знания. Prometheus с Grafana требует специфических знаний, которые нужно поддерживать в актуальном состоянии. PromQL — это полноценный язык запросов, который нужно учить. Конфигурирование Alert Manager с его routing, grouping, inhibition rules — это отдельная история. Когда в команде есть выделенный DevOps-инженер, который занимается этим постоянно — всё нормально. Но в команде до десяти человек DevOps — это часто один из разработчиков по совместительству. Он настроил Prometheus месяц назад, потом три недели занимался основной задачей, и теперь, когда что-то сломалось в конфигурации, ему нужно заново вспоминать, как всё устроено. Это не проблема конкретного человека — это проблема несоответствия инструмента контексту использования.
Наш подход работает иначе. Вся логика — в нескольких сотнях строк TypeScript (или Go, или Python — на чём вам удобнее). Любой разработчик из команды может открыть файл и понять, что происходит. Правила алертов — это JSON в базе данных, их можно редактировать через интерфейс. Никакого специального синтаксиса, никакой магии конфигурационных файлов.
Идея, которую мы реализовали, предельно простая. Есть сервер с Docker. На нём работают контейнеры. Мы хотим знать, что с ними происходит. Вместо того чтобы ставить агент на каждый сервер, мы просто подключаемся по SSH и запрашиваем нужные данные. Это как позвонить другу и спросить "как дела?" вместо того чтобы нанять детектива, который круглосуточно за ним следит.
Вот что мы делаем по SSH раз в тридцать секунд или раз в минуту, в зависимости от потребностей:
```bash docker ps --format '{{json .}}' ```
Эта команда возвращает список всех контейнеров в JSON-формате — название, образ, статус, когда запущен, порты. Дальше:
```bash docker stats --no-stream --format '{{json .}}' ```
Это снимок текущих метрик: CPU, RAM, сеть, дисковый I/O для каждого работающего контейнера. Флаг `--no-stream` принципиален — без него команда будет висеть и обновлять данные бесконечно, а нам нужен один снимок и уйти.
И дополнительно для проверки healthcheck-статусов:
```bash docker inspect --format '{{json .State}}' $(docker ps -q) ```
Это даёт полный стейт каждого контейнера, включая `Health.Status` — running, unhealthy, starting — и историю последних healthcheck-проверок.
Всё это парсится на стороне нашего бэкенда, нормализуется и сохраняется в PostgreSQL. Никакого агента на сервере. Никакого дополнительного процесса. Только SSH-клиент, который работает на нашей стороне, и стандартные Docker-команды, которые есть на любом сервере с Docker.
Когда я впервые показал этот подход коллеге с DevOps-бэкграундом, он посмотрел скептически: "А что если SSH-соединение упадёт?" Хороший вопрос. Но давайте подумаем — если SSH-соединение упало, значит, либо сервер недоступен, либо у нас сетевые проблемы. В обоих случаях это само по себе является алертом: "не можем достучаться до сервера". И мы это фиксируем — если три попытки подряд провалились, летит уведомление. Что, собственно, и есть правильный мониторинг доступности.
Другой аргумент против: "А как же overhead от SSH-соединений?" Посмотрим на цифры. SSH-рукопожатие занимает порядка 100-200 миллисекунд. docker stats --no-stream на двадцати контейнерах выполняется за 2-3 секунды. Если мы опрашиваем раз в тридцать секунд — это меньше 10% времени уходит на сбор метрик. На сервере с двадцатью контейнерами накладные расходы настолько малы, что их не видно на фоне обычной работы. Prometheus с cAdvisor, кстати, тоже не бесплатный — он постоянно сидит в памяти и регулярно скрапит метрики.
Самое важное преимущество SSH-подхода, которое я ценю больше всего — это отсутствие attack surface. Каждый агент, который вы ставите на продакшн-сервер, — это потенциальная точка входа для злоумышленника. Каждый открытый порт — это риск. SSH-порт у вас и так открыт (иначе как вы вообще управляете сервером?). Добавляя мониторинг через SSH, вы не добавляете новых рисков. Ставя экспортёры на 9090, 9100, 8080 — добавляете.
Когда у нас есть сырые данные из docker ps и docker stats, встаёт вопрос: а что с ними делать? Это не риторический вопрос — я видел команды, которые собирают метрики, а потом никогда не смотрят, потому что не знают, на что обращать внимание.
Давайте разберём по категориям то, что реально важно для команды до десяти человек.
Первая категория — статус контейнеров. Тут всё просто: контейнер либо running, либо нет. Если нет — это проблема. Но есть нюансы. Контейнер может быть в статусе `restarting` — это значит, он падает и перезапускается циклически. Это restart loop, и он часто хуже, чем просто остановленный контейнер, потому что он создаёт иллюзию работоспособности. Контейнер может быть `unhealthy` — это значит, он запущен, но healthcheck показывает, что внутри что-то не так. Например, Nginx запущен, но не отвечает на HTTP-запросы. Вот почему важно смотреть не только на `running/not running`, но и на `Health.Status`.
Для определения restart loop мы смотрим на поле `RestartCount` в docker inspect. Если за последние пятнадцать минут это число выросло больше чем на три — это уже повод для алерта. Не сразу на один рестарт, потому что иногда контейнер честно рестартует при деплое — а именно на аномальный рост счётчика.
Вторая категория — потребление ресурсов. Здесь я выработал практические пороги, которые хорошо работают для большинства приложений. По памяти: жёлтый алерт при 75% от лимита (если лимит задан), красный при 90%. Если лимит не задан — смотрим на абсолютные значения и тренд. Контейнер, который потребляет 1 GB RAM, — это нормально для тяжёлого приложения. Контейнер, который был на 200 MB и за час вырос до 1 GB без видимых причин — это memory leak, и с этим надо разбираться.
По CPU: постоянная загрузка выше 80% — это сигнал. Но кратковременные пики до 100% при обработке запросов — совершенно нормальны и не должны вызывать алерты. Поэтому мы смотрим не на мгновенное значение, а на скользящее среднее за пять минут. Контейнер, который в среднем за пять минут потребляет больше 80% CPU — это проблема. Контейнер, который пикнул до 95% на две секунды — это жизнь.
По сети: здесь аномалия определяется иначе. Мы смотрим на резкое изменение паттерна. Если контейнер обычно гоняет 100 MB/час, а вдруг начал гнать 10 GB/час — это либо DDoS, либо утечка данных, либо кто-то запустил что-то неожиданное. Абсолютные пороги тут работают хуже, чем relative deviation от baseline.
Третья категория — логи. Это самое недооценённое направление в мониторинге. Все следят за CPU и RAM, никто не читает логи — до тех пор, пока что-то не сломается. А потом оказывается, что в логах было написано об ошибке за три дня до падения, просто никто не смотрел.
Мы забираем последние N строк из docker logs для каждого контейнера. Парсим на наличие слов `ERROR`, `CRITICAL`, `FATAL`, `Exception`, `panic`, `OOM`. Если такие паттерны появились — сохраняем и показываем в дашборде. Это не замена нормальному log management, но для команды до десяти человек это закрывает восемьдесят процентов потребностей. За оставшиеся двадцать процентов отвечает уже ELK или Loki — но только когда вы доросли до этого масштаба.
Четвёртая категория — алерты. Я убеждён, что плохо настроенные алерты хуже, чем их отсутствие. Когда алерты шумят каждые пятнадцать минут по несущественным поводам, люди перестают на них реагировать. Синдром "мальчик, который кричал волк" в мире DevOps — это реальная проблема.
Поэтому мы настраиваем минимальный, но точный набор: контейнер остановился (и это не плановая остановка); контейнер в состоянии unhealthy больше пяти минут; restart count вырос больше трёх раз за десять минут; RAM больше 90% от лимита. Всё. Четыре правила, которые реально требуют внимания. Остальное — в дашборде для тех, кто хочет смотреть глубже.
Есть один аспект Docker-мониторинга, о котором я хочу поговорить отдельно — потому что большинство команд либо не используют его вообще, либо используют неправильно. Это healthcheck.
Если посмотреть на случайную выборку docker-compose.yml файлов в небольших командах, в семи из десяти не будет ни одного healthcheck. Контейнер либо running, либо exited — и всё. Но состояние процесса — это не то же самое, что состояние приложения. Nginx может быть запущен (процесс живой, PID есть), но при этом возвращать 502 на все запросы, потому что upstream умер. PostgreSQL-контейнер может быть running, но при этом база ещё инициализируется и не принимает соединения. Node.js приложение может виснуть в event loop, процесс живой, но запросы не обрабатываются — они просто накапливаются в очереди.
Healthcheck — это механизм, который позволяет самому приложению говорить: "я не просто запущен, я реально работаю". Выглядит в Dockerfile это примерно так:
```dockerfile HEALTHCHECK --interval=30s --timeout=10s --start-period=60s --retries=3 \ CMD curl -f http://localhost/healthz || exit 1 ```
Или в docker-compose.yml:
```yaml healthcheck: test: ["CMD", "curl", "-f", "http://localhost/healthz"] interval: 30s timeout: 10s retries: 3 start-period: 60s ```
После того как healthcheck настроен, docker inspect начинает возвращать реальный статус приложения: `healthy`, `unhealthy` или `starting`. И это принципиально меняет качество мониторинга. Вместо "процесс запущен" мы получаем "приложение работает как ожидается".
Параметр `start-period` особенно важен и часто игнорируется. Это время, в течение которого healthcheck не засчитывается как failed — для случаев, когда приложению нужно время на инициализацию. Если у вас Java-приложение, которое стартует 45 секунд, и start-period выставлен в 10 секунд — вы будете получать ложные unhealthy алерты при каждом деплое. Мы на одном проекте потратили полдня, разбираясь, почему после каждого деплоя система фиксирует инцидент. Оказалось — start-period был слишком короткий.
Что конкретно проверять в healthcheck — зависит от приложения. Для HTTP-сервисов — простой GET /health или /healthz, который возвращает 200 и, желательно, JSON со статусами зависимостей. Для баз данных — можно использовать встроенные инструменты: `pg_isready` для PostgreSQL, `mysqladmin ping` для MySQL, `redis-cli ping` для Redis. Для приложений без HTTP — можно проверять наличие lock-файла, наличие процесса, доступность сокета.
Я настоятельно рекомендую делать healthcheck-эндпоинты минимальными и быстрыми. Не надо в /healthz делать тяжёлый запрос к базе данных на проверку данных — достаточно простого SELECT 1. Healthcheck выполняется каждые тридцать секунд, и если он тяжёлый, вы создаёте дополнительную нагрузку. Я видел проект, где healthcheck делал полный обход таблицы с миллионом записей "для надёжности" — это было и медленно, и бессмысленно.
Когда healthcheck настроен правильно на всех контейнерах, картина мониторинга меняется качественно. Вы видите не просто "контейнер запущен", а "контейнер работает, его приложение отвечает на запросы, зависимости доступны". И когда что-то идёт не так — unhealthy статус появляется раньше, чем пользователи начинают жаловаться. Это то, что я называю proactive monitoring в противовес reactive — не "узнали когда сломалось", а "заметили что деградирует".
Я хочу пройтись по технической стороне чуть детальнее, потому что в этом подходе есть несколько нетривиальных моментов, которые стоит прояснить.
Начнём с подключения. Мы используем библиотеку node-ssh (или аналог на вашем языке). Соединение не переоткрываем каждый раз — держим пул соединений, по одному на сервер. SSH-соединение дорогое в плане установки (рукопожатие, согласование ключей), но дёшевое в поддержании. Держим его живым через keepalive-пакеты каждые тридцать секунд.
Один важный момент: никогда не делайте параллельные SSH-команды на один и тот же сервер если у вас ограничение на число сессий. На некоторых серверах есть лимит MaxSessions в sshd_config, и если вы откроете десять параллельных сессий — часть откажет. Мы используем очередь: одна команда за другой на каждый сервер, но разные серверы опрашиваем параллельно.
Теперь про парсинг docker stats. Есть подводный камень, о котором многие не знают. docker stats выводит процент CPU в виде строки типа "2.34%". Казалось бы, просто убери знак процента и получи число. Но вот нюанс: на многоядерном сервере этот процент может быть больше 100%. Если у вас 8 ядер и контейнер использует все восемь, вы получите "800%". Это не ошибка — это значит "800% от одного ядра". Для нормализации делим на количество CPU: `parseFloat(cpuPercent) / numberOfCPUs * 100`. Тогда 800% на 8-ядерном сервере превращается в 100% — что и означает "контейнер полностью загружает весь CPU".
С памятью другая история. docker stats показывает её в человекочитаемом формате: "1.234GiB / 2GiB". Нам нужно это распарсить в байты для хранения и сравнения. Мы написали простую функцию, которая понимает MiB, GiB, MB, GB и переводит всё в байты. Выглядит как мелочь, но именно здесь у нас был баг в первой версии, который давал неправильные алерты по памяти — оказалось, мы сравнивали гигабайты с мегабайтами, не переведя в единую единицу.
Теперь про хранение в PostgreSQL. Мы создаём две таблицы. Первая — `container_status` — хранит текущий статус каждого контейнера: имя, образ, сервер, статус, restart count, health. Это "горячие" данные, которые перезаписываются при каждом опросе. Вторая — `container_metrics` — хранит временной ряд метрик: timestamp, container_id, cpu_percent, mem_usage, mem_limit, net_in, net_out. Это "холодные" данные для истории и трендов.
Сколько хранить историю? Для команды до десяти человек и пятидесяти контейнеров суточное потребление при опросе раз в тридцать секунд — около 50 контейнеров × 2880 записей/сутки × ~200 байт = примерно 30 MB в день. За месяц — около 900 MB. Это нормально. Мы храним тридцать дней метрик, старше удаляем через cron. Индексы по (container_id, timestamp) обязательны, иначе запросы за последние 24 часа будут медленными.
Дашборд мы делали на React, но это вообще не важно — можно взять любой фронтенд или вообще Grafana (да-да, Grafana с PostgreSQL datasource, без Prometheus — это вполне рабочая связка, гораздо проще, чем с Prometheus). Главное, что показываем: список контейнеров с цветовой индикацией статуса (зелёный/жёлтый/красный), текущие метрики CPU и RAM, sparkline-графики за последние час-два, логи с фильтрацией по уровню ошибки. Никаких сложных drill-down интерфейсов — просто таблица с цветами и главными числами.
Особо хочу остановиться на алертах. Мы реализовали их как simple rule engine прямо в базе данных. Каждое правило — это запись в таблице `alert_rules`: threshold type, metric, operator, value, cooldown. Например: `{type: "mem_percent", operator: ">", value: 90, cooldown_minutes: 30}`. Раз в две минуты cron-джоб проверяет все правила против текущих данных и создаёт инциденты, если условие выполнено. Cooldown не даёт создавать дублирующие алерты — если уже создан инцидент по этому правилу за последние тридцать минут, новый не создаётся.
Уведомления уходят в Telegram через Bot API. Это не самый красивый способ, но это наиболее надёжный и быстрый способ добраться до людей. Telegram-бот поднимается за пятнадцать минут, работает стабильно, сообщения приходят мгновенно. Email тоже поддерживаем, но практика показала — на email алерты читают с задержкой в несколько часов. В телеграме — сразу.
Хочу остановиться ещё на одном аспекте, который кажется незначительным, но на практике сильно влияет на удобство использования — это разница между "контейнер упал" и "контейнер упал из-за деплоя". Когда разработчик деплоит новую версию, контейнеры по очереди останавливаются и запускаются снова. Если наивно смотреть только на факт остановки — будет шквал ложных алертов при каждом деплое. Это быстро убивает доверие к системе мониторинга.
Мы решили это через метки плановых событий. Перед деплоем CI/CD или сам разработчик вызывает наш API: `POST /api/maintenance {server: "prod-01", duration_minutes: 30}`. На это время алерты по данному серверу подавляются (но по-прежнему фиксируются в базе). После окончания maintenance window система проверяет: все ли контейнеры, которые были running до начала, снова running? Если нет — это уже реальный алерт, а не плановая остановка. Такой механизм maintenance window — обязательный элемент любой нормальной системы алертинга, и удивительно, как часто о нём забывают.
Раз мы уже заговорили о технической стороне, давайте разберём схему базы данных подробнее — потому что именно здесь скрываются детали, которые влияют на удобство работы в долгосрочной перспективе.
Таблица `container_status` — это "живая" таблица с текущим снимком состояния. Она небольшая и постоянно обновляется. Структура примерно такая: `id`, `server_id` (внешний ключ на таблицу серверов), `container_id` (хэш Docker), `container_name`, `image`, `status` (running/exited/restarting/paused), `health_status` (healthy/unhealthy/starting/none), `restart_count`, `started_at`, `updated_at`. Плюс поле `ports` в виде JSON — иногда нужно знать, на каком порту торчит контейнер.
Нюанс с `container_id`: Docker генерирует новый ID при каждом пересоздании контейнера. Если вы делаете `docker compose up --force-recreate`, старый контейнер исчезает, появляется новый с другим ID. Если ваша история метрик привязана к `container_id` — история обрывается. Мы решили это через `container_name` как основной идентификатор для пользователя, и дополнительный индекс по `(server_id, container_name)`. История при пересоздании продолжается — потому что имя сохраняется.
Таблица `container_metrics` — это временной ряд. Основные поля: `id`, `server_id`, `container_name`, `cpu_percent` (FLOAT), `mem_usage_bytes` (BIGINT), `mem_limit_bytes` (BIGINT), `net_in_bytes` (BIGINT), `net_out_bytes` (BIGINT), `block_read_bytes` (BIGINT), `block_write_bytes` (BIGINT), `collected_at` (TIMESTAMPTZ). Все байтовые значения храним как абсолютные числа, а не дельты — дельты вычисляем на лету при запросе. Это удобнее для хранения, хотя и занимает чуть больше места.
Индексы — критически важная тема для таблицы метрик. Обязательно нужен составной индекс `(server_id, container_name, collected_at DESC)` — именно в таком порядке, потому что типичный запрос звучит как "покажи мне метрики контейнера X на сервере Y за последние 24 часа". Без индекса этот запрос на таблице с несколькими миллионами строк будет занимать секунды. С индексом — миллисекунды.
Ещё один момент, который я хочу отметить отдельно: партиционирование. Для PostgreSQL таблица метрик, которая хранит данные за тридцать дней при опросе раз в тридцать секунд на пятидесяти контейнерах — это примерно 200 миллионов строк. Без партиционирования даже с индексами запросы начнут тормозить. Мы используем партиционирование по дате (PARTITION BY RANGE на `collected_at`), создавая партицию на каждый день. Удаление старых данных тогда превращается в `DROP TABLE` на партицию вместо `DELETE FROM WHERE` — мгновенно вместо долгих минут с локами.
Если не хочется возиться с партиционированием вручную — есть TimescaleDB, расширение для PostgreSQL, которое делает всё это автоматически и добавляет compression, continuous aggregates и много другого. Для нашего масштаба оно немного избыточно, но если вы планируете держать историю дольше тридцати дней и на большом количестве контейнеров — TimescaleDB стоит рассмотреть. Устанавливается как обычное PostgreSQL-расширение, синтаксис SQL тот же.
Таблица `alert_rules` и `incidents` — это уже не про метрики, а про реакцию. В `alert_rules` храним: тип правила (container_down, high_memory, restart_loop, unhealthy), параметры порога, cooldown в минутах, флаг enabled, список каналов уведомлений. В `incidents` — каждый сработавший алерт: какое правило, какой контейнер, когда началось, когда закрылось (если контейнер восстановился — auto-resolve), статус (open/resolved/acknowledged), кто подтвердил получение.
Поле `acknowledged_at` в инцидентах появилось после нескольких месяцев использования — оказалось, что команде важно иметь возможность сказать "я вижу этот алерт, работаю над ним". Иначе если инцидент не решается быстро, система продолжает отправлять ремайндеры, и это раздражает. Acknowledgement — простая механика, но она сильно улучшает опыт работы с алертами в команде.
Было бы нечестно не поговорить о том, чего наш подход не умеет. Потому что я не пытаюсь продать "убийцу Prometheus" — я просто говорю, что для определённого масштаба этот подход лучше подходит.
Первое ограничение — нет real-time мониторинга. Опрос раз в тридцать секунд означает, что между событием и его фиксацией проходит до тридцати секунд. Для большинства задач это приемлемо — если контейнер упал, мы узнаем об этом через полминуты, что вполне нормально. Но если вам нужно мониторить latency ваших API-эндпоинтов с гранулярностью в секунды — это не наш случай.
Второе ограничение — нет PromQL и мощной аналитики. Prometheus позволяет делать сложные корреляции, вычислять производные метрики, строить сложные условия для алертов. Наш approach — это простые правила "больше/меньше порогового значения". Если вам нужно "алертить когда rate запросов за последние пять минут превышает 95-й перцентиль за последние семь дней" — Prometheus ваш выбор.
Третье ограничение — масштаб. Мы говорим о до пятидесяти контейнерах на до десяти серверах. Выше этих цифр SSH-опрос начинает занимать заметное время, PostgreSQL начинает требовать серьёзной оптимизации под нагрузку временных рядов (тут лучше работает TimescaleDB или специализированное TSDB), и административная нагрузка растёт. При ста серверах и пятистах контейнерах надо смотреть в сторону Prometheus с proper service discovery.
Четвёртое ограничение — application metrics. docker stats дают вам системные метрики: CPU, RAM, сеть. Но метрики вашего приложения — количество обработанных заказов, время ответа базы данных, количество активных пользователей — это уже другая история. Для этого нужна инструментация кода (OpenMetrics, StatsD, пользовательские метрики). Наш подход это не закрывает. Prometheus с этим справляется отлично.
Пятое ограничение — мультитенантность и безопасность в командах с разными ролями. Если у вас несколько команд, работающих на одной инфраструктуре, и вы хотите ограничить видимость — кто какие серверы и контейнеры видит — наш простой подход потребует доработки. Это решаемо, но это дополнительная работа, которую Prometheus с его label-based access control закрывает из коробки.
Так когда переходить? Мой ответ: когда почувствуете, что текущего решения не хватает для конкретной задачи. Не "потому что так принято", не "потому что мы выросли до ста контейнеров теоретически", а когда конкретная проблема не решается простым инструментом. Пришёл запрос "нам нужны p99 latency по эндпоинтам" — добавляйте OpenMetrics. Выросли до двадцати серверов и ощутили, что SSH-опрос стал занимать минуты — переходите на Prometheus с Node Exporter. Инфраструктура должна расти вместе с командой, а не опережать её.
И ещё один момент, который я считаю важным. Prometheus — это не конечная цель. Это инструмент. Много команд воспринимают его внедрение как некую точку зрелости: "вот когда у нас будет Prometheus, мы будем нормальной компанией." Но зрелость определяется не инструментами, а тем, насколько хорошо вы понимаете своею инфраструктуру и насколько быстро реагируете на проблемы. Команда с простым SSH-мониторингом, которая знает каждый контейнер и реагирует на алерты за пять минут — гораздо зрелее команды с Prometheus, в dashboards которого никто не заходит.
Возвращаясь к той веб-студии из начала разговора. Мы внедрили описанный подход примерно за неделю. Неделю — это с нуля, включая разработку бэкенда для сбора метрик, простой React-дашборд, настройку алертов в Telegram. Это восемнадцать серверов и тридцать два контейнера в их случае. Итоговое решение потребляет на сервере мониторинга около 200 MB RAM и практически нулевое CPU в фоне.
Первый месяц использования дал интересные результаты. Система поймала три инцидента, о которых команда иначе бы узнала от пользователей: один контейнер с MySQL ушёл в restart loop в 3 ночи (memory limit был слишком занижен), один Nginx-контейнер стал unhealthy из-за бага в healthcheck-скрипте, и один контейнер с воркером медленно съедал память — вырос с 400 MB до 1.8 GB за три дня, что оказалось memory leak в коде.
Три реальных проблемы за месяц, две из которых случились ночью. Без мониторинга — это три звонка от клиентов о недоступном сайте. С мониторингом — это три алерта в Telegram, которые были закрыты без простоя.
Средний MTTD (mean time to detect) у них теперь — около двух минут. Это время между возникновением проблемы и получением алерта. Для команды до десяти человек на трёх серверах — это отличный результат. Не секундный отклик, как в больших SRE-командах, но и не "узнали от пользователей через час".
Стоимость решения — ноль рублей на инфраструктуру мониторинга. Работает на том же сервере, что и остальные сервисы, потребляя пренебрежимо мало ресурсов. Telegram Bot API — бесплатный. PostgreSQL — уже был. Единственная статья расходов — время разработки, которое окупилось уже первым пойманным инцидентом.
Ещё один проект — промышленная компания, семь человек в IT, восемь серверов, около сорока контейнеров. Там основной болью был не мониторинг сам по себе, а то, что каждый разработчик по-разному понимал, что сейчас происходит в продакшне. Один заходил по SSH и руками смотрел docker ps, другой смотрел логи в Kibana (который они когда-то поставили и забыли половину функций), третий просто надеялся, что всё хорошо. После внедрения единого дашборда появилось общее место, куда смотрит вся команда. Это звучит банально, но это огромная разница — когда все видят одну картину, а не у каждого своя.
Был ещё один интересный момент с той промышленной компанией. У них работал сервис синхронизации данных между 1С и веб-порталом — фоновый воркер в Docker-контейнере, который раз в пятнадцать минут забирал данные и обновлял базу. Сервис не имел HTTP-интерфейса, поэтому healthcheck казался неочевидным. Предыдущий подход был прост до примитивности: если контейнер running — значит, работает. Только вот однажды воркер завис в середине цикла синхронизации — процесс живой, контейнер running, но данные на портале не обновлялись уже двенадцать часов. Клиенты звонили в поддержку, поддержка не понимала, что происходит, потому что "всё зелёное".
После этого инцидента мы добавили простой healthcheck: воркер при каждом успешном цикле синхронизации обновлял timestamp в файле `/tmp/last_sync`. Healthcheck читал этот файл и проверял, что он обновился не позже двадцати минут назад. Если файл старше — exit 1, контейнер переходит в unhealthy. Простейшее решение — и оно работает. Никакого HTTP, никакого дополнительного порта — просто файл с timestamp. С тех пор подобные зависания стали обнаруживаться через две-три минуты, а не через двенадцать часов.
Этот пример показывает важную вещь: мониторинг — это не только "контейнер запущен или нет". Это "приложение делает то, что от него ожидается". И для каждого приложения эта проверка своя. Для HTTP-сервиса — ответ на /healthz. Для воркера — timestamp последнего успешного цикла. Для базы данных — способность принимать соединения и выполнять запросы. Потратьте время на то, чтобы сформулировать для каждого своего контейнера: "что значит, что этот сервис работает?" — и вы автоматически получите правильный healthcheck.
Что я особенно ценю в SSH-подходе — это отсутствие магии. Когда что-то не работает в Prometheus, вы начинаете копаться в scrape_configs, в relabeling rules, в cardinality issues, в WAL-сегментах. Это не просто сложно — это сложно диагностировать, потому что система непрозрачная. Когда что-то не работает в нашем подходе, вы берёте SSH и руками выполняете ту же команду, которую выполняет ваш мониторинг. Видите то же самое — значит, проблема в парсинге. Видите ошибку — значит, проблема на сервере. Прозрачность — это не просто удобство, это фундаментальное свойство надёжных систем.
Я долго думал, как лучше всего сформулировать главную мысль этого разговора. И, пожалуй, вот она: сложность инструмента должна соответствовать сложности задачи. Не опережать её, не отставать — соответствовать. Команда из восьми человек с тридцатью контейнерами — это не "маленький Uber". Это другой масштаб задач, другие ресурсы, другой допустимый уровень операционной сложности. И выбирать инструменты нужно исходя из этой реальности, а не из того, что написано в блогах крупных компаний.
Инструменты крупных компаний решают проблемы крупных компаний. У вас другие проблемы. И есть инструменты, которые решают именно их — просто и надёжно.
И напоследок — мысль, которая мне кажется самой важной во всём этом разговоре. Мониторинг — это не про инструменты. Это про культуру в команде. Можно поставить самый лучший Prometheus с самыми красивыми Grafana-дашбордами — и это не поможет, если в команде нет привычки смотреть на статус продакшна каждое утро, реагировать на алерты в течение нескольких минут и разбирать причины инцидентов после их закрытия.
Простой инструмент помогает сформировать эту культуру. Когда дашборд понятен — его открывают. Когда алертов мало и они точные — на них реагируют. Когда система прозрачна — её понимает вся команда, а не только тот один человек, который её настраивал. Именно поэтому я так верю в простые решения на начальном этапе: они не только решают технические задачи, но и помогают команде научиться работать с мониторингом как таковым. А научившись — вы уже будете знать, чего вам не хватает, и осознанно выбирать следующий инструмент. Это гораздо лучше, чем сразу взять самый мощный инструмент и не понимать, что с ним делать.
Вопрос, с которым я оставлю вас: когда вы в последний раз смотрели на статус всех ваших контейнеров? Не потому что что-то сломалось, а просто так, превентивно? Если ответ "давно" или "никогда" — возможно, дело не в отсутствии правильного инструмента, а в том, что существующий инструмент слишком сложен или непонятен, чтобы открывать его без повода. И это, пожалуй, лучший критерий для оценки любого мониторинга: хочется ли в него заходить просто так?