Health checks как контракт сервиса: SLO, зависимости и что обещать наружу
«Health check» в инженерной практике обычно сводят к разговору про probes в оркестраторе. Это операционная сторона, и она важна, но в ней часто теряется главное: health check — это контракт сервиса. Заявление, при каких условиях сервис считает себя пригодным для работы, и кто на эту информацию опирается. Без архитектурной рамки настройка пробников превращается в копирование чужого YAML, а реальное поведение системы под давлением — в сюрприз.
Расскажу, как я смотрю на health-checks как на часть проектирования сервиса: какие категории состояния стоят за ним, как они связаны с SLO, и какие промахи в этом контракте регулярно встречаю в чужих архитектурах.
Что такое здоровье сервиса с точки зрения архитектора
Если отбросить «возвращает 200 или нет», здоровье — это ответ на конкретный вопрос: «можешь ли ты сейчас выполнить контрактные обязательства перед потребителями?» И тут вылезает, что обязательств обычно больше одного.
Для одного и того же сервиса могут быть разные ответы на разные вопросы. Один и тот же экземпляр может быть готов к чтению (и не готов к записи), готов к запросам в одном бизнес-домене и неготов в другом, готов к нагрузке в обычном режиме и неготов к деградированному. Если у вас один эндпоинт «здоровье», все эти нюансы стираются.
Категории состояния
В архитектурной модели я держу четыре типа здоровья.
Жизнеспособность процесса
Сервис как процесс ещё может отвечать. Не завис в deadlock, не залип в бесконечном цикле, не исчерпал критичные ресурсы. Это базовый признак — без него ничего не имеет смысла.
Готовность к трафику
Сервис прошёл стартап, прогрел кеши, установил соединения. Может принимать запросы и обрабатывать их в рамках SLA.
Здоровье зависимостей
Сервис может выполнить обязательства, потому что все, от кого он зависит, тоже могут. БД отвечает, очередь принимает, нижестоящий сервис в строю.
Соответствие SLO в реальном времени
Сервис сейчас, в эту минуту, держит обещанный latency и error rate. Может технически принимать запросы, но если 99-й перцентиль ушёл за SLO — это уже сигнал.
Эти категории — разные. Класть всё в одну ручку и называть «health» — значит терять разрешение и принимать решения на смешанных данных.
Контракт наружу: что вы обещаете
Health check — это не только для оркестратора. На него смотрят:
- Балансировщик: пускать ли трафик в этот экземпляр.
- Родительские сервисы: считать ли вас доступным.
- Системы мониторинга: бить ли алерт.
- Operations команда: что показывать на дашборде.
У каждого из этих потребителей свой вопрос. Балансировщик хочет знать, готов ли сервис принять следующий запрос. Родительские сервисы — есть ли у них шанс получить ответ. Мониторинг — соответствует ли сервис заявленному SLO.
Здоровый подход — определить контракт явно. Например:
/livez— «процесс ещё может ответить» (для оркестратора)./readyz— «готов принимать трафик прямо сейчас» (для балансировщика)./health— развёрнутый статус с подсистемами (для людей и дашбордов)./sloили метрики — текущее соответствие SLO (для systems thinking).
Имена не важны. Важно, что у каждого потребителя — свой эндпоинт с предсказуемым поведением.
Связь с SLO
Если у сервиса есть SLO («99% запросов отвечают за 200ms», «99,9% запросов завершаются успешно»), health check должен с этим SLO согласовываться, а не идти параллельно.
Простой пример. SLO — 99,9% доступности. Сервис уйдёт в not-ready, потому что одна из необязательных зависимостей не отвечает. Все экземпляры одновременно делают это — наружу сервис мгновенно стал недоступен, error budget сгорает за минуты.
Это означает, что в health check для readiness должны попадать только обязательные с точки зрения SLO зависимости. Остальное обрабатывается на уровне запроса — конкретный запрос вернул 503, потому что не дозвонился до Х, но в целом сервис продолжает работать. Без этого разделения каждый блип превращается в полное падение.
Здоровье и каскадные сбои
Главная архитектурная опасность плохо устроенного health check — каскадный сбой. Зависимость падает, все экземпляры зависящего сервиса считают себя нездоровыми, балансировщик уводит трафик. Дальше события развиваются по одному из двух сценариев.
Сценарий А. Зависимость восстанавливается. Все экземпляры одновременно объявляют себя ready, и весь накопленный трафик одновременно бьёт по ним и по восстановившейся зависимости. Зависимость снова падает. Цикл.
Сценарий Б. Зависимость восстанавливается, но в health check кто-то добавил «проверку готовности соседнего сервиса», который проверяет нашу. Получается зацикленность: A ждёт B, B ждёт A. До перезагрузки всё стоит.
Лекарство — архитектурное правило: health check не должен зависеть от состояния других сервисов транзитивно. Свои критичные зависимости — да. Сторонние через цепочку — нет.
Деградированный режим как явный статус
В реальности у сервиса часто есть промежуточное состояние: «работаю, но в ограниченном режиме». Например, основная БД упала, мы переключились на read-only из реплики; писать не можем, но читать — можем.
Это не «здоров» и не «болен». Это «деградирован». Большинство архитектур игнорируют такое состояние и сводят всё к двум значениям. Я предпочитаю явно вводить его в контракт.
{
"status": "degraded",
"capabilities": {
"read": "ok",
"write": "unavailable"
},
"reason": "primary db unreachable, serving from replica"
}На уровне роутинга это позволяет умно направлять запросы: read-запросы — в этот экземпляр, write — в другие или на retry. На уровне UI — показывать пользователю «оформление временно недоступно, заказы доступны для просмотра» вместо общего «всё сломалось».
Health check и архитектура зависимостей
Если в health check регулярно перечисляется десять внешних систем, у вас не сервис с health check, у вас распределённый монолит с одной красивой ручкой. Это сигнал, что границы сервиса проведены неправильно: он зависит от слишком многого.
Хорошо устроенный сервис может сказать о своём здоровье на основании 1–3 критичных зависимостей. Если их больше, значит, либо вы слишком многое включили, либо архитектура слишком связанная. Второе исправляется на уровне дизайна, не на уровне эндпоинта.
Контракт во времени: что меняется со старта до остановки
У сервиса в течение жизненного цикла четыре фазы:
- Стартап. Процесс жив, но к работе не готов. Liveness — да. Readiness — нет.
- Прогрев. Стартап завершён, кеши заполняются, JIT собирает горячие пути. Технически готов, но качество ответов хуже. Можно частично включать в трафик через постепенное увеличение веса в балансировщике.
- Рабочий режим. Полная готовность. Liveness и readiness — да.
- Остановка. Получили сигнал на завершение. Liveness — да (мы ещё работаем). Readiness — нет (новый трафик не нужен). Дорабатываем активные запросы.
Контракт health check должен явно описывать переходы. Особенно последний — переход в not-ready при остановке должен быть мгновенным, иначе балансировщик будет лить трафик ещё какое-то время и часть запросов окажется неотвеченной.
Сигнальный, а не диагностический
Эндпоинт probes должен отвечать однозначно: ready/not ready, alive/not alive. Развёрнутая диагностика — это другой эндпоинт.
Я регулярно вижу /health, который возвращает страницу на 30 строк JSON с подробным статусом каждого компонента. Балансировщик при этом просто проверяет HTTP-код. Если хоть одно поле «degraded», код всё равно 200, и трафик идёт. Если архитектор хотел разделять — пусть это будет в коде эндпоинта явно, не в надежде, что балансировщик «прочитает».
SLO-ориентированные health checks
В зрелых архитектурах health check включает не только «работает или нет», но и «соответствует ли я SLO прямо сейчас». Идея: если 99-й перцентиль за последние 60 секунд ушёл за порог, сервис помечает себя как degraded. Балансировщик может уменьшить вес или отвести часть трафика.
Это требует in-process хранения метрик и осторожности (можно создать колебания: один экземпляр объявил себя медленным, трафик ушёл на других, они стали медленнее, объявились медленными — система начала качаться). Но в правильно настроенном виде это даёт самовосстановление под нагрузкой.
Чек-лист контракта здоровья
- Разделены liveness и readiness, у каждого своя ответственность.
- Liveness не зависит от внешних систем.
- Readiness проверяет только обязательные с точки зрения SLO зависимости.
- Развёрнутый «человеческий» health отделён от probes для оркестратора.
- Деградированное состояние явно описано в контракте.
- Переходы между фазами (старт, прогрев, остановка) предусмотрены.
- Health check не вызывает каскадных проверок других сервисов.
- Команда, которая владеет сервисом, формулирует контракт здоровья словами, а не «возвращает 200, посмотри сам».
Что запомнить
Health check — это часть архитектурного контракта сервиса, не операционный артефакт. От того, как он спроектирован, зависит, как ваша система ведёт себя при частичных сбоях, при деплоях и при инцидентах в зависимостях. Хороший контракт здоровья даёт балансировщику и потребителям достаточно информации, чтобы принимать решения, и не превращает каждое колебание в каскад. Плохой — выглядит на бумаге как «у нас есть health check», а в инцидентах оборачивается удивлением.