Пошук уроків, статей та іншого контенту
Дізнаєтеся, як проєктувати перевірки стану сервісів і відрізняти тимчасову недоступність від критичної відмови.
Health check — це контрольна перевірка, за допомогою якої система визначає, чи може сервіс:
продовжувати працювати;
приймати нові запити;
обробляти запити з потрібною якістю;
бути тимчасово вилученим із балансувальника або пулу реплік.
Перевірка стану не повинна відповідати лише на питання «процес запущений чи ні». Процес може працювати, але:
не мати з’єднання з базою даних;
зависнути на блокуванні;
не мати доступу до критичної залежності;
бути перевантаженим;
перебувати в процесі завершення роботи.
Тому в розподіленій системі зазвичай розділяють кілька типів перевірок.
Liveness перевіряє, чи здатен процес продовжувати роботу взагалі.
Успішна liveness-перевірка означає:
процес відповідає;
головний цикл виконання не завис;
сервіс не перебуває у невідновному внутрішньому стані.
Невдала liveness-перевірка зазвичай означає критичну проблему. Оркестратор може перезапустити екземпляр сервісу.
Liveness не повинна перевіряти всі зовнішні залежності. Якщо база даних тимчасово недоступна, перезапуск кожного екземпляра сервісу не обов’язково допоможе. Навпаки, це може створити каскад перезапусків і погіршити ситуацію.
Readiness перевіряє, чи можна направляти на сервіс нові запити.
Сервіс може бути:
живим, але не готовим приймати трафік;
готовим обробляти запити, але тимчасово втратити необов’язкову залежність;
неготовим під час запуску або завершення роботи.
Readiness варто використовувати для:
вилучення екземпляра з балансувальника;
припинення надсилання нових запитів;
очікування готовності критичних залежностей;
контрольованого розгортання нової версії.
Типова модель:
live = процес може продовжувати роботу
ready = процес може приймати нові запитиНевдала readiness-перевірка зазвичай не повинна спричиняти перезапуск процесу.
Liveness має бути простою та локальною. Наприклад:
HTTP-сервер може обробити запит;
внутрішній цикл сервісу не завис;
процес не перебуває у стані завершення.
Не слід виконувати в liveness:
запит до бази даних;
виклик іншого сервісу;
повний бізнес-сценарій;
довгі операції;
повторні спроби підключення.
Інакше відмова однієї залежності може змусити оркестратор перезапустити всі екземпляри.
Readiness може включати перевірки залежностей, без яких сервіс не може коректно виконувати основну роботу:
з’єднання з базою даних;
доступність обов’язкової черги;
наявність локальних ресурсів;
завершення міграцій або ініціалізації.
Важливо перевіряти тільки критичні залежності. Якщо сервіс може працювати без аналітики або кешу, недоступність цих компонентів не обов’язково повинна робити весь сервіс неготовим.
Залежності зручно поділити на групи:
критичні — без них основні запити не можуть бути виконані;
необов’язкові — без них доступна деградована функціональність;
операційні — потрібні для моніторингу, але не для обробки користувацьких запитів.
Для проєктування перевірок корисно явно описати стани сервісу:
STARTING -> сервіс запускається
READY -> може приймати запити
DEGRADED -> працює з обмеженнями
NOT_READY -> не повинен отримувати новий трафік
STOPPING -> завершує роботуНе кожен стан має бути окремим HTTP-відповіддю. Важливо визначити поведінку:
під час STARTING readiness повертає помилку;
у READY обидві перевірки успішні;
у DEGRADED readiness залежить від того, чи збережена основна функціональність;
у STOPPING readiness негайно стає неуспішною, а liveness ще може повертати успіх протягом короткого часу.
Поширена домовленість:
200 OK — перевірка успішна;
503 Service Unavailable — сервіс не готовий або його стан невідомий;
500 Internal Server Error — помилка самої реалізації endpoint, а не звичайна неготовність.
Для readiness краще повертати 503, якщо критична залежність недоступна. Це дозволяє балансувальнику або оркестратору припинити надсилання трафіку.
Відповідь може містити короткий діагностичний стан:
{
"status": "not_ready",
"checks": {
"database": "failed"
}
}Не додавайте до такої відповіді:
паролі;
токени;
рядки підключення;
повні тексти внутрішніх помилок;
персональні дані.
Health check часто доступний через внутрішню мережу, але це не причина ігнорувати його захист.
Один невдалий запит ще не доводить критичну відмову. У мережах трапляються:
короткі затримки;
втрата окремих пакетів;
тимчасове перевантаження;
короткочасний перезапуск залежності;
перевищення тайм-ауту через пікове навантаження.
Тому потрібно відрізняти:
Ознаки:
невдача трапилася один раз або рідко;
наступна перевірка знову успішна;
помилка обмежена однією залежністю;
сервіс може безпечно повторити операцію або працювати в деградованому режимі.
Ознаки:
кілька послідовних перевірок завершилися невдало;
залежність недоступна довше визначеного порогу;
сервіс не може виконувати основну функцію;
процес не відповідає або постійно завершується з помилкою;
відмова відтворюється для різних екземплярів.
Для цього використовують кілька механізмів.
Кожна перевірка залежності повинна мати обмеження часу. Без тайм-ауту health check може сам зависнути й створити хибне уявлення про стан сервісу.
Тайм-аут має бути достатньо коротким, щоб швидко вилучити неготовий екземпляр, але не настільки малим, щоб нормальна мережева затримка виглядала як відмова.
Замість реакції на одну помилку можна вимагати кілька невдалих перевірок поспіль.
Наприклад:
1 невдала перевірка -> екземпляр залишається готовим
3 невдалі перевірки поспіль -> екземпляр стає неготовим
2 успішні перевірки поспіль -> екземпляр знову готовийПороги залежать від частоти перевірок і допустимого часу реакції.
Якщо статус змінюється на кожній окремій невдачі, сервіс може постійно переходити між ready і not ready. Це називають «флапінгом».
Для стабільності поріг переходу в неготовий стан і поріг повернення можуть відрізнятися:
для вилучення потрібно 3 невдачі;
для повернення достатньо 2 успішних перевірок.
Нижче наведено спрощений Node.js-сервіс. Він має:
окремий liveness endpoint;
readiness endpoint;
перевірку критичної залежності;
тайм-аут залежності;
коректну поведінку під час завершення роботи.
const http = require("node:http");
const port = Number(process.env.PORT || 3000);
const dependencyUrl = process.env.DEPENDENCY_URL || null;
let isShuttingDown = false;
async function checkDependency() {
// Якщо критичної зовнішньої залежності немає, перевірка успішна.
if (!dependencyUrl) {
return { ok: true, status: "not_configured" };
}
const controller = new AbortController();
const timeout = setTimeout(() => controller.abort(), 500);
try {
const response = await fetch(dependencyUrl, {
method: "GET",
signal: controller.signal,
headers: {
accept: "application/json"
}
});
return {
ok: response.ok,
status: response.ok ? "ok" : `http_${response.status}`
};
} catch (error) {
return {
ok: false,
status: error.name === "AbortError" ? "timeout" : "unreachable"
};
} finally {
clearTimeout(timeout);
}
}
function sendJson(response, statusCode, body) {
response.writeHead(statusCode, {
"content-type": "application/json; charset=utf-8",
"cache-control": "no-store"
});
response.end(JSON.stringify(body));
}
const server = http.createServer(async (request, response) => {
if (request.url === "/health/live") {
// Liveness не перевіряє зовнішні залежності.
if (isShuttingDown) {
return sendJson(response, 503, {
status: "stopping"
});
}
return sendJson(response, 200, {
status: "live"
});
}
if (request.url === "/health/ready") {
// Під час завершення роботи нові запити більше не приймаються.
if (isShuttingDown) {
return sendJson(response, 503, {
status: "not_ready",
reason: "stopping"
});
}
const dependency = await checkDependency();
if (!dependency.ok) {
return sendJson(response, 503, {
status: "not_ready",
checks: {
dependency: dependency.status
}
});
}
return sendJson(response, 200, {
status: "ready",
checks: {
dependency: dependency.status
}
});
}
if (request.url === "/") {
return sendJson(response, 200, {
message: "Service is running"
});
}
sendJson(response, 404, {
error: "Not found"
});
});
server.listen(port, () => {
console.log(`Service is listening on port ${port}`);
});
function shutdown(signal) {
if (isShuttingDown) {
return;
}
isShuttingDown = true;
console.log(`Received ${signal}, stopping readiness checks`);
server.close((error) => {
if (error) {
console.error(error);
process.exitCode = 1;
return;
}
process.exit(0);
});
}
process.on("SIGTERM", () => shutdown("SIGTERM"));
process.on("SIGINT", () => shutdown("SIGINT"));Для запуску потрібен Node.js 18 або новіший:
node server.jsПеревірка liveness:
curl -i http://localhost:3000/health/liveПеревірка readiness без налаштованої залежності:
curl -i http://localhost:3000/health/readyЯкщо вказати недоступну критичну залежність:
DEPENDENCY_URL=http://localhost:9999/health node server.jsreadiness поверне 503, але liveness продовжить повертати успішну відповідь. Це важлива відмінність: процес живий, але його не слід використовувати для нового трафіку.
Одна й та сама перевірка може мати різні споживачі:
оркестратор контролює життєздатність і готовність;
балансувальник вилучає неготові екземпляри;
система моніторингу будує метрики та сповіщення;
інженер використовує діагностичні endpoints під час розслідування.
Не потрібно робити один універсальний endpoint із усіма можливими перевірками. Важливо, щоб кожен endpoint мав однозначний сенс і передбачувану вартість виконання.
Новий екземпляр не повинен отримувати трафік одразу після старту процесу, якщо йому потрібен час на:
встановлення з’єднань;
завантаження конфігурації;
ініціалізацію кешу;
перевірку критичних ресурсів.
У цей час liveness може бути успішною, а readiness — неуспішною.
Під час контрольного завершення роботи сервіс спочатку повинен стати неготовим, а вже потім припинити обробку.
Типова послідовність:
встановити стан stopping;
readiness починає повертати 503;
балансувальник припиняє направляти нові запити;
сервіс завершує вже прийняті запити;
процес закриває з’єднання і завершується.
Якщо спочатку завершити процес, частина запитів може потрапити на екземпляр, який уже не здатен їх обробити.
Health check сам є запитом і споживає ресурси. Якщо він перевіряє багато залежностей, під час масштабування виникає додаткове навантаження.
Наприклад, 100 екземплярів сервісу, кожен із перевіркою бази даних щосекунди, створюють 100 додаткових запитів за секунду. Тому:
перевіряйте лише необхідні залежності;
використовуйте короткі тайм-аути;
не виконуйте важкі бізнес-операції;
не запускайте нескінченні повтори;
не робіть health check залежним від іншого health check без потреби.
Якщо залежність тимчасово недоступна, readiness має допомогти зупинити надходження нового трафіку, але сама система не повинна почати масово перезапускати всі екземпляри.
Health check показує поточний стан, але не пояснює повну історію відмови. Для розслідування корисно окремо відстежувати:
кількість успішних і невдалих перевірок;
час відповіді залежностей;
кількість переходів у not ready;
тривалість перебування в неготовому стані;
причини невдач: тайм-аут, помилка з’єднання, HTTP-помилка.
Водночас не варто перетворювати кожну невдалу перевірку на негайне критичне сповіщення. Окремий короткий тайм-аут може бути тимчасовим явищем, а тривала втрата readiness — підставою для реагування.
Якщо база даних недоступна, liveness починає повертати помилку, а оркестратор перезапускає сервіс. Після перезапуску проблема з базою не зникає, тому всі екземпляри можуть потрапити в цикл перезапусків.
Перевірку критичної бази даних краще включати в readiness.
200 OK завждиEndpoint, який завжди повертає 200, не допомагає виявити неготовий екземпляр. Система продовжує направляти на нього трафік, навіть коли він не може обробити запити.
Недоступність необов’язкового сервісу не завжди означає, що основний сервіс не готовий. Перевіряйте тільки те, що необхідно для основної функціональності.
Зависла перевірка може зайняти робочий потік або з’єднання. Кожна мережева операція в health check повинна мати обмеження часу.
Одна мережева помилка не обов’язково означає відмову. Використовуйте послідовні невдачі, періодичні перевірки та пороги відновлення.
Health check не повинен створювати замовлення, виконувати складний запит або змінювати стан системи. Його завдання — швидко визначити придатність екземпляра.
Деталі з’єднань, секрети та повні тексти винятків не повинні потрапляти у зовнішню відповідь health check.
Визначте, що означає «процес живий».
Визначте, що означає «сервіс готовий приймати запити».
Складіть список критичних і необов’язкових залежностей.
Додайте зовнішні залежності лише до readiness.
Для кожної мережевої перевірки задайте тайм-аут.
Визначте HTTP-коди для успішного та неуспішного стану.
Налаштуйте кількість послідовних невдач і успішних повторів.
Опишіть поведінку під час запуску та завершення.
Переконайтеся, що health check не створює значного навантаження.
Перевірте сценарії: короткий тайм-аут, тривала відмова, відновлення залежності та зупинка сервісу.
Liveness показує, чи може процес продовжувати роботу.
Readiness показує, чи можна направляти на сервіс нові запити.
Зовнішні залежності зазвичай перевіряють у readiness, а не в liveness.
Одна невдала перевірка ще не обов’язково означає критичну відмову.
Тайм-аути, послідовні невдачі та гістерезис допомагають відрізняти тимчасові проблеми від стійких.
Під час завершення сервіс спочатку має стати неготовим, а потім припинити роботу.
Health checks повинні бути швидкими, безпечними, передбачуваними та не створювати каскадних проблем.