Мониторинг проверял главную страницу, а лежала оплата
Дашборд мониторинга был весь зелёный: сайт открывался, сервер отвечал, аптайм за месяц — 99,98%. А в это время покупатели по полчаса крутили спиннер на кнопке «Оплатить» и уходили. Разница между «сайт жив» и «бизнес работает» — это ровно та щель, в которую проваливаются самые обидные инциденты. Разберём по шагам, что мы видели, какие версии отбросили и в чём была настоящая причина.
Содержание
Что увидели: тишина мониторинга и растущая очередь жалоб
Первый сигнал пришёл не из мониторинга, а из чата поддержки: три обращения подряд с формулировкой «не могу оплатить заказ, кнопка крутится и ничего не происходит». Дежурный админ первым делом открыл дашборд Uptime Kuma — там всё было ровно и зелёно, последняя проверка главной страницы прошла 40 секунд назад с кодом 200.
Открыли Grafana с бизнес-метриками (количество успешных заказов в час, отдельная панель, которую в компании смотрели раз в день, а не в реальном времени) — и там график с 13:50 упал почти в ноль. При этом панель с общим числом запросов к nginx оставалась на обычном уровне: пользователи заходили, листали каталог, но не могли завершить оплату.
Это первая и главная развилка инцидента: инфраструктурный мониторинг проверял факт ответа сервера, а не факт работы конкретной функции. Сайт в широком смысле был жив — отдавал HTML, статику, каталог товаров. Но узкий, критичный для бизнеса путь — создание платежа — не работал уже почти час, и об этом никто не знал, пока не начали копиться тикеты.
Проверка в мониторинге выглядела примерно так:
{
"type": "http",
"url": "https://shop.example.com/",
"method": "GET",
"interval": 60,
"expectedStatusCodes": [200]
}
Она честно делает то, что в неё заложено: раз в минуту забирает главную страницу и проверяет код ответа. К оплате, которая живёт на отдельном эндпоинте /api/payment/create и обслуживается отдельным процессом, эта проверка не имеет никакого отношения.
Первая гипотеза: виноват платёжный шлюз
Первая мысль дежурного — проблема на стороне эквайринга: банк или платёжный агрегатор отвечает с ошибками или недоступен. Версия логичная: именно так чаще всего выглядит «оплата не проходит» с точки зрения пользователя.
Проверили статус-страницу платёжного провайдера — все системы зелёные, инцидентов не заявлено. Написали в чат поддержки провайдера с вопросом, есть ли проблемы с приёмом запросов от нашего мерчант-аккаунта — получили ответ, что за последний час от нашего идентификатора запросов не поступало вообще. Это важная деталь: не «получали и отклоняли», а «не получали». Значит, проблема раньше, чем запрос долетает до банка.
Заодно проверили логи исходящих запросов на своей стороне:
grep "payment/create" /var/log/app/payment-service.log | tail -n 50
Файл лога не обновлялся почти час — последняя строка датирована 13:52. Процесс либо не получает запросы, либо не пишет логи, либо не работает вовсе. Версия про шлюз отпала: до шлюза дело просто не доходило.
Нужен сервер под эту задачу?
Разверните VPS MAATRIX за пару минут: NVMe, AMD EPYC, root-доступ, локации UK, США, Франция и РФ. Оплата картой РФ и по СБП.
Арендовать серверВторая и третья гипотезы: кеш CDN и правило файрвола
Вторая версия — устаревший кеш CDN отдаёт клиентам старую версию страницы оформления заказа, из-за которой форма шлёт запросы не туда. У сайта действительно настроен CDN перед бэкендом, и с кешированием на проекте раньше уже бывали сюрпризы.
Проверили заголовки ответа для страницы оформления:
curl -sI https://shop.example.com/checkout | grep -i cache
cache-control: no-store, no-cache, must-revalidate
x-cache-status: BYPASS
Страница оформления заказа и API платежей изначально исключены из кеширования правилом в конфиге CDN — это специально настраивали раньше именно из-за похожего случая. Версия с кешем отпала за пять минут.
Третья версия — новое правило файрвола или fail2ban заблокировало IP-диапазон, из которого шли запросы к /api/payment/create. Проверили:
sudo iptables -L -n | grep -i drop
sudo fail2ban-client status nginx-req-limit
journalctl -u fail2ban --since "14:00" | grep -i ban
Ни одной свежей блокировки, связанной с нужным путём или диапазоном IP, не нашли. Правила файрвола не менялись со вчерашнего дня, а бан-лист пуст. Три версии отброшены за 20 минут — и это нормальная часть разбора инцидента: дешёвые гипотезы проверяются первыми именно потому, что их легко закрыть и не тратить время дальше.
Что показали логи и systemd
Раз проблема не на стороне шлюза, кеша и файрвола, а лог платёжного сервиса молчит — логично посмотреть на сам процесс. Сервис оплаты вынесен в отдельный systemd-юнит, за него отвечает nginx как reverse proxy на upstream 127.0.0.1:4010.
sudo systemctl status payment.service
● payment.service - Payment processing service
Loaded: loaded (/etc/systemd/system/payment.service; enabled)
Active: failed (Result: start-limit-hit) since Tue 2026-08-25 13:52:41 UTC; 47min ago
Process: 18422 ExecStart=/usr/bin/node /opt/app/payment/server.js (code=killed, signal=KILL)
Вот она, разгадка в одну строку: failed (Result: start-limit-hit). Сервис не просто упал — он упал, systemd честно попытался его перезапустить несколько раз подряд, уткнулся в лимит перезапусков, признал юнит окончательно неисправным и прекратил попытки. С 13:52 процесс просто не существовал, и никто не пытался поднять его руками, потому что никто не знал, что он лежит.
Посмотрели, что было до этого:
journalctl -u payment.service --since "13:40" --until "13:53"
13:47:02 payment.service: server.js: unhandled promise rejection in retryAcquirerCall()
13:48:15 kernel: Out of memory: Killed process 18391 (node) total-vm:2874112kB, anon-rss:1972340kB
13:48:16 systemd: payment.service: Main process exited, code=killed, status=9/KILL
13:48:16 systemd: payment.service: Scheduled restart job, restart counter is at 3.
13:49:40 kernel: Out of memory: Killed process 18405 (node) total-vm:2861004kB, anon-rss:1968012kB
13:50:55 kernel: Out of memory: Killed process 18414 (node) total-vm:2855776kB, anon-rss:1954880kB
13:52:41 systemd: payment.service: Start request repeated too quickly.
13:52:41 systemd: payment.service: Failed with result 'start-limit-hit'.
Картина сложилась. В 13:47 выкатили небольшой релиз с изменением логики повторных запросов к эквайринговому API (retryAcquirerCall) — при определённых сетевых таймаутах функция уходила в повторные вызовы без ограничения глубины и без освобождения ссылок на предыдущие попытки, из-за чего память процесса Node.js быстро росла. Через пять минут под нагрузкой обычных дневных заказов процесс упирался в потолок памяти контейнера, ядерный OOM-killer убивал его, systemd поднимал заново — и цикл повторялся, пока не сработал лимит StartLimitBurst (по умолчанию 5 попыток за 10 секунд в современных дистрибутивах).
Настоящая причина: незаметный сбой и мониторинг, который об этом не спросили
Итоговая причинно-следственная цепочка выглядит так:
- Релиз в 13:47 внёс баг в логику ретраев к API эквайера — при таймауте функция рекурсивно повторяла вызов без ограничения и без очистки памяти.
- Память процесса
payment.serviceросла за минуты, ядро убивало процесс через OOM-killer. - systemd честно перезапускал сервис несколько раз подряд и, упёршись в
StartLimitBurst, перевёл юнит вfailed, прекратив попытки без какого-либо внешнего уведомления. - nginx как reverse proxy на упавший upstream отвечал
502 Bad Gatewayтолько на пути/api/payment/*— но именно это никто не проверял. - Проверка мониторинга ходила на
/, которую отдавал совершенно другой, живой и здоровый процесс (статический фронтенд), поэтому статус оставался зелёным всё это время.
Ни одна из отдельных частей системы не соврала. OOM-killer сделал ровно то, для чего он существует — защитил хост от исчерпания памяти. systemd сделал ровно то, что написано в его юните, — попытался восстановить сервис и остановился, чтобы не долбить систему бесконечными рестартами мёртвого процесса. Мониторинг тоже не соврал: страница действительно отдавала 200. Проблема была не в поломке отдельного компонента, а в том, что ни один из них не был обязан заметить и сообщить о падении именно платёжного пути — а никто не настроил отдельную проверку для него.
Полезно сравнить, что видел каждый уровень наблюдения в момент инцидента:
| Уровень мониторинга | Что проверял | Что видел в 14:20 |
|---|---|---|
HTTP-проверка GET / | Код ответа главной страницы | 200 OK, всё зелёно |
| Общая метрика запросов nginx | Суммарное число запросов в секунду | В норме — трафик на сайт не падал |
systemd юнит payment.service | Никто не проверял снаружи | failed, но алерта об этом не было |
| Бизнес-метрика «заказы в час» | Смотрели вручную раз в день | Провал почти до нуля с 13:50 |
Логи payment-service.log | Никто не мониторил на отсутствие записей | Файл не обновлялся 47 минут |
Именно средняя строка — состояние systemd-юнита — оказалась самым дешёвым и самым быстрым индикатором, но она не была подключена ни к какому алертингу.
Что изменили после инцидента
Первым делом руками перезапустили сервис и сбросили счётчик отказов:
sudo systemctl reset-failed payment.service
sudo systemctl restart payment.service
sudo systemctl status payment.service
Дальше — три группы изменений, каждая закрывает свою дыру в наблюдаемости.
Синтетическая проверка критичного пути, а не только факта ответа сервера. Добавили в Uptime Kuma отдельный монитор с фактическим прогоном запроса к /api/payment/health — эндпоинту, который сервис оплаты сам открывает и который в реальном времени проверяет доступность своей БД-очереди и факт готовности принимать запросы (не настоящий платёж, а лёгкая внутренняя проверка готовности):
{
"type": "http",
"url": "https://shop.example.com/api/payment/health",
"method": "GET",
"interval": 30,
"expectedStatusCodes": [200],
"keyword": "\"status\":\"ready\""
}
Проверка не только смотрит на код ответа, но и ищет конкретную строку в теле — это отсекает ситуацию, когда какой-нибудь общий обработчик ошибок начнёт отдавать 200 с телом «упс».
Алерт на состояние systemd-юнита, а не только на HTTP. Через node_exporter со включённым --collector.systemd состояние юнита стало метрикой Prometheus, и на неё завели простое правило:
- alert: PaymentServiceDown
expr: node_systemd_unit_state{name="payment.service", state="failed"} == 1
for: 1m
labels:
severity: critical
annotations:
summary: "payment.service в состоянии failed"
Это перекрывает как раз тот случай, когда процесс лежит мёртвым и systemd уже даже не пытается его поднять — раньше об этом не узнавал вообще никто, кроме того, кто вручную зайдёт и наберёт systemctl status.
Ограничение памяти и алерт на приближение к потолку, а не только на факт убийства процесса. В юнит добавили жёсткий лимит и предупреждение заранее:
[Service]
MemoryMax=1500M
MemoryHigh=1200M
Restart=on-failure
RestartSec=5
StartLimitIntervalSec=60
StartLimitBurst=5
MemoryHigh заставляет ядро активнее подрезать процесс через управление cgroup ещё до жёсткого килла, а метрика потребления памяти процесса теперь тоже смотрится в Grafana с алертом при приближении к MemoryMax, чтобы утечка была видна за минуты до того, как дело дойдёт до OOM-killer.
Отдельно договорились, что бизнес-метрика заказов в час переезжает из панели «смотрим раз в день» в панель с алертом: падение больше чем на 50% относительно скользящего среднего за предыдущий час — повод для срабатывания дежурному, независимо от того, что показывает инфраструктурный мониторинг. Это тот случай, когда мониторинг доступности сайта технически работал безупречно, но не отвечал на главный вопрос бизнеса: зарабатывает ли сайт деньги прямо сейчас.
Похожая по сути ловушка разбиралась и в другом случае, когда healthcheck проверял не тот показатель, а контейнер продолжал считаться живым — тот же принцип: проверка отвечает буквально на заданный ей вопрос, и если вопрос сформулирован узко, широкая проблема пройдёт мимо. Общий антипаттерн, в который часто вырастает такая настройка мониторинга «для галочки», разобран в статье про мониторинг, на который никто на самом деле не смотрит — в нашем случае дашборд как раз смотрели, просто он не был настроен задавать нужный вопрос.
Нужен сервер под эту задачу?
Разверните VPS MAATRIX за пару минут: NVMe, AMD EPYC, root-доступ, локации UK, США, Франция и РФ. Оплата картой РФ и по СБП.
Арендовать серверНужны сами нейросети для контента?
Генерируйте изображения, видео и озвучку нейросетями на falapi.io — десятки моделей в одном окне. Оплата картой РФ и по СБП.
Частые вопросы
Почему просто не мониторить все эндпоинты сразу?
Мониторить стоит не всё подряд, а критичные для бизнеса пути: оплату, авторизацию, оформление заказа, вебхуки от внешних систем. Проверка каждого второстепенного API раздувает число ложных срабатываний и усталость от алертов сильнее, чем помогает — важно выбрать 3-5 по-настоящему критичных точек и проверять именно их.
Чем плоха проверка GET / как единственный монитор?
Она хорошо ловит полный отказ сервера (сеть, процесс nginx, диск), но ничего не знает о состоянии отдельных сервисов за прокси. Если у вас монолит на одном процессе — риск ниже, но при разделении на отдельные сервисы (оплата, поиск, авторизация) каждый из них может упасть независимо, оставаясь невидимым для общей проверки.
Что такое StartLimitBurst в systemd и почему он не зло?
Это защита от бесконечного цикла перезапусков сломанного процесса, который иначе будет крутить CPU и плодить логи вечно. Проблема не в самом лимите, а в том, что достижение этого лимита обычно не подключено ни к какому алертингу — юнит тихо остаётся в failed до первого ручного systemctl status.
Можно ли было поймать утечку памяти до релиза?
Отчасти да — нагрузочное тестирование эндпоинта payment/create с эмуляцией таймаутов от эквайера с высокой вероятностью показало бы рост памяти за несколько минут прогона. Но даже без этого шага метрика памяти процесса с алертом на приближение к лимиту сократила бы время обнаружения с 47 минут до одной-двух.
Нужен ли отдельный сервер под мониторинг, если и так есть VPS под приложение?
Если мониторинг и приложение живут на одном хосте, авария самого хоста убьёт и наблюдаемость вместе с сервисом — вы не получите даже финального алерта. Для проверок вроде описанной выше синтетический монитор лучше держать на отдельной машине или использовать внешний сервис проверки доступности.
Обсудить статью, задать вопрос или начать новую тему
Есть вопрос по этой статье, идея для обсуждения или просто хотите поделиться опытом? Сообщество MAATRIX ждёт. Для общения, пожалуйста, зарегистрируйтесь в нашем личном кабинете.
Перейти в сообщество →