Технологии: Prometheus, Grafana, Alertmanager, AWS EC2 discovery, Kamailio, FreeSWITCH, SIPp, CloudFormation, Go-утилиты, Packer
На платформе экстренной связи вопрос «жив ли сервер» почти никогда не совпадает с вопросом «можно ли сейчас совершить звонок». Kamailio может слушать 5060/5061, FreeSWITCH — отдавать метрики, node_exporter — показывать нормальную загрузку, а вызов нужного абонента не пройдёт. В отказоустойчивом кластере из восьми SIP-прокси — по четыре в main и backup регионах — одна упавшая нода штатная ситуация, а не инцидент. Другая нода может оставаться «зелёной» по метрикам уровня хоста, пока сигнализация уже деградировала.
Меня пригласили на проект, когда полноценного мониторинга по сути не было. Коллеги смотрели в CloudWatch: CPU, RAM, ёмкость и т. д. Мониторинг был в крайне зачаточном и фрагментированном состоянии, им почти не пользовались. Предыдущие подходы — в том числе отправка метрик в Elastic — не доводили систему до наблюдаемости на уровне сервиса: обнаружение целей, оповещение и сопровождение расходились с тем, как реально функционировала платформа на AWS EC2. Новые инстансы не становились таргетами мониторинга автоматически. Централизованного service-level алертинга не было.
Параллельно начался спор Zabbix против Prometheus, а также выбора между pull/push-моделями. Я разбирал/деплоил инфраструктуру и смотрел, что нужно мне и коллегам-инженерам. Prometheus проще лёг на AWS EC2, labels и PromQL-агрегации, без которых кворумная логика просто не собирается — в отличие от Zabbix, который хорошо закрывает другие задачи (он хорош в основном для сети), но здесь упирался в модель данных.
Задача, которую мне поставили, была конкретной: покрыть критичные компоненты, сделать VoIP-дашборды и научить мониторинг автоматически находить инстансы в AWS по тегам, опрашивая AWS API. Отдельно — то, что на тот момент не было решено: алертить не каждую ноду, а потерю минимального рабочего состава сервиса в регионе и у конкретного клиента. Особенно с учётом автоскейлинга, где инстанс — короткоживущая сущность.
В итоге проект охватил около десяти клиентских AWS-аккаунтов, десятки независимых стеков и несколько сотен EC2-инстансов. В таких масштабах ошибки в модели мониторинга начинают масштабироваться вместе с инфраструктурой: ручное добавление целей перестаёт работать, а алерты на отдельные виртуальные машины перестают отражать состояние сервиса.
Исходное состояние
Платформа жила на EC2 в нескольких регионах AWS. Роли были разнесены: SIP-прокси на Kamailio, медиаслой на FreeSWITCH, RTP через RTPengine, брокер сообщений RabbitMQ, базы данных CouchDB/MySQL/PostgreSQL, демоны маршрутизации, SBC и шлюзы к операторам связи. Инстансы появлялись и исчезали — автомасштабирование, замена нод, новые клиентские окружения. CloudWatch видел виртуалку. Он не видел, сколько рабочих прокси осталось на стороне main или backup и хватает ли этого для отказоустойчивости.
На момент активного развития платформа покрывала около десяти штатов. Каждый штат был отдельным аккаунтом AWS, внутри которого работало по 8-10 стеков. В каждом стеке обычно было 12-18 EC2-инстансов, а при росте нагрузки количество нод могло увеличиваться через автоскейлинг. Каждый стек был разнесён на пару main/backup в разных AWS-регионах — с учётом географии США их логично ставить как можно дальше друг от друга (us-west-1 и us-east-1). Даже в базовой конфигурации это давало сотни инстансов, десятки ролей и множество независимых комбинаций «клиент → стек → main или backup → подсистема». Каждый новый штат означал полностью независимый стек мониторинга с общими для всех правилами, дашбордами и маршрутизацией алертов.
Редактируемые руками списки таргетов в Prometheus или Zabbix в такой среде живут недолго. Elastic как хранилище метрик и событий копил данные, но связь «роль → регион → клиент → критичность» в оповещения не доходила. У эксплуатации не было одного ответа на простой вопрос: сервис сейчас в рабочем состоянии или уже нет.
Единица наблюдения
Проблема оказалась в единице наблюдения. Пока алерт строится на up == 0 для конкретного экспортера, дежурный в NOC получает сигнал о падении EC2. Для отказоустойчивого кластера это шум: трафик ещё идёт, запас прочности уменьшился, но функция не потеряна. Нужно было считать не машины, а роли — proxy, switch, rtpengine, mq, db — в разрезе клиента, подсистемы и стороны стека (main или backup). В labels это client, subsystem, zone: последний — зона доступности, в которой работает инстанс.
Та же проблема на уровне протокола. Метрики хоста и даже «порт слушает» не доказывают, что SIP-трафик проходит. Kamailio может быть запущен, регистрация — в норме на одной ноде, а маршрут к оператору связи уже не работает. Поэтому часть проверок я вынес в отдельный слой: SIP OPTIONS, состояние регистрации, SIPp-сценарии (регистрации и тестовые звонки на служебный диалплан). Node exporter закрывал метрики ВМ, но решение об инциденте принималось по сигналам уровня сервиса.
Обнаружение целей через теги AWS
Инстансы в AWS уже были размечены тегами — инфраструктура уже использовала систему тегов, которую удалось положить в основу discovery. Теги описывали роль и принадлежность: proxy, switch, db, mq, client, subsystem. Мониторинг должен был подхватывать эти labels из сгенерированных файлов целей — иначе каждый новый инстанс превращался в ручную работу.
Раз в пять минут скрипт awspmtgen опрашивал EC2 API: список работающих инстансов, сетевые интерфейсы на нодах с несколькими адресами. Результат превращался в JSON для Prometheus file_sd — на выходе больше сотни файлов целей под node exporter, Kamailio, RTPengine, blackbox-проверки, регистрации, тестовые вызовы, с разбивкой по регионам.
Встроенный ec2_sd Prometheus мы не использовали: discovery должен был выбирать правильный IPv6-адрес на multi-homed нодах, читать нашу схему тегов и порождать отдельные файлы под разные порты и роли. Кастомный генератор + file_sd оказались проще, чем адаптировать встроенный service discovery под нашу модель разметки.
Внутри платформы всё ходило исключительно по IPv6; dual-stack был только наружу — на внешний периметр. Мониторинг стучался на цели тоже по IPv6. На нодах с несколькими интерфейсами адрес выбирался по заранее закреплённым окончаниям IPv6-адресов — иначе Prometheus попадал не на тот интерфейс. Каждая цель получала labels из тегов, и новый инстанс с корректной разметкой попадал в мониторинг без правки конфигурации. Когда схема тегов менялась, генератор какое-то время писал оба формата, чтобы миграция в production прошла без окна слепоты.
Кворумные алерты
Кворумная логика была центральной задачей с самого начала — именно ради неё нужны были labels из discovery. Вместо critical на каждый упавший экспортер — агрегация по роли, клиенту и стороне стека (main или backup):
count by (subsystem, app, notificationtype, job, client) (
up{proxy="true", job="awsnodes", zone=~"us-west.*", notificationtype="prod"}
) < 2
Label zone на каждой цели — конкретная availability zone. В правиле zone=~"us-west.*" — это main-сторона (us-west-1); us-east.* — backup (us-east-1). Если у клиента четыре прокси на main и одна нода недоступна, счётчик три — critical не срабатывает. Срабатывает, когда остаётся меньше двух рабочих proxy-нод: для дежурного это потеря отказоустойчивости маршрутизации SIP, а не перезагрузка одной VM. Аналогичные правила шли для backup, RTPengine, медиасервера, брокера сообщений, баз данных — с разными порогами.
Alertmanager маршрутизировал алерты по клиенту, критичности и типу подсистемы. При перехлёстном мониторинге оба Prometheus слали алерты в кластер Alertmanager — без кластера каждое событие приходило бы дважды. Все алерты собирались на дашборде, за которым следила команда NOC.
Что изменилось для дежурного
До: разрозненный алерт «инстанс недоступен» или письмо из CloudWatch. Дежурный открывал несколько вкладок — консоль EC2, метрики, иногда SSH — и вручную выяснял, в каком штате нода, какая у неё роль и остались ли живые соседи в кластере. Падение одной proxy-ноды из четырёх на стороне main или backup часто выглядело как инцидент, хотя сервис ещё работал.
После: алерт приходит с контекстом — client / zone / subsystem / proxy quorum degraded (3/4) или registration check failed, carrier route unreachable. На дашборде видно здоровье кластера прокси, медиа, RTP, а не сводный CPU по хостам. Дежурный начинает разбор с вопроса «какая функция деградировала», а не «какая VM упала».
VoIP-проверки поверх инфраструктурных метрик
Почему не хватило стандартных экспортеров
Node exporter на каждой ВМ давал базовые метрики ОС — этого мало для решения об инциденте. Поверх шли специализированные экспортеры под RTPengine, RabbitMQ, Postgres, демоны маршрутизации. Blackbox exporter проверял порты Kamailio и конечные точки приложений, script exporter — регистрации. Для RabbitMQ пришлось написать кастомный экспортер под несколько специфичных метрик, которых в стандартном не было.
Сетевое оборудование
Juniper шёл через snmp-exporter с OID-листами под наши модели — готовых шаблонов тогда не нашлось, пришлось писать и генерировать свои для полного покрытия BGP. Отдельно пришлось доработать aws-dc-exporter: метки Direct Connect-интерфейсов AWS наследовались некорректно, без правки BGP-срез по DC не сходился.
SIP и сигнализация
Для сигнализации — SIPp-сценарии: прохождение SIP-диалога, а не «порт слушает». Тестовые вызовы к FreeSWITCH и регистрации к Kamailio шли отдельным слоем discovery. Направления к операторам связи — через собственные Go-exporters, где стандартных метрик не было.
Пограничные SBC-шлюзы метриками в привычном виде не отдавали — их покрытие шло через SNMP и отдельные дашборды. В Git лежало 60+ дашбордов, разбитых по слоям — прокси, медиа, RTP, шлюзы, сеть — а не сводным CPU по хостам.
Главный регион и клиентские окружения
Платформа расползлась по штатам: штат — отдельный клиент со своим Prometheus, Loki, Alertmanager, общими правилами и собственными целями. Правила и дашборды в Git жили в отдельных ветках под клиентов, синхронились через мёрж веток.
В каждом клиентском окружении — две мониторинговые ноды: main и backup, физически в us-west-1 и us-east-1 — с учётом географии США их и разнесли максимально далеко. Мониторинг был перехлёстным: main Prometheus смотрел на себя и на backup, backup — на себя и на main. Если одна мониторинговая нода или целый AWS-регион на время выпадали, картина оставалась со второй стороны. Оба Prometheus слали алерты в пару Alertmanager в кластере по IPv6 — иначе одно и то же событие приходило бы дважды.
Чтобы не копировать CloudFormation под каждый штат, я написал шаблонизатор на Go tpl2cf: один YAML с параметрами клиента, пара шаблонов main/backup — на выходе готовые стеки под основной и резервный регион. Для мониторинга это означало, что идентификатор клиента, сети и AMI не правились руками в сотнях строк шаблона: инженер заполнял YAML-файл и прогонял генерацию. Ошибки адаптации ловились до деплоя. Позже tpl2cf вырос до генерации всех стеков компании из одного YAML с автовыбором шаблона — но это уже отдельная история про IaC, не про мониторинг.
Эксплуатации нужна была единая точка входа. На практике это выяснилось уже в процессе внедрения, когда стало понятно, что переключение между отдельными Grafana быстро превращается в проблему само по себе. Grafana в главном регионе поднимала источники данных на все клиентские окружения — Prometheus, Loki, Alertmanager по паре на штат. В главном стеке — отдельный источник Prometheus на каждое окружение; в клиентском — один. Оператор открывал одну Grafana и переключал клиента, не открывая десяток вкладок.
Grafana была частью продукта: open-source, собиралась из исходников с корпоративным branding patch — свой логотип, favicon, экран входа. RPM лежал в корпоративном репозитории, который я поддерживал; на ноды пакет попадал из образа, а не при каждом старте инстанса.
Я не собирал все TSDB в один глобальный Prometheus — распределённый сбор и централизованная визуализация. Федерация и Thanos на бумаге выглядели аккуратно, но давали единый радиус отказа, межрегиональный трафик метрик и эксплуатацию, которую некому было бы содержать. VictoriaMetrics тоже не рассматривали: стек уже работал, миграция TSDB не окупала риск. Managed Prometheus в AWS отпал по той же причине — кастомные Go-exporters, SIPp, snmp-exporter с ручными OID и собственный discovery не ложились в managed-модель.
Мониторинговая нода в регионе — EC2 со всем стеком рядом; в клиентском окружении таких две — main и backup, с перехлёстным сбором метрик между ними. Данные TSDB, Grafana и Loki — на отдельных EBS-томах. Глобальный scrape interval — 1 минута, большинство VoIP-джобов — 2 минуты, тяжёлые пробы регистрации и SIP OPTIONS — 5 минут. Retention TSDB — 180 дней. На стек приходилось около сорока scrape jobs, порядка 270 правил алертинга и 330 recording rules — последние нужны были, чтобы кворумные запросы в PromQL не убивали TSDB на каждом refresh дашборда.
Инстансы поднимались с кастомных AMI на CentOS Stream, собранных через Packer: пакеты из корп-репо, скрипты и unit-файлы уже внутри. CloudFormation отвечал за сеть, диски, IAM и донастройку — не за установку полстека на bootstrap. При автомасштабировании на платформе экстренной связи это критично: нода должна выйти в строй быстро, а dnf install -y на старте съедает время, которого у аварийного контура нет.
Репозиторий и воспроизводимость
После того как базовая схема стабилизировалась, её начали использовать при развёртывании новых клиентских окружений. Каркас стека — Go-шаблоны CloudFormation, готовые YAML под штат — из tpl2cf. Стек поднимал EC2 из Packer-AMI, сеть, IAM, EBS, короткий cfn-init с конфигами Prometheus, Alertmanager, Grafana и Loki. Софт на образе, параметры клиента в YAML, правила и дашборды — из Git.
Обновление шло без ручного SSH: смена AMI в стеке редеплоила инстансы, обновлялось по региону за раз. Перед этим изменения проходили полное тестирование в stage-аккаунте AWS, где работал идентичный стек, поэтому production-деплой проходил предсказуемо. Правила и дашборды подтягивались из Git раз в час; цели discovery перегенерировал awspmtgen.timer каждые пять минут — Prometheus подхватывал новые JSON без рестарта.
Дальше — наращивание покрытия: SBC, пограничные шлюзы, SNMP Juniper, SIPp. Базовая схема сложилась за несколько недель; ещё примерно год календарного времени ушёл на масштабирование по клиентским окружениям, полировку правил и дашбордов, сбор обратной связи от эксплуатации и дожимание покрытия. Это не был год, посвящённый только мониторингу: параллельно шли остальные задачи системной инженерии — автоматизация деплоя и AMI, VoIP-инфраструктура, CloudFormation-стеки, поддержка корпоративного репозитория, tpl2cf и масса другой автоматизации. Мониторинг наращивался итерациями между этими работами, часто из того же контекста: новый штат, новая роль в стеке, очередной экспортер. Когда работа была закончена, мониторинг передали в NOC, NOC уже продолжил его развивать в сторону лучшего покрытия сетевой инфры.
Что бы сделал иначе сейчас
Сейчас awspmtgen писал бы сразу на Go. В начале проекта bash с aws CLI и jq был нормальным компромиссом: discovery нужно было поднять быстро, а с Go я тогда только входил в тему. Скрипт делал своё — раз в пять минут опрашивал EC2 и клал JSON для file_sd. Потом требования наслоились: IPv6 на multi-homed нодах, два формата тегов, отдельные файлы под каждый порт и регион. Логика разъехалась по shell-пайплайнам, которые неудобно тестировать и страшно менять без регресса. Отдельный бинарник с AWS SDK не трогал бы остальную схему — тот же file_sd, тот же timer, тот же reload Prometheus — но вернул бы нормальные unit-тесты на разбор тегов и адресов.
Вывод
Установка Prometheus сама по себе мало что меняла. Схема, в которой мониторинг описывает способность платформы выполнять функцию, задалась за несколько недель и за год доведена до рабочего состояния — не как отдельный проект в вакууме, а вперемешку с остальной инженерией платформы.
Алерт считался критичным, когда на стороне main или backup оставалось меньше минимального числа нод роли — не когда падала одна VM из четырёх прокси на этой стороне. Новый EC2 с правильными тегами попадал в мониторинг без тикета на ручное добавление цели. Критерии были простыми: сколько рабочих прокси на main и backup, проходит ли SIPp-сценарий, есть ли путь регистрации к оператору связи.
Через год после начала работы мониторинг перестал быть отдельным проектом, который требует весь фокус. Он стал частью эксплуатации: новые клиентские окружения разворачивались по тем же шаблонам, правила эволюционировали вместе с платформой, а NOC работал уже не с набором отдельных EC2-инстансов, а с моделью состояния сервиса — около десяти клиентских окружений, десятки стеков, сотни EC2-инстансов и роли, которые могли масштабироваться без ручного добавления целей в мониторинг.
Для меня это и стало главным критерием успешности: мониторинг перестал требовать постоянного внимания разработчика и превратился в обычный рабочий инструмент операционной команды. Хорошая инженерная система в итоге становится скучной — она перестаёт быть проектом автора и начинает жить собственной жизнью.
Результат: Мониторинг стал самостоятельным инструментом эксплуатации и масштабировался на новые штаты без моего участия.