Представим обычный production-сервис.
На сервере запущен Node.js.
Docker показывает:
app-api Upsystemctl тоже доволен:
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 #3PostgreSQL через несколько секунд восстанавливается.
Но теперь 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 #2API #1:
Ready ✓API #2:
Ready ✗Правильное поведение:
Nginx
│
▼
API #1API #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 = 73API уже запущен.
Процесс жив.
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:
200Not 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 → falseNginx направляет новые запросы:
→ API #2API #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_atMonitoring проверяет:
now - last_seen_atНапример:
last heartbeat:
15 seconds ago
→ OKlast 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 trafficHealth 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 FAILInstance постоянно:
добавляется в трафик;
удаляется;
добавляется;
удаляется.Это flapping.
Причиной может быть слишком агрессивный timeout или зависимость, находящаяся на грани допустимой latency.
Нужно выбирать threshold исходя из назначения
Для readiness имеет смысл довольно быстро убрать действительно неработающий instance из трафика.
Для liveness — быть осторожнее с restart.
То есть:
readiness failureобычно менее разрушителен, чем:
liveness failure.Первый говорит:
Временно не отправляйте сюда пользователей.
Второй:
Уничтожьте процесс и создайте заново.
Очень важно понять именно цену действия
Health-check — это не просто наблюдение.
За ним часто следует автоматическое действие.
Например:
Readiness FAIL
→ stop trafficLiveness 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 → READYState transitions health сами по себе полезны
Например:
14:32:10
API-1 READY → NOT_READY
postgres timeout14: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 MBDeployment приходит ровно в этот момент.
Если 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 остаётся:
503deployment считается неудачным.
Это намного надёжнее:
sleep 5
↓
наверное запустился.sleep 5 — классический анти-паттерн deployment
Сегодня приложение запускается:
за 2 секунды.Завтра после увеличения схемы:
за 8.Скрипт:
sleep 5начинает ломаться.
Readiness меняет модель:
Мы не угадываем время запуска.
Мы спрашиваем само приложение:
Ты готово?
Health-check особенно полезен при rollback
Например:
deploy v3.5
↓
restart API
↓
/ready FAILRelease 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
Paymentshealth 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
FAILEDExternal 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 200DB unavailable
→ ready 503DB unavailable
→ live 200shutdown started
→ ready 503schema 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 перестаёт зависеть от предположения:
«Контейнер зелёный — значит всё нормально».
И начинает опираться на состояние, которое действительно имеет значение для пользователя.