DevOps и инфраструктура

Health-check и readiness: почему «процесс запущен» ещё не значит «сервис готов работать»

Представим обычный production-сервис.

На сервере запущен Node.js.

Docker показывает:

app-api    Up

systemctl тоже доволен:

active (running)

Процесс существует.

Порт слушается.

Запрашиваем:

GET /health

и получаем:

{
  "ok": true
}

На первый взгляд всё работает.

А пользователь в этот момент нажимает:

Войти

и получает:

500 Internal Server Error

Причина оказывается очень простой:

Node.js      ✓
PostgreSQL   ✗

Процесс действительно жив.

Но веб-продукт работать не способен.

В другом случае PostgreSQL доступен, однако приложение после deployment ещё выполняет инициализацию и не готово принимать реальные запросы.

В третьем экземпляр API уже останавливается перед обновлением, но балансировщик продолжает отправлять ему новых пользователей.

Во всех трёх ситуациях проверка:

process exists

даёт правильный ответ на неправильный вопрос.

В production нас интересует не только:

«Жив ли процесс?»

Но и:

«Готов ли этот конкретный экземпляр прямо сейчас обслуживать пользовательский трафик?»

Именно поэтому в зрелой архитектуре появляются разные понятия:

Liveness
Readiness
Startup
Diagnostics

Они похожи, но отвечают на совершенно разные вопросы.

Разберём это на архитектуре реального веб-продукта.


Начнём с обычной production-схемы

Допустим приложение состоит из:

                    Internet
                       │
                       ▼
                     Nginx
                  ┌────┴────┐
                  ▼         ▼
              API #1      API #2
                  │         │
                  └────┬────┘
                       │
          ┌────────────┼────────────┐
          ▼            ▼            ▼
     PostgreSQL       Redis     Object Storage

                       │
                       ▼
                     Worker

Дополнительно могут существовать:

SMTP
Payment API
CRM API
Telegram
AI API
Scheduler

Теперь зададим простой вопрос:

Что значит «API #1 работает»?

Можно придумать несколько совершенно разных ответов.


Ответ №1: процесс существует

Например:

PID 1842

или:

container running

Это означает только:

Операционная система ещё не завершила процесс.

Процесс при этом может:

зависнуть;
потерять соединение с БД;
оказаться в deadlock;
исчерпать connection pool;
не закончить initialization;
отвечать на каждый бизнес-запрос ошибкой.

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


Ответ №2: HTTP-сервер отвечает

Например:

GET /health

возвращает:

200 OK

Это уже лучше.

Мы знаем:

процесс жив;
event loop способен обработать запрос;
HTTP server принимает соединения.

Но PostgreSQL всё ещё может быть недоступен.

Поэтому такой endpoint отвечает примерно на вопрос:

«Живо ли само приложение?»

Это и есть хорошая отправная точка для liveness.


Liveness: нужно ли перезапустить процесс?

Liveness отвечает не на:

Всё ли вокруг приложения работает?

А на:

Способен ли сам процесс продолжать работу или его необходимо перезапустить?

Например:

GET /health/live

ответ:

{
  "status": "alive"
}

Если приложение может обработать этот запрос, оно считается живым.


Liveness намеренно должен быть простым

Есть соблазн сделать:

GET /health

и внутри проверить:

PostgreSQL;
Redis;
S3;
SMTP;
Telegram;
Payment API;
DNS;
AI provider.

Кажется:

Чем больше проверим, тем надёжнее.

Но для liveness это может сделать систему менее надёжной.


Представим кратковременный сбой PostgreSQL

Архитектура:

API #1 ─┐
API #2 ─┼→ PostgreSQL
API #3 ─┘

PostgreSQL на 20 секунд становится недоступен.

Если liveness каждого API зависит от БД:

PostgreSQL ✗
↓
Liveness ✗

оркестратор решает:

API сломан. Нужно перезапустить.

И одновременно перезапускает:

API #1
API #2
API #3

PostgreSQL через несколько секунд восстанавливается.

Но теперь application layer сам находится в процессе массового restart.

Мы превратили короткую проблему базы в более крупный инцидент.


Перезапуск API не чинит PostgreSQL

Это главный вопрос, который стоит задавать при проектировании liveness:

Если эта проверка провалится, поможет ли restart процесса?

Если:

event loop завис

— возможно, да.

Если:

сам процесс находится
в неисправимом состоянии

— возможно, да.

Если:

PostgreSQL временно недоступен

— обычно нет.

Если:

SMTP не отвечает

— тоже нет.

Если:

внешний AI API вернул 503

— restart приложения совершенно бессмысленен.

Поэтому зависимости обычно не стоит бездумно включать в liveness.


Readiness отвечает на другой вопрос

Readiness означает:

«Можно ли прямо сейчас направлять на этот экземпляр новые пользовательские запросы?»

Например:

GET /health/ready

может проверять:

application initialized ✓
PostgreSQL available     ✓
required configuration   ✓
schema compatible        ✓

Если всё хорошо:

200 OK

Если критичная зависимость недоступна:

503 Service Unavailable

Получается важное различие

Liveness
=
Нужно ли перезапускать этот процесс?
Readiness
=
Нужно ли отправлять этому процессу трафик?

Это не одно и то же.

Сервис может быть:

Alive = true
Ready = false

И это совершенно нормальное состояние.


Например, PostgreSQL временно недоступен

Приложение:

Node.js      ✓
PostgreSQL   ✗

Тогда:

Liveness:
PASS

потому что с самим API всё нормально.

А:

Readiness:
FAIL

потому что обслуживать большинство бизнес-запросов оно не может.

Получаем:

{
  "alive": true,
  "ready": false
}

Это гораздо точнее универсального:

{
  "ok": false
}

Зачем это балансировщику

Допустим за Nginx работают два API:

             Nginx
            /     \
           /       \
       API #1     API #2

API #1:

Ready ✓

API #2:

Ready ✗

Правильное поведение:

       Nginx
         │
         ▼
      API #1

API #2 остаётся запущенным, может восстановить соединения, завершить initialization или дождаться зависимости.

Но новые пользовательские запросы к нему временно не идут.


Kubernetes формализует именно это различие

В Kubernetes liveness probe определяет, когда контейнер следует перезапустить, а readiness probe — когда Pod готов получать трафик. Если readiness не проходит, экземпляр исключается из обычного обслуживания через Service, но его не обязательно уничтожать. Отдельная startup probe предназначена для периода первоначального запуска.

Но эти идеи полезны даже если никакого Kubernetes в проекте нет.


На одном VPS readiness тоже имеет смысл

Представим:

Nginx
 ├── API :4173
 └── API :4174

При обновлении можно работать последовательно.

Сначала:

API :4173
↓
restart
↓
wait /ready

Пока он не готов, production продолжает обслуживать:

API :4174

После:

/ready = 200

первый экземпляр возвращается в рабочий пул.

Только затем обновляется второй.

Это уже намного безопаснее:

restart everything
↓
надеемся, что приложение быстро поднимется

Именно readiness позволяет строить контролируемый deployment

Упрощённо:

Deploy new release
       ↓
Restart API #1
       ↓
Liveness?
       ↓
Readiness?
       ↓
PASS
       ↓
Restart API #2
       ↓
Readiness?
       ↓
PASS
       ↓
Deployment complete

Если API #1 не становится ready:

STOP

Второй рабочий экземпляр не трогаем.

Так readiness становится не просто monitoring endpoint.

Он участвует непосредственно в release process.


Третий тип проверки — startup

Есть приложения, которые после запуска не готовы мгновенно.

Например им нужно:

загрузить конфигурацию;
проверить schema version;
подготовить локальные ресурсы;
прогреть cache;
загрузить модель;
выполнить безопасную initialization.

В течение этого времени процесс существует.

Но считать его полноценным рабочим экземпляром рано.


Startup отвечает на вопрос

Приложение вообще закончило запуск?

Получаем три стадии:

Process started
      ↓
Startup complete
      ↓
Ready for traffic
      ↓
Normal work

И три разных ситуации отказа.


Почему startup нельзя путать с liveness

Допустим приложение обычно стартует:

45 секунд.

Liveness запускается через 5 секунд.

Получает:

FAIL

Оркестратор убивает приложение.

Оно запускается заново.

Через 5 секунд снова:

FAIL

Получаем:

start
↓
kill
↓
start
↓
kill
↓
start
↓
kill

Приложение физически никогда не получает 45 секунд, необходимых ему для старта.

Startup probe как раз позволяет отделить фазу инициализации от последующего контроля жизни процесса. В Kubernetes, например, liveness и readiness не начинают обычную работу до успешной startup probe, если она настроена.


Итак, базовая модель

Можно запомнить так:

STARTUP
«Я уже запустился?»
LIVENESS
«Я всё ещё жив?»
READINESS
«Я могу принимать пользователей?»

Это три разных вопроса.


А что тогда такое обычный health-check?

Название health само по себе слишком неоднозначно.

Например endpoint:

/health

может означать:

процесс жив

или:

все зависимости работают

или:

вся инфраструктура идеально здорова

Именно поэтому мы предпочитаем давать проверкам более точную семантику.

Например:

/health/live
/health/ready

А более глубокую диагностику вынести отдельно.


Readiness должна проверять только действительно критичные зависимости

Это одна из самых сложных частей.

Представим SaaS использует:

PostgreSQL
Redis
S3
SMTP
AI API
Analytics API
Telegram

Нужно ли падать readiness при отказе любого из них?

Нет.


Начнём с PostgreSQL

Допустим почти каждый запрос требует:

User
Project
Permissions
Session

из PostgreSQL.

Без базы продукт практически бесполезен.

Значит:

PostgreSQL unavailable
→ Ready = false

выглядит логично.


А теперь SMTP

SMTP временно недоступен.

Можно ли пользователь:

войти;
открыть проект;
редактировать данные;
работать с CRM?

Да.

Не работают только письма.

Если сделать:

SMTP ✗
→ Ready ✗

мы полностью выключим продукт из-за почты.

Это намного хуже исходной проблемы.


Внешняя необязательная зависимость не должна обязательно убивать весь сервис

Например:

AI provider ✗

может означать:

AI-функции временно недоступны

Но:

CRM
Messages
Projects
Files

по-прежнему работают.

Такое состояние лучше представить как:

Ready = true
Degraded = true

а не:

Ready = false

Здесь появляется понятие degraded mode

Не всё в production бинарно:

работает
/
не работает.

Реальное состояние может быть:

Core service       ✓
PostgreSQL         ✓
Redis              ✓
Object Storage     ✓
SMTP               ✗
AI provider        ✗

Ready:
YES

Status:
DEGRADED

Пользователь по-прежнему получает основную услугу.

Мониторинг знает о проблемах дополнительных функций.


Хороший readiness требует классификации зависимостей

Например:

ЗависимостьКритична для APIПри отказе
PostgreSQLДаNot Ready
Runtime configДаNot Ready
Redis session storeЕсли используется для сессийNot Ready
Object StorageЗависит от продуктаВозможно Degraded
SMTPОбычно нетDegraded
AnalyticsНетDegraded
AI providerОбычно нетDegraded
TelegramНетDegraded

Такая таблица гораздо полезнее общего:

Проверим всё.

Даже Redis не всегда одинаково важен

В одном проекте Redis используется как:

cache.

Если он упал:

cache miss
↓
PostgreSQL

Продукт становится медленнее, но работает.

Тогда:

Redis ✗
→ degraded

В другом проекте Redis хранит:

sessions.

Теперь без Redis никто не может авторизоваться.

Та же технология стала критичной.

Поэтому readiness зависит не от названия сервиса.

А от его роли в конкретной архитектуре.


То же самое относится к Object Storage

Если приложение — CRM и storage содержит вложения:

S3 ✗

может означать:

основная CRM работает;
файлы временно недоступны.

Возможно, это degraded mode.

Если продукт целиком является файловым сервисом:

S3 ✗

означает:

главная функция отсутствует.

Тогда сервис разумно считать not ready.


Readiness не должна выполнять реальную пользовательскую операцию

Плохая проверка PostgreSQL:

создать test user
↓
прочитать
↓
удалить

каждые пять секунд.

Представим:

2 API
×
12 проверок в минуту
×
24 часа

Получится десятки тысяч искусственных mutations в день только ради health-check.


Проверка должна быть дешёвой

Для PostgreSQL часто достаточно чего-то концептуально похожего на:

SELECT 1;

Мы проверяем:

можем установить/получить соединение;
сервер отвечает;
query выполняется.

Не нужно при каждой probe запускать:

SELECT *
FROM orders
JOIN users
JOIN projects
ORDER BY ...

Но SELECT 1 тоже отвечает не на всё

Представим соединение с PostgreSQL работает.

Но приложение ожидает таблицу:

payment_events

а migration не применена.

Тогда:

SELECT 1
→ PASS

но реальный endpoint:

POST /payments
→ FAIL

Значит, readiness иногда должна учитывать ещё и совместимость схемы.


Schema readiness особенно полезна после deployment

Например application release ожидает:

schema version = 74

А production DB:

schema version = 73

API уже запущен.

Процесс жив.

PostgreSQL доступен.

Но трафик ему давать нельзя.

Readiness:

Application ✓
PostgreSQL  ✓
Schema      ✗

Ready:
NO

Это намного лучше, чем обнаружить пропущенную migration первым пользовательским запросом.


Но readiness не должна сама выполнять migrations

Проверка:

schema version correct?

— нормально.

Действие:

schema wrong
→ автоматически изменить production DB

из health endpoint — уже гораздо опаснее.

Проверки и mutating operations лучше разделять.


Readiness должна иметь строгий timeout

Представим PostgreSQL завис.

Readiness делает запрос.

И ждёт:

30 секунд.

Балансировщик проверяет readiness каждые:

5 секунд.

Через некоторое время можно получить множество зависших health requests.

Health-check, созданный для повышения надёжности, сам начинает потреблять ресурсы.


Поэтому каждая dependency check должна быть ограничена

Например:

PostgreSQL:
500 ms timeout

Redis:
300 ms timeout

Storage:
500 ms timeout

Конкретные числа зависят от инфраструктуры.

Главная идея:

probe должна завершаться быстро и предсказуемо.


Проверки независимых зависимостей можно выполнять параллельно

Плохо:

PostgreSQL 400 ms
↓
Redis      300 ms
↓
Storage    500 ms

Итого:

1.2 s

Если они независимы, можно:

      ┌→ PostgreSQL
Ready ├→ Redis
      └→ Storage

и получить результат примерно за время самой медленной проверки.

Но опять же — без чрезмерной нагрузки.


Readiness нельзя превращать в mini load test

Например endpoint:

/ready

каждые пять секунд запускает:

20 SQL queries;
S3 upload;
email;
payment API;
DNS lookup;
AI request.

Это уже не health-check.

Это постоянный synthetic test, который сам способен стать причиной проблем.


Полноценный synthetic test нужен отдельно

Есть важное различие.

Readiness:

Может ли этот instance сейчас обслуживать трафик?

Synthetic monitoring:

Работает ли реальный пользовательский сценарий?

Например synthetic check может периодически:

Login
↓
GET Project
↓
Logout

или:

создать тестовую операцию
↓
проверить результат
↓
очистить

Но он запускается значительно реже и не участвует непосредственно в балансировке.


Получается несколько уровней наблюдения

PROCESS
│
├── существует?
│
▼
LIVENESS
│
├── сам runtime здоров?
│
▼
READINESS
│
├── можно принимать трафик?
│
▼
DEPENDENCY HEALTH
│
├── какие компоненты degraded?
│
▼
SYNTHETIC CHECK
│
└── работает ли бизнес-сценарий?

Каждый уровень отвечает на собственный вопрос.


Один endpoint на всё обычно слишком грубый

Представим:

GET /health

возвращает:

{
  "ok": true
}

Что это означает?

Непонятно.

Гораздо информативнее:

GET /health/live
{
  "status": "alive"
}

И:

GET /health/ready
{
  "status": "ready"
}

А отдельная внутренняя диагностика:

GET /internal/diagnostics

может дать:

{
  "api": "ok",
  "database": "ok",
  "redis": "ok",
  "storage": "ok",
  "smtp": "degraded"
}

Почему diagnostics лучше не делать публичной слишком подробной

Ответ вроде:

{
  "postgresHost": "10.0.0.14",
  "database": "production_main",
  "redis": "redis.internal:6379",
  "bucket": "private-client-files",
  "smtp": "..."
}

помогает не только администратору.

Он раскрывает внутреннюю инфраструктуру любому посетителю.

Публичный endpoint должен отдавать минимум необходимой информации.

Например:

{
  "status": "ready"
}

А подробности доступны:

внутреннему monitoring;
администратору;
логам.

Не нужно возвращать stack trace из /ready

Например:

{
  "error": "password authentication failed for user app_prod",
  "host": "...",
  "password": "..."
}

— очевидно недопустимо.

Даже менее чувствительный raw error может раскрывать слишком много.

Снаружи достаточно:

503 NOT_READY

Внутри logs:

dependency=postgres
error=connection_timeout
requestId=...

Status code имеет значение

Условно:

Ready:
200
Not Ready:
503

Так endpoint понимает не только человек, но и:

load balancer;
deployment script;
monitoring;
container orchestrator.

Machine-readable проверка становится частью инфраструктуры.


Почему 200 {"ready": false} хуже

Представим:

HTTP 200
{
  "ready": false
}

Если внешняя система смотрит только HTTP status, она решает:

PASS

Поэтому transport status должен соответствовать семантике проверки.


Readiness полезна при graceful shutdown

Представим начинается deployment.

API #1 получает:

SIGTERM

Плохой сценарий:

SIGTERM
↓
process immediately exits

В этот момент внутри могут находиться:

HTTP request;
file upload;
database transaction.

Они обрываются.


Более аккуратный shutdown выглядит иначе

SIGTERM
   ↓
Ready = false
   ↓
Balancer stops new traffic
   ↓
Existing requests finish
   ↓
Connections close
   ↓
Process exits

Это называется draining.

Readiness здесь используется не для обнаружения сбоя.

А для контролируемого удаления instance из трафика.


Это особенно важно при нескольких API-инстансах

Например:

API #1
API #2

Обновляем первый:

API #1
Ready → false

Nginx направляет новые запросы:

→ API #2

API #1 завершает старые.

Перезапускается.

Проходит startup.

Проходит readiness.

Возвращается.

Теперь можно обновлять:

API #2.

Так можно получить deployment с минимальным или нулевым пользовательским downtime даже на довольно простой инфраструктуре.


Readiness может быть false и во время maintenance

Например сервис должен временно перестать принимать новые операции перед migration.

Можно перевести экземпляр:

Ready = false

дождаться завершения активных запросов.

И только затем выполнять обслуживание.

Это значительно аккуратнее жёсткого:

kill -9

А что делать с worker?

У background worker нет обычного пользовательского HTTP-трафика.

Значит классическое:

Ready for load balancer?

к нему применяется не совсем так же.

Для worker полезнее другие сигналы.

Например:

process alive?
heartbeat fresh?
can reach PostgreSQL?
can claim jobs?
last successful job?

Process running не означает, что worker работает

Например:

Worker process:
UP 12 days

Но из-за ошибки он уже два дня не забрал ни одной job.

Контейнер абсолютно жив.

Бизнес-функция мертва.

Поэтому для background systems особенно полезен heartbeat.


Простой heartbeat

Worker периодически сохраняет:

worker_id
last_seen_at

Monitoring проверяет:

now - last_seen_at

Например:

last heartbeat:
15 seconds ago
→ OK
last heartbeat:
20 minutes ago
→ FAIL

Это лучше, чем смотреть только:

docker ps.

Но heartbeat тоже не доказывает обработку jobs

Worker может исправно писать:

I'm alive

но не выполнять полезную работу.

Поэтому дополнительно полезны:

last successful job;
oldest queued job;
failed job count;
queue depth.

Например:

Worker heartbeat:
OK

Queue:
1 842 jobs

Oldest job:
47 minutes

Status:
DEGRADED

Теперь видно реальное состояние фонового контура.


Очередь может быть health-сигналом

Например нормальная queue depth:

0–20.

Внезапно:

5 000.

Сам worker может быть жив.

Но throughput явно недостаточен или downstream завис.

Это уже operational health.


Scheduler тоже требует отдельной проверки

Представим cron должен каждый день:

03:00 → backup

Процесс scheduler существует.

Но timezone настроена неправильно.

Job фактически никогда не срабатывает в ожидаемое время.

Поэтому monitoring должен смотреть не только:

scheduler alive

но и:

last backup completed at ...

То есть лучше контролировать бизнес-результат scheduled operation.


Один из лучших принципов health architecture

Проверяйте не только компоненты.

Проверяйте ожидаемые последствия их работы.

Например:

SMTP connection ✓

полезно.

Но ещё полезнее:

notification jobs
не зависли.
Worker process ✓

полезно.

Но ещё:

последняя job действительно завершена.
Backup cron ✓

полезно.

Но ещё:

последний verified backup свежий.

Какие зависимости не стоит проверять слишком часто

Представим readiness вызывается:

каждые 2 секунды

на:

20 instances.

Это:

10 запросов в секунду

только health traffic.

Если каждый запрос обращается к пяти внешним API, health subsystem начинает создавать вполне заметную нагрузку.


Внешние сервисы особенно опасны

Допустим каждый /ready обращается к payment provider.

Провайдер отвечает медленно.

Теперь readiness тоже начинает тормозить.

Ещё хуже — у провайдера rate limit.

Ваши health-check сами исчерпывают лимит, необходимый реальным пользователям.

Поэтому удалённые некритичные API обычно не стоит включать в frequent readiness.


Можно кешировать дорогую dependency health

Например отдельный background monitor проверяет external API:

раз в 30 секунд

и хранит:

lastKnownStatus.

Readiness читает локальное состояние.

Но применять такой паттерн нужно осознанно: кеш означает, что статус некоторое время может быть устаревшим.

Для критичных локальных dependencies вроде PostgreSQL часто проще выполнить дешёвую прямую проверку.


Health-check должен избегать cascading failure

Представим PostgreSQL уже перегружен.

В норме на него идут:

business queries.

Теперь 50 API-инстансов замечают замедление и каждый начинает чаще выполнять сложный health SQL.

Получается:

DB overloaded
↓
health probes increase pressure
↓
DB slower
↓
more probes fail
↓
instances removed
↓
remaining instances get more traffic

Health system усилила инцидент.

Это называют неприятным классом каскадных отказов.

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


Поэтому probes должны быть консервативными

Особенно liveness.

Параметры:

timeout
period
failure threshold

не должны превращать одну временную задержку в мгновенный restart production.

Например один timeout не обязательно означает:

Процесс безнадёжно сломан.

Несколько последовательных failures гораздо информативнее.


Flapping тоже опасен

Представим readiness:

10:00:01 PASS
10:00:06 FAIL
10:00:11 PASS
10:00:16 FAIL

Instance постоянно:

добавляется в трафик;
удаляется;
добавляется;
удаляется.

Это flapping.

Причиной может быть слишком агрессивный timeout или зависимость, находящаяся на грани допустимой latency.


Нужно выбирать threshold исходя из назначения

Для readiness имеет смысл довольно быстро убрать действительно неработающий instance из трафика.

Для liveness — быть осторожнее с restart.

То есть:

readiness failure

обычно менее разрушителен, чем:

liveness failure.

Первый говорит:

Временно не отправляйте сюда пользователей.

Второй:

Уничтожьте процесс и создайте заново.

Очень важно понять именно цену действия

Health-check — это не просто наблюдение.

За ним часто следует автоматическое действие.

Например:

Readiness FAIL
→ stop traffic
Liveness FAIL
→ restart process

Поэтому неправильная классификация failure напрямую меняет production.


Health endpoint не должен зависеть от самого frontend

Например проверка:

GET /

может вернуть:

200

из кеша Nginx.

А API за ним полностью мёртв.

Или наоборот — frontend build временно отсутствует, но API работает.

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


Мониторинг снаружи и readiness изнутри решают разные задачи

Внутренний:

GET /ready

отвечает:

Этот instance способен работать?

Внешний:

https://example.ru

отвечает:

Пользователь вообще способен дойти до продукта через DNS, HTTPS и reverse proxy?

Можно иметь:

API Ready ✓

но:

DNS ✗

Пользователь всё равно не получит сервис.


Поэтому полезна многоуровневая наблюдаемость

Например:

External HTTP
       ↓
DNS / TLS / Nginx
       ↓
Readiness
       ↓
Application dependencies
       ↓
Workers
       ↓
Business metrics

Каждый уровень ловит свой класс проблем.


Пример хорошей структуры endpoints

Условно:

GET /health/live

публичный или доступный внутренней инфраструктуре:

{
  "status": "alive"
}
GET /health/ready

возвращает:

{
  "status": "ready"
}

или HTTP 503.

И:

GET /internal/health/details

только для internal monitoring:

{
  "status": "degraded",
  "checks": {
    "postgres": {
      "status": "ok",
      "latencyMs": 4
    },
    "redis": {
      "status": "ok",
      "latencyMs": 2
    },
    "storage": {
      "status": "ok"
    },
    "smtp": {
      "status": "degraded"
    }
  }
}

Latency dependency тоже может быть полезной

Не только:

PostgreSQL:
UP

Но:

PostgreSQL:
12 ms

Если обычно:

3 ms

а сегодня:

800 ms

он технически ещё доступен.

Но production уже деградирует.


Однако readiness не должна превращаться в performance alert

Если DB отвечает за:

150 ms

не обязательно нужно сразу:

Ready = false.

Иначе небольшая деградация может полностью снять сервис с трафика.

Latency лучше параллельно отслеживать мониторингом.

Readiness — более грубая граница:

Способен обслуживать запросы или нет?

Health и observability — не одно и то же

Health:

состояние сейчас.

Observability:

Почему система пришла в это состояние?

Для второго нужны:

logs;
metrics;
traces;
audit.

/ready = 503 говорит:

Этот instance не готов.

Но инженер всё ещё должен понять:

Почему?

Поэтому failure должен появиться в logs

Например:

event=readiness_failed
dependency=postgres
reason=timeout
duration_ms=501

Не обязательно записывать такую строку каждые пять секунд бесконечно.

Иначе при outage база логов заполняется одной и той же ошибкой.

Можно использовать:

rate limiting;
state-change logging.

Например логировать:

READY → NOT_READY

и:

NOT_READY → READY

State transitions health сами по себе полезны

Например:

14:32:10
API-1 READY → NOT_READY
postgres timeout
14:32:26
API-1 NOT_READY → READY
postgres recovered

Из этого сразу видно:

проблема длилась 16 секунд.

Гораздо информативнее 200 одинаковых строк:

database timeout
database timeout
database timeout

Пример Node.js реализации

Концептуально liveness может быть почти тривиальным:

app.get('/health/live', (req, res) => {
  res.status(200).json({
    status: 'alive'
  });
});

Readiness уже содержит ограниченные проверки:

app.get('/health/ready', async (req, res) => {
  const checks = await Promise.allSettled([
    checkPostgres(),
    checkRequiredConfig(),
    checkSchemaVersion()
  ]);

  const ready = checks.every(
    result =>
      result.status === 'fulfilled' &&
      result.value === true
  );

  res
    .status(ready ? 200 : 503)
    .json({
      status: ready ? 'ready' : 'not_ready'
    });
});

Это только упрощённый пример.

В реальной реализации нужны:

timeouts;
structured errors;
graceful shutdown state;
startup state;
неблокирующие проверки.

А detailed health можно строить отдельно

Например:

{
  status: 'degraded',

  checks: {
    postgres: 'ok',
    redis: 'ok',
    storage: 'ok',
    smtp: 'failed'
  }
}

Но такой ответ необязательно отдавать всему интернету.


Readiness должна учитывать graceful shutdown flag

Например:

let shuttingDown = false;

При:

SIGTERM

устанавливаем:

shuttingDown = true;

Теперь:

/health/ready
→ 503

Балансировщик прекращает новый трафик.

Далее:

wait active requests
↓
close server
↓
close DB pool
↓
exit

Так deployment становится значительно аккуратнее.


Что делать с WebSocket

Если продукт использует постоянные соединения, graceful shutdown становится ещё важнее.

Нельзя просто:

kill process

и надеяться, что клиент ничего не заметит.

Можно:

stop accepting new connections;
закрыть существующие контролируемо;
позволить frontend переподключиться
к другому instance.

Readiness помогает убрать старый instance из нового traffic до его завершения.


Что делать с long-running upload

То же самое.

Пользователь загружает:

500 MB

Deployment приходит ровно в этот момент.

Если restart мгновенный:

upload failed at 96%.

При draining можно дать существующему request закончиться в пределах установленного grace period.


Но бесконечно ждать тоже нельзя

Некоторые соединения способны жить очень долго.

Поэтому graceful shutdown обычно имеет:

grace timeout.

Например концептуально:

stop new traffic
↓
wait existing requests
↓
timeout
↓
force close remaining

Иначе одна зависшая операция навсегда остановит deployment.


Readiness полезна даже без балансировщика

Допустим существует только один API.

После restart deployment script может выполнить:

start
↓
poll /ready

И только после:

200

считать release успешным.

Если через допустимый период endpoint остаётся:

503

deployment считается неудачным.

Это намного надёжнее:

sleep 5
↓
наверное запустился.

sleep 5 — классический анти-паттерн deployment

Сегодня приложение запускается:

за 2 секунды.

Завтра после увеличения схемы:

за 8.

Скрипт:

sleep 5

начинает ломаться.

Readiness меняет модель:

Мы не угадываем время запуска.

Мы спрашиваем само приложение:

Ты готово?

Health-check особенно полезен при rollback

Например:

deploy v3.5
↓
restart API
↓
/ready FAIL

Release automation может:

не переключать traffic

или:

вернуть v3.4.

То есть readiness становится частью автоматического quality gate.


Нельзя считать запуск успешным только по Docker status

Например:

docker compose ps

показывает:

Up 20 seconds

Но application log:

database authentication failed
retrying...

Контейнер вполне способен оставаться:

UP

вечно.

Поэтому infrastructure process status и application readiness нужно разделять.


Docker healthcheck тоже должен иметь правильную семантику

Если используется:

HEALTHCHECK ...

нужно заранее решить:

Это liveness?
Readiness?
Или просто дополнительная диагностика?

Одно слово:

healthy

не отменяет архитектурного различия.


Самая распространённая плохая реализация

app.get('/health', (req, res) => {
  res.send('OK');
});

Затем:

Monitor:
UP

даже когда:

PostgreSQL:
DOWN

Такая проверка не бесполезна.

Она просто является liveness, хотя команда ошибочно считает её полной проверкой сервиса.


Вторая плохая реализация — противоположная

/health

проверяет:

PostgreSQL
Redis
SMTP
S3
Telegram
Payment
AI
Analytics

Одно падение analytics:

/health = FAIL

оркестратор перезапускает весь сервис.

Это уже слишком строгая связь.


Третья ошибка — один endpoint используется для всего

Например:

/load balancer

использует /health.

Monitoring тоже.

Docker тоже.

Deployment script тоже.

Все получают один ответ, хотя им нужны разные смыслы.

Гораздо лучше:

live
ready
details

с отдельными назначениями.


Четвёртая ошибка — слишком тяжёлая readiness

Каждые пять секунд:

SELECT COUNT(*) FROM huge_table;

или:

full S3 upload/download.

Health subsystem начинает конкурировать с реальными пользователями за ресурсы.


Пятая ошибка — отсутствие timeout

Dependency зависла.

Health endpoint тоже завис.

Теперь monitoring видит не:

503

а:

request timeout after 30 sec.

Реакция становится медленнее и менее предсказуемой.


Шестая ошибка — liveness зависит от внешней сети

Например DNS внешнего AI provider временно недоступен.

Liveness падает.

API перезапускается.

После restart DNS всё ещё недоступен.

Он снова падает.

Получается бесконечный restart loop без единого шанса исправить исходную проблему.


Седьмая ошибка — readiness всегда true после старта

Например:

let ready = true;

устанавливается после initialization и больше никогда не пересматривается.

PostgreSQL через час исчезает.

Readiness продолжает:

PASS.

Если зависимость может сломаться в runtime, соответствующая проверка тоже должна работать в runtime.


Восьмая ошибка — health показывает secrets

Иногда диагностический endpoint создаётся для удобства и туда выводят:

DATABASE_URL;
S3 endpoint;
SMTP login;
environment.

Через некоторое время endpoint случайно оказывается доступен публично.

Health subsystem не должен становиться новым источником утечки.


Девятая ошибка — monitoring проверяет только /health из localhost

Например:

curl localhost:4173/health
→ OK

Но:

DNS wrong
TLS expired
Nginx misconfigured

Пользователь всё равно не может открыть приложение.

Нужна также внешняя проверка реального production URL.


Десятая ошибка — health без alert

Dashboard показывает:

Ready:
FAIL

три часа.

Но никто его не открыл.

Это не monitoring.

Это просто экран.

Критическое изменение состояния должно создавать:

alert.

Какие события действительно стоит алертить

Например:

весь production недоступен;
нет ready API instances;
PostgreSQL недоступен;
worker heartbeat устарел;
queue растёт;
backup просрочен.

При этом один кратковременный failed probe необязательно должен будить инженера ночью.

Нужно различать:

transient failure

и:

sustained incident.

Readiness помогает избежать ложного успешного deployment

Представим автоматический updater пишет:

Container started successfully.

Если проверка заканчивается на Docker:

release PASS.

Но если updater ждёт:

/api/ready

он может обнаружить:

container started
↓
PostgreSQL OK
↓
Redis OK
↓
schema mismatch
↓
Ready FAIL

Теперь плохой release не считается успешным.


Это особенно важно при автоматическом обновлении

Человек при ручном deployment может открыть браузер и заметить:

Что-то не так.

Автоматическая система такой интуиции не имеет.

Ей нужны формализованные признаки успеха.

Readiness — один из них.


Но readiness всё равно не заменяет post-deploy smoke test

Представим:

PostgreSQL ✓
Redis ✓
Schema ✓

Readiness:

PASS.

Но в новой версии сломан:

POST /projects.

Это уже application regression.

Поэтому после readiness полезно пройти хотя бы несколько критичных сценариев.

Получается:

Process start
↓
Startup
↓
Readiness
↓
Smoke tests
↓
Release accepted

Каждый уровень ловит разные ошибки.


Health архитектура должна соответствовать архитектуре продукта

Для маленького сайта:

Application
↓
SQLite

может хватить двух простых проверок.

Для SaaS:

Nginx
API × N
PostgreSQL
Redis
S3
Worker
Scheduler
Payments

health model будет значительно глубже.

Не нужно копировать enterprise-схему туда, где она ничего не даёт.


Практичная модель для обычного сложного веб-продукта

Например:

/health/live

Проверяет:

runtime отвечает;
приложение не завершает работу.

Не проверяет внешние сервисы.


/health/ready

Проверяет:

startup завершён;
не начался graceful shutdown;
critical configuration загружена;
PostgreSQL отвечает;
schema совместима;
критичная локальная зависимость доступна.

Возвращает:

200

или:

503.

Internal dependency health

Показывает:

PostgreSQL
Redis
Storage
SMTP
external APIs
worker

с состояниями:

OK
DEGRADED
FAILED

External monitoring

Проверяет:

DNS
HTTPS
Nginx
production URL.

Business monitoring

Следит за:

successful logins;
job completion;
webhook processing;
backup freshness;
queue age;
payment confirmation.

Получается многослойная модель.


Не каждый failure должен менять readiness

Например:

SMTP unavailable

может создать:

Health:
DEGRADED

и alert.

Но:

Ready:
YES

Основной продукт остаётся доступным.


Но critical dependency должна влиять

Если:

PostgreSQL unavailable

и без него невозможно:

authenticate;
authorize;
read user data;
perform mutations,

то:

Ready:
NO

логичен.


Это позволяет пользователю получить меньше ошибок

Вместо того чтобы отправить request заведомо неспособному instance:

Request
↓
API
↓
DB error
↓
500

балансировщик вообще не выбирает такой instance.

Это основная практическая ценность readiness.


Но если все экземпляры используют одну БД?

Возникает логичный вопрос.

Если PostgreSQL упал:

API #1 Not Ready
API #2 Not Ready

куда отправлять трафик?

Никуда.

И это нормально.

Пользователь получит управляемый:

503 Service Unavailable

вместо случайного набора:

500
timeout
broken JSON
partial response.

А мониторинг сразу увидит:

0 ready instances.

Readiness не устраняет single point of failure

Если существует один PostgreSQL, он остаётся single point of failure.

Health-check не делает БД отказоустойчивой.

Он лишь даёт системе правдивое понимание:

Сейчас обслуживать пользователей невозможно.

Высокая доступность PostgreSQL — уже отдельная архитектурная задача.


Это важное ограничение любых health-check

Они:

не чинят архитектуру;

они:

измеряют её состояние

и позволяют автоматике корректнее реагировать.


Хорошая readiness должна быть детерминированной

Один и тот же набор условий должен давать один результат.

Не стоит добавлять:

случайный внешний request;
тест, иногда занимающий минуту;
нестабильный third-party API.

Иначе сам readiness становится источником случайности.


Тестировать health endpoints нужно так же, как обычный код

Например:

DB available
→ ready 200
DB unavailable
→ ready 503
DB unavailable
→ live 200
shutdown started
→ ready 503
schema outdated
→ ready 503

Такие проверки прекрасно автоматизируются.


Очень полезен тест «PostgreSQL упал»

Не мок:

dbHealthy = false

а integration test:

API running
↓
stop PostgreSQL
↓
/live ?
↓
/ready ?

Ожидаем:

/live  = 200
/ready = 503

Затем:

start PostgreSQL

и:

/ready = 200

без обязательного restart API, если приложение умеет восстановить соединение.


Это проверяет реальное восстановление после dependency outage

Очень важный сценарий:

Dependency failed
↓
Service Not Ready
↓
Dependency recovered
↓
Service Ready

Если API после восстановления PostgreSQL остаётся навсегда сломанным, значит одной readiness недостаточно — нужно исправлять connection recovery.


Полезен и тест deployment

Например:

API #1 current
API #2 current

↓
upgrade API #1

API #1 Not Ready
API #2 Ready

↓
API #1 Ready

↓
upgrade API #2

API #1 Ready
API #2 Not Ready

↓
API #2 Ready

На всём пути:

ready instances >= 1.

Это уже проверяемый zero/minimal-downtime invariant.


Можно контролировать readiness прямо в updater

Например алгоритм:

Deploy release
↓
Start API #1
↓
Wait readiness up to N
↓
FAIL?
   └→ abort
PASS?
   ↓
Start next component

Так порядок deployment задаётся архитектурой зависимостей.


Worker можно обновлять после API

Если новая версия worker зависит от новой schema, порядок может быть:

backup
↓
migration
↓
API #1
↓
readiness
↓
API #2
↓
readiness
↓
worker
↓
worker heartbeat
↓
post-deploy checks

У другого продукта правильный порядок будет иным.

Главное — чтобы он был формализован.


Health-check помогает и при передаче проекта другому инженеру

Без него специалисту нужно выяснять:

Работает ли API?

и вручную проверять:

process;
port;
database;
logs.

С нормальным readiness большая часть состояния уже выражена машинно.

Например:

curl /health/ready

и internal diagnostics сразу показывают, где искать проблему.

Это снижает зависимость продукта от знания первоначального разработчика.


Это напрямую связано с технической поддерживаемостью

Хорошая production-система умеет сама отвечать хотя бы на часть вопросов:

Я жива?

Я готова?

Какая зависимость сломана?

Как давно?

Когда восстановилась?

Чем больше таких ответов формализовано, тем меньше support превращается в:

Зайдём на сервер и попробуем понять, что происходит.

Практический checklist для health/readiness

Перед production мы бы проверили:

  • отдельна ли liveness от readiness;
  • liveness действительно проверяет состояние самого приложения, а не всех внешних сервисов;
  • отказ PostgreSQL не вызывает бессмысленный restart loop API;
  • readiness учитывает критичные зависимости;
  • некритичные зависимости способны переводить продукт в degraded mode вместо полного отказа;
  • readiness учитывает startup/initialization;
  • readiness становится false при graceful shutdown;
  • dependency checks имеют короткие timeouts;
  • health-запросы не создают данные и не выполняют тяжёлые запросы;
  • probes не создают заметную нагрузку на PostgreSQL и внешние API;
  • HTTP status соответствует состоянию (200 / 503);
  • публичный endpoint не раскрывает secrets и внутреннюю инфраструктуру;
  • подробная диагностика доступна только там, где действительно нужна;
  • monitoring контролирует изменение health-state, а не только existence процесса;
  • worker проверяется не только через PID, но и через heartbeat/результаты jobs;
  • scheduler контролируется по фактическим результатам scheduled jobs;
  • есть внешний check production URL через DNS/HTTPS;
  • deployment ожидает readiness вместо фиксированного sleep;
  • после readiness выполняется отдельный smoke test;
  • протестирован сценарий временного отказа критичной dependency;
  • протестировано восстановление readiness после возврата зависимости;
  • при нескольких API deployment не оставляет систему без ready instance;
  • настроены alerts на устойчивое отсутствие готовых экземпляров.

Если эти свойства проверены, /ready становится не декоративным endpoint.

Он действительно отражает способность системы принимать трафик.


Как выглядит итоговая production-модель

Условно:

                     USER
                       │
                       ▼
                 External check
                       │
                       ▼
                     Nginx
                 ┌─────┴─────┐
                 │           │
                 ▼           ▼
              API #1      API #2
              READY       READY
                 │           │
                 └─────┬─────┘
                       │
           ┌───────────┼───────────┐
           ▼           ▼           ▼
       PostgreSQL    Redis       Storage
           │
           ▼
         Worker
           │
           ├── heartbeat
           ├── queue age
           └── failed jobs

Monitoring:
DNS
TLS
API liveness
API readiness
dependencies
worker
backup
business indicators

И теперь состояние:

Docker:
UP

является лишь одной маленькой частью картины.


Вместо вывода

Одна из самых обманчивых production-фраз звучит так:

Процесс запущен.

Потому что между:

process exists

и:

пользователь может нормально работать

находится целая цепочка зависимостей и состояний.

Приложение может быть живым, но не готовым.

Может быть готовым к основной работе, но находиться в degraded mode из-за некритичной интеграции.

Может закончить startup, но уже начинать graceful shutdown.

Worker может существовать как процесс, но месяц не выполнять jobs.

Backup scheduler может быть запущен, но неделю не создавать backup.

Поэтому зрелая production-архитектура постепенно перестаёт спрашивать:

«Процесс работает?»

и начинает задавать более точные вопросы:

Liveness:
«Сам процесс способен продолжать работу?»
Readiness:
«Можно ли отправлять ему пользователей?»
Startup:
«Он уже закончил запуск?»
Dependency health:
«Какие возможности продукта сейчас деградировали?»
Business health:
«Выполняется ли то, ради чего сервис вообще существует?»

Именно это различие позволяет сделать deployment безопаснее, monitoring полезнее, а аварии — значительно понятнее.

Хороший health-check не обязан доказывать, что весь интернет вокруг приложения работает идеально.

Его задача — точно отвечать на конкретный операционный вопрос.

А хороший readiness должен в каждый момент давать инфраструктуре честный ответ:

Этот экземпляр можно включать в пользовательский трафик.

или:

Пока нельзя.

Когда эта граница формализована, production перестаёт зависеть от предположения:

«Контейнер зелёный — значит всё нормально».

И начинает опираться на состояние, которое действительно имеет значение для пользователя.

Есть похожая задача?

Опишите продукт, интеграции и ограничения. До разработки зафиксируем объём, риски и критерии приёмки.

Обсудить проект →