Мониторинг личного сервиса обычно устроен так: пользователь пишет «у тебя сайт не работает», вы идёте смотреть. Это работает ровно до того момента, когда сбой оказывается тихим — сервис отвечает, страница открывается, а делает он не то.
Задача здесь не в полноте метрик. Дашборд с сорока графиками, на который никто не смотрит, стоит ровно ноль. Задача в том, чтобы вы вообще узнали о сбое, и узнали по каналу, который в момент сбоя работает. Ниже — минимальный набор из семи проверок, каждая с порогом и с объяснением, какой конкретно отказ она ловит.
#Что понадобится
- Ручка живости в приложении, которая отдаёт версию сборки. Без версии она бесполезна, и ниже видно почему.
- Планировщик, который умеет запускать скрипт по расписанию (
cron, таймер systemd, задача расписания Windows). - Канал оповещения, до которого машина реально дотягивается. Это отдельная и неочевидная проблема — раздел про неё ниже.
#Шаг 1. Признать, что порт не доказывает ничего
Самая распространённая проверка — «отвечает ли порт». Она ловит ровно один сценарий: процесс умер и никто не занял сокет. Всё остальное она пропускает.
Реальный случай из эксплуатации этого проекта. После обновления кода деплой рапортовал успех, порт был занят, сайт открывался — и отдавал версию недельной давности. Причина: старый процесс работал с повышенными правами в служебной сессии, обычный терминал его не убил, а taskkill при этом напечатал SUCCESS и вернул нулевой код. Скрипт деплоя добросовестно поверил коду возврата.
Вывод, который стоит записать в регламент: проверка живости обязана отдавать версию, а деплой обязан её сверять. У Persona для этого две независимые проверки, и обе должны сойтись с версией в исходниках:
/healthz— версия процесса, который реально слушает порт;- параметр
?v=в HTML страницы — то, что уедет в браузер вместе со статикой.
Первая ловит осиротевший процесс. Вторая ловит ситуацию, когда процесс новый, а браузеру и сервис-воркеру достаётся закэшированная старая статика.
#Шаг 2. Семь проверок с порогами
Порядок здесь — по стоимости отказа, а не по сложности реализации.
| # | Проверка | Порог тревоги | Какой отказ ловит |
|---|---|---|---|
| 1 | HTTP 200 на ручке живости | 3 неудачи подряд | процесс умер или завис |
| 2 | Версия в ответе == версия сборки | любое расхождение | тихий стухший деплой |
| 3 | Свободное место на диске данных | ниже 10% или 5 ГБ | база перестанет писать |
| 4 | Срок сертификата | меньше 14 дней | автопродление сломалось |
| 5 | Время ответа на дешёвый запрос | выше 2× обычного | блокировки базы, своп, деградация |
| 6 | Возраст последней успешной фоновой задачи | больше двух периодов | воркер тихо умер |
| 7 | Свежесть резервной копии | старше суток | бэкап перестал делаться месяц назад |
Порог в первой строке — не осторожность, а компромисс. У нас watchdog считает сервис упавшим после трёх неудачных проб подряд при периоде в минуту, то есть сайт лежит около трёх минут, прежде чем его перезапустят. Это осознанный размен: при пороге в одну пробу медленный ответ под нагрузкой убивал здоровый сервер, тот уходил в холодный старт, отвечал ещё медленнее — и проверка убивала его снова. Если три минуты для вас много, снижайте порог до двух проб и укорачивайте период, но помните, чем вы за это платите.
Шестая проверка — самая недооценённая. Веб-часть падает громко, фоновый воркер умирает беззвучно: сайт работает, а обработка новых данных встала три недели назад. Ловится только по времени последней успешной итерации, записанному самим воркером.
Седьмая — про то же самое, но дороже: резервная копия, которая перестала делаться, обнаруживается в день, когда она понадобилась.
#Шаг 3. Собрать это без внешнего сервиса
Внешний мониторинг удобен, но у него есть свойство, которое для личного сервиса иногда решающее: он видит ваши адреса, ваше время отклика и обычно требует агента на машине. Схема без внешних зависимостей выглядит так:
#!/usr/bin/env sh
# /opt/persona/ops/check.sh — запускается по расписанию раз в 5 минут
set -eu
URL=http://127.0.0.1:8000/healthz
WANT=$(sed -n 's/^__version__ = "\(.*\)"/\1/p' /opt/persona/app/__init__.py)
STATE=/var/lib/persona/check.state
fail() { echo "$(date -Iseconds) ALERT: $1" >> /var/lib/persona/alerts.log; notify "$1"; }
BODY=$(curl -fsS --max-time 5 "$URL" 2>/dev/null) || { fail "healthz недоступен"; exit 1; }
GOT=$(printf '%s' "$BODY" | sed -n 's/.*"version":"\([^"]*\)".*/\1/p')
[ "$GOT" = "$WANT" ] || fail "версия расходится: отдаётся $GOT, ожидается $WANT"
FREE=$(df -P /var/lib/persona | awk 'NR==2 {print $4}')
[ "$FREE" -gt 5242880 ] || fail "меньше 5 ГБ на диске данных"
echo "$(date -Iseconds) ok $GOT" > "$STATE"
Ключевая деталь — не сам скрипт, а функция notify. Она должна пользоваться каналом, который в момент сбоя работает.
#Шаг 4. Выбрать канал оповещения, который действительно доставляет
Здесь нас поймали, и история стоит пересказа целиком.
Оповещения по почте — очевидный выбор. Конфигурация была заполнена, приложение считало почту настроенной, тесты отправки писались как «отправлено в фоне». В реальности исходящий порт 587 на этой машине закрыт на сетевом уровне провайдера, и отправка честно падала примерно через шестнадцать секунд — то есть по таймауту соединения, а не с внятной ошибкой.
Два следствия, оба неприятные:
- Любая операция, ждавшая отправки письма, висела шестнадцать секунд. Отправку пришлось делать фоновой задачей с собственным потолком в десять секунд, чтобы посетитель не ждал на ней.
- Механика проверки почты «настроено» отвечала утвердительно, потому что галочка стояла и хост был указан. Пришлось переделать проверку так, чтобы «можем ли мы отправить» и «отправка на самом деле» решались одним и тем же кодом — иначе эти два ответа неизбежно расходятся.
Практический вывод для мониторинга: проверьте канал оповещения тем же способом, каким им будете пользоваться, и сделайте это до того, как он понадобится. И заведите канал, который не зависит от той же сети и того же процесса: сообщение в мессенджер по исходящему HTTPS, запись в файл, который вы читаете при входе на машину, локальное уведомление. Почтовое оповещение с сервера, у которого закрыт исходящий SMTP, — это ноль оповещений, выглядящий как настроенный мониторинг.
#Проверка результата
Мониторинг проверяется единственным честным способом — сломать сервис нарочно и посмотреть, узнали ли вы об этом.
- Остановите приложение. Ожидание: тревога по первой проверке за время, равное периоду, умноженному на порог.
- Запустите заведомо старую версию. Ожидание: тревога по второй проверке, даже если сайт полностью работоспособен.
- Займите диск большим файлом до порога. Ожидание: тревога по третьей.
- Остановите фонового воркера, не трогая веб. Ожидание: тревога по шестой через два его периода.
Если хотя бы один из четырёх сценариев прошёл незамеченным, у вас не мониторинг, а его имитация. Пункт 2 проваливают чаще всего, потому что версия в ручке живости либо не отдаётся, либо не сверяется.
#Что может пойти не так
Проверка ходит через прокси и меряет прокси. Ручку живости дёргайте с самой машины по локальному адресу, минуя прокси, а доступность снаружи проверяйте отдельно и другой проверкой. Иначе одна тревога будет означать два разных отказа.
Проверка живости ходит в базу. Тогда медленная база выглядит как мёртвое приложение, и watchdog начнёт перезапускать здоровый процесс. Ручка живости должна быть дешёвой и не трогать ни базу, ни воркеров; состояние зависимостей — это отдельная, более редкая проверка. Почему база вообще становится медленной и что с блокировками — в разборе WAL и конкурентного доступа в SQLite.
Пороги выставлены «на всякий случай» узко. Пять ложных тревог — и вы перестаёте их читать. Лучше меньше проверок с честными порогами, чем много с шумными.
Тревоги приходят, но по ним нечего делать. Каждая проверка должна иметь заранее записанное действие: перезапустить, освободить место, продлить сертификат. Если действия нет, проверка не нужна.
Первый запрос после простоя всегда медленный. Это не отказ, а холодный старт, и лечится он не порогами — приёмы разобраны в гайде про холодный старт приложения.
Тревога есть, а причины в логах не найти. Значит логи не структурированы: расследование по текстовому журналу занимает часы вместо минут. Минимальный набор обязательных полей — в статье про структурированные логи.
#Частые вопросы
#Сколько проверок нужно личному сервису на самом деле?
Семь из таблицы выше покрывают подавляющее большинство отказов, которые случаются в первый год. Расширять список стоит только по факту: случился отказ, которого проверки не увидели, — добавляется проверка на него. Расширять «впрок» бессмысленно.
#Как часто запускать проверки?
Раз в минуту для живости, раз в пять минут для остальных, раз в сутки для сертификата и резервной копии. Более частый опрос не даёт выигрыша, но создаёт нагрузку и увеличивает шанс ложной тревоги под пиком.
#Нужен ли отдельный сервер для мониторинга?
Для личного сервиса — обычно нет, и это осознанный компромисс: проверки с той же машины не увидят отказ сети или всей машины целиком. Компенсируется одной внешней проверкой доступности с другого устройства — например, с домашнего мини-ПК или телефона по расписанию.
#Что делать, если сервис падает раз в неделю без видимой причины?
Не поднимать порог, а начать собирать состояние до падения: потребление памяти, число открытых соединений, время ответа. Внезапные падения раз в неделю почти всегда оказываются медленным ростом чего-то, что упирается в потолок — памяти, дескрипторов, места на диске.
#Обязательно ли перезапускать сервис автоматически?
Полезно, но с двумя оговорками. Во-первых, автоперезапуск маскирует причину: сервис, который перезапускается трижды в сутки, выглядит работающим. Во-вторых, у автоперезапуска обязан быть предохранитель от цикла — на Linux это лимит попыток в юните, разобранный в гайде про автозапуск через systemd.
#Как узнать, что упал именно внешний провайдер модели, а не мой сервис?
По разделению проверок: живость приложения и доступность провайдера — это две разные строки в таблице. Что делать, когда провайдер лежит, и как заранее подготовить запасной вариант, разобрано в статье про доступность и сбои провайдеров.
Общий маршрут, в котором мониторинг — шестой и последний этап, описан в статье о том, как поднять свой ИИ-сервис.