Пошук уроків, статей та іншого контенту
Реалізуйте liveness та readiness-перевірки для контролю стану процесу, залежностей і готовності сервісу приймати трафік.
Health check — це HTTP-ендпойнт, який повідомляє платформі запуску або балансувальнику про стан сервісу.
Зазвичай потрібні дві різні перевірки:
liveness — чи живий процес;
readiness — чи готовий сервіс приймати трафік.
Ці перевірки відповідають на різні запитання:
| Перевірка | Запитання | Типова дія при помилці | |---|---|---| | Liveness | Процес працює або завис? | Перезапустити процес | | Readiness | Сервіс готовий обробляти запити? | Прибрати з балансування |
Не слід змішувати ці перевірки. Якщо база даних тимчасово недоступна, це зазвичай означає, що сервіс не готовий, але сам процес може бути повністю працездатним.
Liveness-перевірка має бути максимально простою. Вона не повинна звертатися до бази даних, черги повідомлень або сторонніх API.
Якщо Node.js-процес може відповісти на такий запит, він, найімовірніше, не завис:
GET /health/liveУспішна відповідь:
200 OK
Content-Type: application/json
{
"status": "ok"
}Якщо процес не відповідає або повертає помилку, система оркестрації може перезапустити його.
Readiness-перевірка визначає, чи можна направляти до сервісу звичайний трафік:
GET /health/readyВона може перевіряти:
завершення початкової ініціалізації;
доступність бази даних;
доступність брокера повідомлень;
наявність необхідної конфігурації;
працездатність критичної зовнішньої залежності.
Можливі відповіді:
200 OK
Content-Type: application/json
{
"status": "ready",
"dependency": "ok"
}Якщо залежність недоступна:
503 Service Unavailable
Content-Type: application/json
{
"status": "not_ready",
"dependency": "unavailable"
}Код 503 повідомляє балансувальнику, що сервіс тимчасово не готовий приймати трафік.
Нижче наведено повністю runnable-приклад без сторонніх npm-пакетів. Він містить:
/health/live для liveness;
/health/ready для readiness;
періодичну перевірку залежності;
початковий стан not_ready;
коректну поведінку під час завершення процесу;
тестову HTTP-залежність, яка запускається на порту 4000.
Для запуску потрібен Node.js 18 або новіший, оскільки приклад використовує вбудований fetch.
const http = require('node:http');
const APP_PORT = Number(process.env.PORT || 3000);
const DEPENDENCY_PORT = 4000;
const DEPENDENCY_URL =
process.env.DEPENDENCY_URL || `http://localhost:${DEPENDENCY_PORT}/health`;
let isReady = false;
let isCheckingReadiness = false;
function sendJson(response, statusCode, payload) {
response.writeHead(statusCode, {
'Content-Type': 'application/json; charset=utf-8',
'Cache-Control': 'no-store',
});
response.end(JSON.stringify(payload));
}
async function checkDependency() {
const controller = new AbortController();
const timeout = setTimeout(() => controller.abort(), 1000);
try {
const response = await fetch(DEPENDENCY_URL, {
signal: controller.signal,
});
return response.ok;
} catch {
return false;
} finally {
clearTimeout(timeout);
}
}
async function updateReadiness() {
// Не запускаємо кілька перевірок залежності одночасно.
if (isCheckingReadiness) {
return;
}
isCheckingReadiness = true;
try {
const dependencyIsAvailable = await checkDependency();
// Сервіс готовий лише тоді, коли доступна критична залежність.
isReady = dependencyIsAvailable;
if (!isReady) {
console.error('Readiness check failed: dependency is unavailable');
}
} finally {
isCheckingReadiness = false;
}
}
const appServer = http.createServer((request, response) => {
if (request.method === 'GET' && request.url === '/health/live') {
sendJson(response, 200, {
status: 'ok',
uptimeSeconds: Math.round(process.uptime()),
});
return;
}
if (request.method === 'GET' && request.url === '/health/ready') {
if (isReady) {
sendJson(response, 200, {
status: 'ready',
dependency: 'ok',
});
} else {
sendJson(response, 503, {
status: 'not_ready',
dependency: 'unavailable',
});
}
return;
}
if (request.method === 'GET' && request.url === '/') {
if (!isReady) {
sendJson(response, 503, {
error: 'Service is not ready',
});
return;
}
sendJson(response, 200, {
message: 'Application is ready',
});
return;
}
sendJson(response, 404, {
error: 'Not found',
});
});
const dependencyServer = http.createServer((request, response) => {
if (request.method === 'GET' && request.url === '/health') {
const dependencyIsDown = process.env.DEPENDENCY_DOWN === 'true';
if (dependencyIsDown) {
sendJson(response, 503, {
status: 'down',
});
return;
}
sendJson(response, 200, {
status: 'ok',
});
return;
}
sendJson(response, 404, {
error: 'Not found',
});
});
async function start() {
dependencyServer.listen(DEPENDENCY_PORT, () => {
console.log(`Mock dependency is listening on port ${DEPENDENCY_PORT}`);
});
appServer.listen(APP_PORT, async () => {
console.log(`Application is listening on port ${APP_PORT}`);
// До першої успішної перевірки сервіс не вважається готовим.
await updateReadiness();
setInterval(updateReadiness, 5000);
});
}
function shutdown(signal) {
console.log(`Received ${signal}, stopping traffic`);
// Нові запити більше не повинні надходити до цього процесу.
isReady = false;
appServer.close(() => {
dependencyServer.close(() => {
process.exit(0);
});
});
}
process.on('SIGTERM', () => shutdown('SIGTERM'));
process.on('SIGINT', () => shutdown('SIGINT'));
start().catch((error) => {
console.error('Failed to start application', error);
process.exit(1);
});Збережіть код у файлі server.js і запустіть:
node server.jsУ новому терміналі перевірте liveness:
curl -i http://localhost:3000/health/liveОчікуваний результат — 200 OK.
Перевірте readiness:
curl -i http://localhost:3000/health/readyПісля запуску тестової залежності результат також має бути 200 OK.
Тепер змоделюйте відмову залежності. Зупиніть процес і запустіть його з параметром середовища:
DEPENDENCY_DOWN=true node server.jsТепер:
/health/live продовжить повертати 200;
/health/ready поверне 503;
звичайний endpoint / поверне 503.
Це демонструє важливу різницю: процес живий, але не готовий обслуговувати трафік.
Під час запуску сервіс може ще не мати готового з’єднання із залежностями. Наприклад:
процес Node.js запущено;
конфігурацію прочитано;
підключення до бази даних ще встановлюється;
readiness-перевірка повертає 503;
після успішної перевірки readiness змінюється на 200.
Тому readiness має початковий стан false. Не варто вважати сервіс готовим лише тому, що HTTP-сервер уже почав слухати порт.
У прикладі залежність перевіряється кожні п’ять секунд:
setInterval(updateReadiness, 5000);Такий підхід має кілька переваг:
health endpoint відповідає швидко;
кожен запит до /health/ready не створює нове з’єднання із залежністю;
короткочасні проблеми не спричиняють багато одночасних перевірок.
Важливо, щоб перевірка мала timeout. Без timeout невідповідь залежності може залишити readiness-перевірку в стані очікування.
У прикладі timeout дорівнює одній секунді:
const controller = new AbortController();
const timeout = setTimeout(() => controller.abort(), 1000);Значення timeout потрібно підбирати відповідно до вимог сервісу, але health check не повинен чекати залежність необмежено довго.
Під час зупинки процесу застосунок спочатку встановлює:
isReady = false;Це важливо, оскільки сервіс має перестати отримувати новий трафік ще до повного завершення процесу.
Порядок завершення такий:
readiness стає негативним;
балансувальник перестає направляти нові запити;
HTTP-сервер закриває приймання нових з’єднань;
процес завершується.
Liveness під час завершення не потрібно використовувати для визначення готовності до нового трафіку. Саме для цього існує окрема readiness-перевірка.
До readiness варто включати лише залежності, без яких сервіс справді не може виконувати основну роботу.
Наприклад:
сервіс не може обробити жоден запит без бази даних;
сервіс не може прийняти повідомлення без брокера;
сервіс не може виконати основну операцію без обов’язкового зовнішнього API.
Не всі залежності є критичними. Якщо необов’язковий сервіс аналітики недоступний, основний сервіс, можливо, все одно має залишатися ready.
Перевірка повинна бути достатньо глибокою, щоб виявляти реальну проблему, але не повинна виконувати повну бізнес-операцію. Наприклад, замість створення тестового запису в базі даних краще використати просту команду перевірки з’єднання, яку підтримує клієнт бази даних.
Якщо база даних тимчасово недоступна, liveness поверне помилку, і платформа почне перезапускати процес.
Це може погіршити ситуацію:
процеси постійно перезапускаються;
зростає кількість помилок;
відновлення стає складнішим.
Залежності зазвичай потрібно перевіряти в readiness, а не в liveness.
200Якщо /health/ready завжди повертає 200, балансувальник продовжить надсилати трафік до сервісу, який не може його обробляти.
Для неготового сервісу використовуйте 503 Service Unavailable.
Запит до залежності може зависнути через проблеми з мережею. Health check не повинен очікувати нескінченно.
Завжди використовуйте обмеження часу для мережевих перевірок.
Якщо додати до readiness усі зовнішні сервіси, тимчасова проблема другорядної залежності зробить увесь застосунок неготовим.
Перевіряйте лише критичні залежності.
Health endpoint не повинен:
виконувати складні SQL-запити;
запускати бізнес-операції;
створювати або змінювати дані;
повертати великі об’єми інформації.
Його завдання — швидко та передбачувано повідомити про стан сервісу.
Не повертайте у відкритій відповіді:
паролі;
токени;
рядки підключення;
повні тексти помилок сторонніх систем;
чутливу конфігурацію.
Для health response достатньо статусу та короткої назви проблемної залежності.
Liveness відповідає на запитання, чи працює процес.
Readiness відповідає на запитання, чи можна направляти до сервісу трафік.
Liveness має бути простою і не повинна залежати від бази даних або інших зовнішніх сервісів.
Readiness може перевіряти критичні залежності.
Для неготового сервісу використовуйте HTTP-статус 503.
Перевірки залежностей повинні мати timeout.
Під час graceful shutdown спочатку потрібно вимкнути readiness.
Тимчасова недоступність залежності не обов’язково означає, що процес потрібно перезапускати.