Алерт ушёл в чат, который никто не открывал четыре месяца
Мониторинг настроен, алерты уходят, бот отвечает "200 OK" — а сервис всё равно падает, и об этом узнают из тикетов в поддержку, а не из системы оповещений. Самое неприятное в таких историях — что при разборе выясняется: система работала правильно от начала и до конца. Просто уведомления шли туда, куда их уже давно никто не читал. Разберём конкретный инцидент с такой конфигурацией: что сломалось, что показывали логи, какие версии причины отбросили и что изменили, чтобы это не повторилось.
Содержание
Что сломалось: ночь перед распродажей
Небольшой интернет-магазин на связке Django-бэкенда и Postgres готовился к сезонной распродаже. Заранее подняли дополнительный воркер, прогрели кеш, предупредили поддержку о повышенной нагрузке. В два часа ночи, на пике первого всплеска трафика, основной процесс приложения на боевом сервере несколько раз подряд ушёл в перезапуск. Каждый рестарт — это порядка 20-30 секунд простоя на приём заказов, а на высокой нагрузке это ещё и очередь из повторных запросов от клиентов, которые не дождались ответа и обновили страницу.
Дежурный инженер узнал о проблеме не от мониторинга, а от службы поддержки: клиенты стали писать, что корзина "зависает" и оплата не проходит. Первая же проверка systemctl status app.service показала, что unit несколько раз падал с кодом выхода, характерным для сигнала SIGKILL, и автоматически поднимался заново благодаря Restart=on-failure. Перезапуск под нагрузкой означает потерянные сессии и часть неоплаченных заказов, которые клиент просто не стал повторять.
Дальше стандартная последовательность на инциденте: зафиксировать время первого сбоя, поднять логи, проверить график нагрузки, понять масштаб. И вот тут всплыл первый неприятный момент: канал, куда должны были прийти алерты о деградации, за это время не показал ничего нового — в телефоне не было ни одного уведомления. Как будто мониторинг вообще не сработал.
Что показывали логи и метрики
Первым делом посмотрели dmesg и journalctl -k на сервере — и сразу нашли причину рестартов:
$ journalctl -k --since "02:00" --until "02:20" | grep -i "out of memory\|oom"
kernel: Out of memory: Killed process 18422 (gunicorn) total-vm:2145184kB, anon-rss:1887420kB
kernel: oom_reaper: reaped process 18422 (gunicorn), now anon-rss:0kB
OOM killer убивал главный процесс gunicorn, systemd поднимал его заново — отсюда рестарты и просадки. Дальше логично посмотреть, как вело себя потребление памяти во времени. В Grafana подняли панель по node_memory_MemAvailable_bytes и по метрике самого приложения (RSS процесса через process_resident_memory_bytes из prometheus_client). График показал не резкий скачок в ночь инцидента, а плавный, растянутый во времени рост — потребление памяти процессом заметно ползло вверх на протяжении последних месяцев, и в ночь распродажи просто дошло до потолка, заданного MemoryMax в unit-файле systemd.
То есть падение в 2 часа ночи было не причиной, а следствием: процесс медленно "тёк" по памяти уже давно, а высокая нагрузка распродажи просто ускорила момент, когда он упёрся в лимит. Это меняет фокус расследования — важно понять не "почему упало сегодня", а "почему никто не заметил тренд раньше".
Проверили Alertmanager — и вот здесь начинается собственно детективная часть истории. В конфиге были правила на предупреждающий и критический пороги по памяти:
groups:
- name: app-memory
rules:
- alert: HighMemoryUsage
expr: process_resident_memory_bytes{job="app"} / 1024 / 1024 > 1200
for: 15m
labels:
severity: warning
- alert: CriticalMemoryUsage
expr: process_resident_memory_bytes{job="app"} / 1024 / 1024 > 1800
for: 5m
labels:
severity: critical
Запрос к самому Alertmanager (/api/v2/alerts) и его логи показали: правило HighMemoryUsage срабатывало и гасилось десятки раз за последние месяцы, а в ночь инцидента отработало и CriticalMemoryUsage. Алерты честно генерировались. Дальше проверили доставку — webhook-receiver, который дергает Telegram Bot API:
$ journalctl -u alertmanager --since "-30 days" | grep -i "notify"
level=info msg="Notify success" receiver=telegram-ops integration=webhook
Никаких ошибок доставки. Прямой запрос к Telegram Bot API тоже отработал штатно:
$ curl -s "https://api.telegram.org/bot<TOKEN>/getChat?chat_id=<CHAT_ID>"
{"ok":true,"result":{"id":-100xxxxxxxxxx,"type":"supergroup","title":"ops-alerts (old)"}}
Бот жив, токен рабочий, чат существует, сообщения туда доставлены с кодом 200. Формально — цепочка мониторинга отработала от метрики до доставки сообщения без единого сбоя. Проблема была не в технике доставки, а в том, что происходило с сообщением после того, как оно долетело.
Нужен сервер под эту задачу?
Разверните VPS MAATRIX за пару минут: NVMe, AMD EPYC, root-доступ, локации UK, США, Франция и РФ. Оплата картой РФ и по СБП.
Арендовать серверГипотезы, которые отбросили
Первая версия — memory leak появился из-за недавнего деплоя. Проверили git log по датам релизов и наложили их на график потребления памяти: рост начался задолго до последнего значимого релиза, деплои за три месяца ничего не меняли в характере кривой. Версию отбросили — утечка была системной, а не результатом конкретного коммита.
Вторая версия — аномальный трафик или DDoS, который разово раздул память под кеш ответов. Проверили access.log и метрики nginx по RPS за ночь инцидента: рост запросов был ожидаемым для распродажи, никаких аномальных паттернов, ботов или подозрительных User-Agent не нашли. Трафик был реальным и прогнозируемым, просто оказался той самой "последней каплей" для процесса, который и так был близок к лимиту.
Третья версия — что мониторинг вообще не настроен на память, то есть дыра в конфигурации. Отбросили сразу после проверки конфига Alertmanager и истории сработавших алертов — правила были, пороги были, алерты стреляли исправно на протяжении месяцев. Это самая коварная ложная версия в таких разборах: она снимает вопрос "почему никто не увидел" и подменяет его на "почему не было алерта", хотя алерт был.
Четвёртая версия — Telegram зарезал доставку из-за антиспам-лимитов бота (например, 429 Too Many Requests при частых повторных срабатываниях одного и того же алерта). Проверили логи webhook-receiver за весь период — ни одного 429, ни одной ошибки доставки. Бот укладывался в лимиты Telegram по количеству сообщений в чат в секунду, потому что предупреждения срабатывали не так часто, чтобы упереться в rate limit.
Отбросив все четыре версии, оставалась одна: сообщения доходили, но их никто не видел. Дальше расследование сместилось с "почему сломался сервис" на "почему не сработало оповещение о деградации, которая тянулась месяцами".
Как нашли реальную причину
Открыли тот самый Telegram-чат ops-alerts (old), в который стреляли алерты — и всё встало на свои места. В чате было несколько сотен непрочитанных сообщений, вперемешку алерты HighMemoryUsage, служебные уведомления от CI и старые обсуждения по инфраструктуре примерно четырёхмесячной давности. Последнее сообщение, которое кто-то фактически прочитал и на которое ответил, датировалось как раз концом того периода — дальше шёл сплошной поток автоматических сообщений без единого ответа человека.
Разбор истории команды дал простое организационное объяснение. Четыре месяца назад компания переезжала с общего Telegram-чата на отдельные тематические каналы: завели новый супергруппу ops-alerts-v2 для алертов и отдельный канал для общих обсуждений. Часть интеграций перенесли — CI/CD, деплой-бот, уведомления от биллинга. А вот webhook в Alertmanager остался нетронутым, потому что chat_id для него был прописан не в общем инфраструктурном репозитории, а прямо в переменной окружения на сервере мониторинга, в файле, который не входил в чек-лист миграции:
# /etc/alertmanager/telegram.env
TELEGRAM_CHAT_ID=-100xxxxxxxxxx # старый чат, никто не обновил
Отдельно выяснилось, что старый чат ещё и был заглушён (mute) одним из инженеров ещё до переезда — во время предыдущего периода повышенной "шумности" алертов, когда пороги были настроены слишком чувствительно и чат буквально засыпало нотификациями. Human-фактор здесь двойной: во-первых, никто не проверил, что при миграции все интеграции переехали вместе с людьми, а во-вторых, у оставшегося источника сообщений даже теоретически не было шанса быть замеченным, потому что телефон не показывал по нему всплывающих уведомлений.
Итого реальная причина раскладывается на два независимых, но одинаково важных слоя:
- Технический: медленная утечка памяти в приложении, которая долго не давала о себе знать под нормальной нагрузкой и проявилась только на пике.
- Организационный: канал доставки алертов о ней исправно работал, но вёл в чат, который физически никто не открывал — из-за незавершённой миграции интеграций и ранее выставленного mute.
Утечка сама по себе не была бы критичной, если бы предупреждающие алерты кто-то видел месяц-два назад на спокойном трафике. А "мёртвый" чат сам по себе не был бы проблемой, если бы приложение не текло по памяти.
Как чат "умер": анатомия обрыва цепочки
Стоит отдельно разобрать механику того, почему проверки на "мониторинг работает" ничего не показывают в такой ситуации. Стандартный синтетический тест мониторинга обычно проверяет именно то, что проверили в начале расследования: жив ли бот, отвечает ли Telegram API, доходит ли сообщение с кодом 200. Всё это — про доставку сообщения в канал. Ни один из этих тестов не отвечает на вопрос "прочитает ли это сообщение живой человек в разумное время".
Это системная слепая зона большинства настроек алертинга: между "сообщение доставлено" и "человек отреагировал" стоит целая цепочка допущений, каждое из которых может незаметно сломаться:
- у получателя включены уведомления от этого чата или канала;
- получатель вообще состоит в этом чате (а не был удалён при реорганизации команды);
- частота сообщений в чате не настолько высокая, чтобы алерт потерялся в потоке шума;
- есть хоть кто-то, для кого этот канал — рабочий, а не архивный.
В разобранном инциденте не сработало сразу три пункта из четырёх: уведомления были отключены, канал считался устаревшим после миграции, а поток непрочитанных сообщений скрывал единичный критический алерт среди сотен менее важных. При этом с точки зрения "мониторинга мониторинга" (если бы он вообще был) всё выглядело бы штатно — сообщение доставлено, ошибок нет.
Здесь же сработал классический эффект "алерт без наблюдателя не отличается от отсутствия алерта" — с точки зрения бизнес-результата разницы никакой, хотя формально система оповещения была настроена правильно.
Что изменили после разбора
По итогам инцидента правки внесли в трёх направлениях: техническое (утечка), доставка алертов и процесс миграций.
Утечку памяти в приложении локализовали профилированием (tracemalloc в Python-процессе на тестовом окружении с воспроизведением похожей нагрузки) и нашли неограниченный по размеру in-memory кеш ответов, который никогда не очищался и не имел TTL. Добавили maxsize и вытеснение по LRU, а также ограничение через MemoryMax в systemd-юните с более консервативным порогом и явным алертом заранее.
По доставке алертов сделали несколько изменений:
- Завели отдельный heartbeat-алерт — правило, которое не про проблему, а про то, что цепочка мониторинга жива: раз в сутки в тот же канал уходит контрольное сообщение, и отдельная проверка (внешний healthcheck-сервис) следит, что оно действительно приходит. Если heartbeat не пришёл — это сигнал, что сама цепочка доставки сломана, независимо от того, есть реальные инциденты или нет.
- Убрали привязку
chat_idк переменной окружения на конкретном сервере и перенесли конфигурацию Alertmanager, включая receivers, в общий инфраструктурный репозиторий, который проверяется при каждом изменении команды состава чатов. - Ввели правило: критические алерты дублируются в два независимых канала разной природы (мессенджер и e-mail или SMS через отдельный сервис), чтобы отказ одного канала доставки не был единой точкой отказа.
- Добавили обязательное подтверждение (реакция или короткий ответ дежурного) на критические алерты, а отсутствие реакции в течение заданного окна эскалирует уведомление на следующего человека в списке дежурства.
По процессу миграций добавили пункт в чек-лист переезда команды между чатами: явный аудит всех automated-интеграций, которые постят в чат — CI/CD, биллинг, мониторинг, деплой-боты — с проверкой, что каждая указывает на актуальный канал после переезда, а не только на то, что люди физически перешли в новый чат.
Отдельно провели ревизию всех чатов на предмет "мёртвых" интеграций: сверили chat_id/webhook-адреса в Alertmanager, Zabbix и cron-задачах с реально активными каналами. Нашли ещё две похожие ситуации — уведомления от бэкап-скрипта и от сертификатов Let's Encrypt тоже уходили в устаревшие каналы. Поправили в рамках той же ревизии, не дожидаясь отдельного инцидента по каждой.
Нужен сервер под эту задачу?
Разверните VPS MAATRIX за пару минут: NVMe, AMD EPYC, root-доступ, локации UK, США, Франция и РФ. Оплата картой РФ и по СБП.
Арендовать серверНужны сами нейросети для контента?
Генерируйте изображения, видео и озвучку нейросетями на falapi.io — десятки моделей в одном окне. Оплата картой РФ и по СБП.
Частые вопросы
Как вообще заметить, что алерты уходят в "мёртвый" канал, если формально всё работает?
Регулярно проверяйте не только логи доставки, но и активность в самом канале — если последнее сообщение от человека там было несколько месяцев назад, а бот продолжает постить, это тревожный признак. Heartbeat-алерт с внешней проверкой (healthchecks.io или аналог) полезнее любых внутренних логов, потому что проверяет весь путь снаружи цепочки.
Нужно ли дублировать критические алерты сразу в несколько каналов?
Для действительно критических событий — да, хотя бы в два независимых по природе канала (например, мессенджер и SMS или e-mail), потому что отказ одного канала доставки не должен становиться единственной причиной пропущенного инцидента. Для предупреждающего уровня дублирование обычно избыточно и создаёт лишний шум.
Что делать, если алерты слишком шумные и их всё равно заглушают?
Шумные алерты — отдельная и очень частая причина, по которой каналы в итоге мьютят вручную; стоит пересмотреть пороги и логику for: в правилах, а не мириться с mute как с постоянным решением. Тема разобрана отдельно в статье про пороги алертов, которые не бесят.
Как выстроить дежурство так, чтобы критический алерт точно кто-то увидел?
Помогает эскалация с обязательным подтверждением: если дежурный не отреагировал за заданное окно, уведомление автоматически уходит следующему человеку по списку. Базовые схемы для небольших команд разобраны в статье про дежурство и эскалацию в маленькой команде.
Чем эта ситуация отличается от классического "мониторинг ничего не заметил"?
Принципиально: здесь мониторинг сработал правильно на каждом техническом шаге — от метрики до HTTP 200 при отправке сообщения. Проблема была не в детектировании и не в доставке, а в человеческом звене после доставки. Это стоит отличать от случаев, когда система вообще не настроена на нужную метрику — разбор такого антипаттерна есть в статье про мониторинг, который никто не смотрит.
Обсудить статью, задать вопрос или начать новую тему
Есть вопрос по этой статье, идея для обсуждения или просто хотите поделиться опытом? Сообщество MAATRIX ждёт. Для общения, пожалуйста, зарегистрируйтесь в нашем личном кабинете.
Перейти в сообщество →