MAATRIX / Блог / SMS об аварии пришла через 40 минут: очередь уведомлений жила на упавшем узле

SMS об аварии пришла через 40 минут: очередь уведомлений жила на упавшем узле

MAATRIX

Дежурный узнал об аварии не от системы алертинга, а от клиента в чате поддержки. SMS с текстом «сервис недоступен» пришла на телефон через 40 минут после начала простоя — когда сервис уже почти сам ожил. Самое неприятное было не в задержке, а в том, что по всем правилам мониторинг сработал вовремя: правило Prometheus сработало, Alertmanager честно отправил вебхук. Просто получать и превращать этот вебхук в SMS было некому — потому что этот «кто-то» жил на том же узле, который в тот момент лежал.

Что сломалось

Ночью в конце августа 2026 года на боевом узле app-01 закончилось место в разделе с логами приложения — ротация логов была настроена, но не на тот каталог, куда реально писало одно из фоновых заданий. Диск заполнился под ноль, приложение начало падать с ошибками записи, systemd пытался его перезапускать, нагрузка на CPU от бесконечных рестартов росла, и через несколько минут узел фактически перестал отвечать на запросы — включая SSH.

Формально это отдельный, довольно тривиальный инцидент: кончилось место на диске, сервис недоступен. Но в этой истории интересен не он, а то, что случилось с оповещением о нём.

На app-01, кроме основного приложения, много лет назад «временно» разместили ещё один компонент — небольшой сервис sms-relay. Его задача простая: принимать вебхуки от Alertmanager и превращать их в вызов API SMS-шлюза, потому что почта дежурный ночью не проверяет, а Telegram-бот один раз подвёл (сообщение потерялось из-за проблем у самого Telegram). SMS в этой схеме была последним рубежом — тем каналом, который должен был достучаться в любом случае. И этот последний рубеж стоял на том же узле, авария на котором он и должен был сообщать.

Когда app-01 встал колом, sms-relay встал вместе с ним. Дежурный получил SMS только через 40 минут — не потому что она долго шла, а потому что её физически некому было отправить: процесс поднялся заново лишь после того, как узел откликнулся на ручной перезапуск.

Что видели в логах и метриках

Первым делом подняли Alertmanager — благо он крутился на отдельном мониторинговом узле и был жив всё время инцидента. В его интерфейсе алерт InstanceDown для app-01 был в статусе firing с точным временем начала — 03:12. Значит, правило Prometheus и сам Alertmanager отработали штатно, вопрос был только в доставке.

Дальше посмотрели в логи Alertmanager по конкретному получателю:

level=warn ts=2026-08-27T03:12:41Z caller=notify.go:748 component=dispatcher \
  msg="Notify for alerts failed" num_alerts=1 err="Post \"http://app-01:8090/hook/sms\": \
  dial tcp 10.20.4.11:8090: connect: no route to host"

Дальше строчки повторялись каждые пару минут — это работал retry с экспоненциальной задержкой, встроенный в Alertmanager. Каждая попытка упиралась в тот же connect: no route to host, потому что адресат — sms-relay на app-01 — был недоступен вместе со всем узлом.

На стороне sms-relay, когда он наконец поднялся, в его собственном логе нашлась одна показательная строка:

2026-08-27T03:53:02+03:00 INF starting sms-relay version=1.4 queue_backend=redis-local
2026-08-27T03:53:02+03:00 INF redis connection restored, replaying pending webhooks count=1
2026-08-27T03:53:03+03:00 INF sms sent to +7900XXXXXXX status=queued provider_id=...

Ключевая деталь — redis-local. Очередь недоставленных вебхуков sms-relay хранил не во внешнем Redis, а в локальном инстансе, поднятом тем же docker-compose прямо на app-01. Иными словами, у сервиса была логика на случай кратковременных сбоев сети («сохрани вебхук в очередь, отправь позже») — но она была бесполезна против сценария «узел с самой очередью недоступен».

Нужен сервер под эту задачу?

Разверните VPS MAATRIX за пару минут: NVMe, AMD EPYC, root-доступ, локации UK, США, Франция и РФ. Оплата картой РФ и по СБП.

Арендовать сервер

Гипотезы, которые отбросили

Прежде чем дошли до реальной причины, проверили несколько версий — по порядку, от самой вероятной по опыту до самой экзотической.

  • Проблема на стороне SMS-провайдера. Зашли на статус-страницу провайдера и в его личный кабинет — инцидентов не было, баланс не нулевой, лимит по сообщениям не исчерпан. Отправили тестовое SMS через тот же API вручную с ноутбука дежурного — пришло за секунды. Провайдер ни при чём.
  • Firewall или сетевое правило заблокировало исходящий трафик к API шлюза. Проверили правила iptables/security group на мониторинговом узле — правил, которые могли бы блокировать именно вызовы SMS-шлюза, не нашли. Да и это не объясняло бы, почему сообщение всё-таки в итоге ушло — просто с опозданием.
  • Alertmanager замьютил алерт силенсом или неправильным роутингом. Проверили amtool silence query — активных силенсов не было. В конфиге роутинга получатель sms-relay был указан верно, без опечаток в маршруте route.receiver.
  • У дежурного была ошибка в номере телефона или неверно настроен on-call график. Номер в конфиге sms-relay совпадал с тем, что и раньше, график дежурств не менялся. Отбросили сразу, как только увидели firing в Alertmanager и запись о попытках доставки в его логе — значит, адресат был выбран правильно.
  • SMS-шлюз ограничил скорость отправки (rate limit). В личном кабинете провайдера в разделе статистики не было ни одного отклонённого запроса за ночь — запрос на отправку просто не был сделан вовремя, потому что до sms-relay вебхук физически не дошёл.

Каждая из этих версий по отдельности звучала правдоподобно, но ни одна не объясняла главного факта: сообщение всё-таки ушло, просто ровно тогда, когда app-01 снова стал доступен. Это и было главной уликой.

Как нашли настоящую причину

Совпадение времени доставки SMS (03:53) со временем восстановления app-01 после ручного перезапуска — вот что сдвинуло расследование в верную сторону. Свели на одном графике в Grafana три ряда: доступность app-01 по node_exporter (up{instance="app-01"}), время последней успешной попытки Alertmanager по получателю sms-relay, и время фактической отправки SMS из лога sms-relay. Все три линии сошлись в одной точке — 03:53.

Дальше посмотрели, что вообще крутится на app-01, командой:

docker compose -f /opt/monitoring/sms-relay/docker-compose.yml ps

И увидели, что sms-relay и его redis — оба контейнера — определены в docker-compose.yml, который лежит прямо в /opt/monitoring/sms-relay/ на app-01, том самом узле, где крутится боевое приложение. Причина размещения нашлась в истории коммитов репозитория инфраструктуры: три года назад сервис подняли «временно, на свободном узле, потом перенесём» — и не перенесли. За три года про это просто забыли, потому что sms-relay ни разу не подводил: сеть моргала, провайдер иногда тормозил, но сам узел app-01, на котором он сидел, ни разу не падал полностью. До этой ночи.

Реальная причина инцидента с задержкой SMS — не сеть, не провайдер и не конфиг Alertmanager, а архитектурная ошибка размещения: единственный канал экстренного оповещения о падении узла зависел от того же самого узла. Это классический single point of failure, только замаскированный под «у нас есть retry и очередь на случай сбоев» — retry и очередь спасают от кратковременных сетевых проблем, но не от смерти самого хоста, на котором они живут. Похожая ловушка разобрана в статье про мониторинг, который стоял на том же сервере и умер вместе с ним — там страдал сам Prometheus, здесь пострадал канал доставки, но механизм один и тот же.

Почему очередь уведомлений оказалась на аварийном узле

Стоит отдельно разобрать, почему такие вещи вообще случаются — потому что дело не в чьей-то отдельной глупости, а в понятной цепочке маленьких решений.

  1. «Временное» решение прижилось. Сервис подняли на свободном узле, потому что заводить для него отдельный VPS ради одного маленького демона казалось излишним. Формальной задачи «перенести» никто не завёл — она осталась в переписке, а не в трекере.
  2. Локальный Redis выглядел как надёжность, а не как риск. Разработчик, писавший sms-relay, добавил очередь на диске именно для устойчивости к сбоям — и был прав насчёт сетевых сбоев. Но никто не задал вопрос «а что если ляжет сам хост с очередью», потому что фокус был на устойчивости самого сервиса, а не всей цепочки целиком.
  3. Мониторинг мониторил всё, кроме самого канала оповещения. Дашборды показывали аптайм приложения, диска, базы — но не было ни одной проверки в духе «оповещения вообще доходят». Дежурный узнавал, что алерт не дошёл, только когда это уже случалось.
  4. Не было независимого «сторожа». Ни здесь, ни у большинства команд на старте нет отдельного heartbeat-механизма, который бы сам поднимал тревогу, если sms-relay не подавал признаков жизни дольше нескольких минут. Именно такую роль обычно играют внешние сервисы дедмен-свитчей — например, разобранный в статье про мониторинг cron-задач через healthchecks.io: сервис сам должен регулярно «отмечаться», а если пропустил отметку — сработает отдельный алерт с независимой инфраструктуры.

Ни один из этих четырёх пунктов сам по себе не выглядит катастрофой. Вместе они и дали 40 минут тишины в момент, когда служба оповещения была нужнее всего.

Что изменили после разбора

По итогам разбора приняли несколько конкретных мер — без иллюзий, что теперь «такое не повторится», но с целью сократить и вероятность, и длительность подобных инцидентов в будущем.

Вынесли sms-relay на отдельный узел. Причём не просто на другой сервер в том же дата-центре, а на отдельный небольшой VPS в другой локации — логика простая: если авария случится на уровне дата-центра или сети провайдера, канал оповещения не должен зависеть от той же инфраструктуры. Для такой роли не нужен мощный сервер — годится минимальная конфигурация, лишь бы она физически не пересекалась с продакшеном.

Добавили независимую проверку самого канала оповещения. Раз в 5 минут sms-relay теперь сам делает ping на внешний heartbeat-сервис; если отметка не приходит дольше 15 минут, срабатывает отдельный алерт по другому каналу — на Telegram-бота, который тоже живёт отдельно от боевых узлов. Настройка такого бота на VPS разобрана в статье про алерты в Telegram на VPS: важное условие — бот и токен для него живут не на мониторимой инфраструктуре.

Убрали локальную очередь как единственную страховку. Оставили небольшой буфер в памяти на случай секундных сетевых сбоев, но отказались от идеи, что диск на том же узле — это надёжное место для хранения недоставленных уведомлений. Если sms-relay не смог отправить сообщение за разумное время, он теперь дублирует попытку через второй, резервный SMS-шлюз с другим API — на случай проблем именно у провайдера.

Завели чек-лист по размещению критичных сервисов. Для всего, что относится к цепочке «инцидент → оповещение → человек», теперь действует простое правило при код-ревью инфраструктурных изменений: ни один компонент этой цепочки не может физически жить на узле, аварию которого он должен обнаруживать. Проверить, откуда конкретно ушло уведомление, стало проще — админка sms-relay пишет в лог не только текст сообщения, но и hostname, с которого была сделана отправка.

Ниже — сравнение архитектуры до и после разбора инцидента.

До инцидентаПосле разбора
Где живёт sms-relayНа app-01, том же узле, что и продакшен-приложениеНа отдельном VPS в другой локации
Очередь недоставленных вебхуковЛокальный Redis на том же узлеБуфер в памяти + резервный SMS-шлюз
Проверка самого канала оповещенияОтсутствовалаHeartbeat раз в 5 минут, отдельный алерт при пропуске
Резервный канал, если SMS не ушлаНе былоTelegram-бот на отдельной инфраструктуре
Кто знает, откуда ушло сообщениеНикто, лог был минимальнымHostname отправителя пишется в каждую запись лога

Отдельный узел под такие вспомогательные сервисы не обязан быть большим или дорогим — по сути это страховка ценой одного недорогого VPS, которая должна отбить себя уже на первом инциденте, где она сработает вовремя.

Нужен сервер под эту задачу?

Разверните VPS MAATRIX за пару минут: NVMe, AMD EPYC, root-доступ, локации UK, США, Франция и РФ. Оплата картой РФ и по СБП.

Арендовать сервер

Нужны сами нейросети для контента?

Генерируйте изображения, видео и озвучку нейросетями на falapi.io — десятки моделей в одном окне. Оплата картой РФ и по СБП.

Частые вопросы

Разве Alertmanager не должен сам слать SMS напрямую, без промежуточного сервиса?

Некоторые интеграторы SMS-шлюзов поддерживают вебхуки в формате, совместимом с Alertmanager webhook_configs напрямую, но чаще нужен адаптер, который приводит формат payload'а к тому, что ожидает конкретный провайдер, и добавляет логику вроде дедупликации повторных срабатываний. Именно этот адаптер и стал слабым местом в разобранном случае — проблема не в самом принципе «промежуточный сервис», а в том, где он физически размещён.

Достаточно ли просто вынести sms-relay на отдельный узел, или нужно ещё что-то?

Отдельный узел закрывает главный риск — совпадение точки отказа с точкой оповещения, — но без независимой проверки самого канала («жив ли sms-relay прямо сейчас») риск снова накапливается незаметно, просто медленнее. Оба изменения — размещение и heartbeat — стоит делать вместе.

Что если резервный Telegram-бот тоже окажется на том же узле, что и sms-relay?

Тогда single point of failure просто переедет на уровень выше — вместо одного узла у вас будет одна точка отказа для двух каналов сразу. Резервные каналы оповещения должны физически и организационно не пересекаться друг с другом: разные узлы, а по возможности и разные провайдеры хостинга.

Как понять, что похожая проблема уже есть в вашей инфраструктуре, до того как она проявится в реальном инциденте?

Возьмите список всего, что участвует в доставке алерта дежурному — от правила Prometheus до SMS-шлюза, — и для каждого звена честно ответьте на вопрос «а что если упадёт узел, на котором это работает». Если ответ «тогда это же звено и должно было сообщить об этом падении» — вы нашли точную копию разобранного здесь сценария.

Стоит ли вообще держать SMS как канал оповещения, если есть Telegram и почта?

SMS остаётся полезной именно как канал последней надежды, потому что не зависит от того, установлено ли у дежурного приложение и есть ли интернет — только сотовая сеть. Но её ценность в этой роли ровно такая, насколько независима вся цепочка её доставки от инфраструктуры, за которой она следит.

Обсудить статью, задать вопрос или начать новую тему

Есть вопрос по этой статье, идея для обсуждения или просто хотите поделиться опытом? Сообщество MAATRIX ждёт. Для общения, пожалуйста, зарегистрируйтесь в нашем личном кабинете.

Перейти в сообщество →