Пошук уроків, статей та іншого контенту
Безпечно зупиняйте сервер, завершуйте активні запити та звільняйте ресурси під час деплою або сигналів ОС.
Під час деплою або зупинки процес Node.js отримує сигнал від операційної системи:
SIGTERM — типовий сигнал для завершення процесу в контейнерах і системах оркестрації;
SIGINT — зазвичай надходить після натискання Ctrl+C у терміналі.
Без спеціальної обробки процес може завершитися тоді, коли:
HTTP-відповідь ще не відправлена;
запис у базу даних або чергу ще не завершився;
відкриті з’єднання із зовнішніми сервісами;
буфер журналу ще не скинутий на диск.
Коректне завершення, або graceful shutdown, зазвичай складається з таких кроків:
Припинити приймати нові HTTP-з’єднання.
Дочекатися завершення активних запитів.
Закрити підключення до бази даних, черг та інших ресурсів.
Завершити процес із правильним кодом виходу.
Якщо завершення затягнулося, примусово припинити роботу після тайм-ауту.
server.close()Метод server.close():
припиняє приймати нові з’єднання;
дозволяє вже розпочатим запитам завершитися;
викликає callback після закриття з’єднань.
Виклик server.close() не перериває активні обробники миттєво. Якщо обробник запиту виконує довгу операцію, сервер чекатиме її завершення.
Під час graceful shutdown важливо не використовувати одразу process.exit(). Цей виклик завершує процес негайно та може перервати:
відправлення HTTP-відповіді;
асинхронне закриття підключення;
запис журналів;
завершення операції з базою даних.
Замість цього слід дочекатися всіх необхідних операцій і встановити process.exitCode.
Нижче сервер:
обробляє HTTP-запити;
відстежує активні запити та TCP-з’єднання;
реагує на SIGTERM і SIGINT;
припиняє приймати нові з’єднання;
закриває умовний зовнішній ресурс;
має тайм-аут для примусового завершення.
const http = require('node:http');
const PORT = 3000;
const SHUTDOWN_TIMEOUT = 10_000;
let isShuttingDown = false;
let shutdownPromise = null;
const sockets = new Set();
const activeRequests = new Set();
// У реальному застосунку тут може бути клієнт бази даних або черги.
const externalResource = {
async close() {
await new Promise((resolve) => setTimeout(resolve, 200));
console.log('Зовнішній ресурс закрито');
},
};
const server = http.createServer((req, res) => {
let resolveRequest;
const requestFinished = new Promise((resolve) => {
resolveRequest = resolve;
});
activeRequests.add(requestFinished);
let finished = false;
const timer = setTimeout(() => {
if (!res.writableEnded) {
res.end('Відповідь сервера\n');
}
}, 2_000);
const finishRequest = () => {
if (finished) {
return;
}
finished = true;
clearTimeout(timer);
activeRequests.delete(requestFinished);
resolveRequest();
};
res.once('finish', finishRequest);
res.once('close', finishRequest);
if (isShuttingDown) {
res.statusCode = 503;
res.end('Сервер завершує роботу\n');
return;
}
res.setHeader('Content-Type', 'text/plain; charset=utf-8');
res.write('Запит прийнято. Обробка триває...\n');
});
server.on('connection', (socket) => {
sockets.add(socket);
socket.once('close', () => {
sockets.delete(socket);
});
});
function closeServer() {
return new Promise((resolve, reject) => {
server.close((error) => {
if (error) {
reject(error);
return;
}
resolve();
});
// Закриваємо простоюючі keep-alive-з'єднання, якщо API доступне
// у поточній версії Node.js.
server.closeIdleConnections?.();
});
}
async function shutdown(signal) {
if (shutdownPromise) {
return shutdownPromise;
}
shutdownPromise = (async () => {
isShuttingDown = true;
console.log(`Отримано ${signal}. Починаємо завершення роботи...`);
const forceShutdownTimer = setTimeout(() => {
console.error('Час завершення вичерпано. Закриваємо з’єднання примусово.');
for (const socket of sockets) {
socket.destroy();
}
}, SHUTDOWN_TIMEOUT);
// Таймер не повинен сам утримувати процес, якщо інших активних ресурсів уже немає.
forceShutdownTimer.unref?.();
try {
// Нові з'єднання більше не приймаються.
await closeServer();
// Додатково чекаємо завершення обробників запитів,
// які ще залишилися зареєстрованими.
await Promise.all(activeRequests);
// Після HTTP-запитів можна закрити зовнішні ресурси.
await externalResource.close();
clearTimeout(forceShutdownTimer);
console.log('Сервер коректно завершив роботу');
process.exitCode = 0;
} catch (error) {
clearTimeout(forceShutdownTimer);
console.error('Помилка під час завершення:', error);
process.exitCode = 1;
}
})();
return shutdownPromise;
}
server.listen(PORT, () => {
console.log(`Сервер запущено на http://localhost:${PORT}`);
});
process.once('SIGTERM', () => {
void shutdown('SIGTERM');
});
process.once('SIGINT', () => {
void shutdown('SIGINT');
});Запустіть файл і відкрийте інший термінал:
curl http://localhost:3000Поки запит очікує відповідь, натисніть Ctrl+C у терміналі сервера. Сервер припинить приймати нові з’єднання, дочекається поточного запиту та закриє зовнішній ресурс.
server.close() керує HTTP-сервером, але приклад додатково відстежує запити в activeRequests.
Це може бути корисно, якщо потрібно:
показати кількість запитів під час завершення;
дочекатися завершення власних асинхронних операцій;
виконати додаткову логіку для кожного запиту;
контролювати запити, які можуть тривати довше за звичайні.
Подія finish означає, що відповідь повністю передана сервером. Подія close потрібна для випадку, коли клієнт розірвав з’єднання раніше.
Функція finishRequest захищена прапорцем finished, тому один запит не видаляється з набору двічі: обидві події можуть відбутися для однієї відповіді.
HTTP keep-alive дозволяє клієнту повторно використовувати TCP-з’єднання. Таке з’єднання може бути простоюючим: попередній запит уже завершено, але TCP-з’єднання ще відкрите.
Під час зупинки сервера:
активні з’єднання потрібно залишити для завершення запитів;
простоюючі keep-alive-з’єднання можна закрити;
після завершення тайм-ауту всі з’єднання можна знищити примусово.
У сучасних версіях Node.js для простоюючих з’єднань доступний метод server.closeIdleConnections(). Виклик через optional chaining:
server.closeIdleConnections?.();дозволяє запускати код і в середовищах, де цього методу немає.
Подія connection використовується для збереження всіх сокетів у Set. Це дає змогу примусово закрити їх після завершення тайм-ауту:
for (const socket of sockets) {
socket.destroy();
}Таке закриття є аварійним заходом. Воно може перервати активні HTTP-запити, тому його слід використовувати лише після розумного періоду очікування.
Сигнал може надійти повторно. Наприклад, процес може отримати SIGTERM, а потім ще один сигнал під час уже розпочатого завершення.
Функція shutdown() повинна бути ідемпотентною: повторний виклик не має запускати процедуру вдруге.
У прикладі для цього використовується shutdownPromise:
if (shutdownPromise) {
return shutdownPromise;
}Завдяки цьому:
сервер не закривається кілька разів;
ресурси не закриваються повторно;
кілька обробників сигналів очікують одну й ту саму операцію завершення.
Graceful shutdown не повинен тривати нескінченно. Причинами зависання можуть бути:
довгий або завислий запит;
клієнт, який не закриває з’єднання;
зовнішній сервіс, що не відповідає;
помилка в обробнику завершення.
Тому потрібно встановити обмеження часу. Після його завершення можна:
закрити відкриті сокети;
зафіксувати помилку в журналі;
встановити ненульовий process.exitCode, якщо завершення було аварійним.
Значення тайм-ауту потрібно узгоджувати з інфраструктурою. Якщо середовище деплою припиняє процес через 5 секунд, тайм-аут застосунку на 10 секунд не дасть очікуваного результату.
HTTP-сервер слід закривати перед зовнішніми ресурсами:
нові запити більше не приймаються;
активні запити завершуються;
операції, пов’язані із запитами, більше не створюються;
закривається база даних, клієнт черги або інший ресурс.
Порядок залежить від конкретного клієнта, але метод закриття має бути очікуваним:
await database.close();Не слід просто викликати асинхронне закриття без await:
database.close(); // процес може завершитися ранішеЯкщо ресурс не має методу close(), потрібно використати відповідний метод саме його клієнта. Не можна припускати, що достатньо зупинити лише HTTP-сервер: активні таймери, сокети або пул підключень можуть і далі утримувати процес.
process.exitCode визначає код, з яким Node.js завершить роботу після завершення подій у циклі:
0 — успішне завершення;
ненульове значення — помилка або аварійне завершення.
Перевага process.exitCode над process.exit() полягає в тому, що Node.js отримує змогу завершити вже заплановані асинхронні операції.
Примусовий виклик process.exit() варто залишати для ситуацій, коли процес гарантовано потрібно завершити негайно, наприклад після перевищення зовнішнього дедлайну. У звичайному graceful shutdown він може втратити незавершені дані.
process.exit() одразу після сигналуprocess.on('SIGTERM', () => {
process.exit(0);
});Такий код може обірвати активні запити та операції із зовнішніми ресурсами. Спочатку потрібно виконати процедуру завершення, а вже потім встановити код виходу.
Якщо спочатку закрити пул підключень, активні обробники можуть отримати помилки під час роботи.
Правильніше припинити приймання нових запитів, дочекатися поточних операцій і лише після цього закривати пул.
Кілька викликів server.close() або database.close() можуть призвести до помилок і непередбачуваного стану. Зберігайте спільний Promise завершення та повертайте його під час повторних викликів.
Один завислий запит може заблокувати весь деплой. Завжди визначайте максимальний час graceful shutdown і майте аварійний сценарій.
SIGINTЛокально процес часто зупиняють через Ctrl+C, але в продакшені типовим сигналом є SIGTERM. Застосунок має обробляти обидва сигнали або винести їх у спільну функцію завершення.
server.close() припиняє приймати нові з’єднання та дає активним запитам завершитися.
Активні запити й сокети можна відстежувати через Set.
Після зупинки HTTP-сервера слід закрити зовнішні ресурси.
Обробник сигналу має бути ідемпотентним.
Для завислих запитів потрібен тайм-аут і примусове закриття сокетів.
Для нормального завершення краще використовувати process.exitCode, а не негайний process.exit().
Обробляйте щонайменше SIGTERM і SIGINT.