Graceful shutdown в Node.js: корректное завершение сервера
Содержание
О проверке кода. Примеры не запускались в этой сессии (по договорённости — фокус на контенте, а не прогонах). Говорим прямо, а не ставим бейдж «проверено».
Сервер не работает вечно: деплой новой версии, перезапуск, масштабирование — всё это останавливает процесс. Вопрос в том, как он останавливается. Мгновенное убийство обрывает работу на полуслове; graceful shutdown завершает её чисто. Разберём, как это делают в Node.js.
Проблема: мгновенная остановка
Представьте: идёт деплой, оркестратор шлёт серверу сигнал остановки. Если процесс умирает немедленно:
- текущие запросы обрываются — пользователь получает ошибку вместо ответа;
- незавершённые записи в базу прерываются на середине — риск порчи данных;
- открытые соединения не закрываются корректно — БД и внешние сервисы держат зависшие подключения;
- сообщения из очереди теряются, не будучи обработанными.
При деплое несколько раз в день это регулярные обрывы для пользователей. Graceful shutdown решает проблему.
Перехват сигналов остановки
Система сообщает процессу о необходимости остановиться сигналами:
// SIGTERM — от оркестратора/менеджера при остановке (деплой, масштабирование)
process.on('SIGTERM', () => shutdown('SIGTERM'));
// SIGINT — Ctrl+C в терминале
process.on('SIGINT', () => shutdown('SIGINT'));
SIGTERM — стандартный сигнал «завершись» от Docker, Kubernetes, PM2 при остановке или передеплое. SIGINT — нажатие Ctrl+C. Перехватив их через process.on, вы получаете шанс завершиться корректно до того, как система убьёт процесс жёстко.
Процедура корректного завершения
const server = app.listen(3000);
async function shutdown(signal) {
console.log(`Получен ${signal}, завершаюсь корректно...`);
// 1. перестать принимать НОВЫЕ соединения (текущие доработают)
server.close(async () => {
console.log('Новые запросы не принимаются, текущие завершены');
// 2. закрыть соединения с внешними сервисами
await db.close();
await redis.quit();
console.log('Все ресурсы освобождены, выхожу');
process.exit(0); // чистый выход
});
// 3. таймаут — если что-то зависло, не ждать вечно
setTimeout(() => {
console.error('Завершение затянулось, выхожу принудительно');
process.exit(1);
}, 10000).unref();
}
- 1 Пришёл SIGTERMОркестратор просит остановиться
- 2 server.close()Новые запросы не принимаем, текущие доделываем
- 3 Закрыть БД, Redis, очередиОсвобождаем внешние соединения
- 4 process.exit(0)Чистый выход. Или таймаут → выход принудительно
Ключевой момент — server.close() перестаёт принимать новые соединения, но даёт текущим запросам завершиться. Это не мгновенная смерть, а «доработать начатое и не брать нового». После того как активные запросы обслужены, закрывают соединения с БД, Redis, очередями — и только тогда выходят.
Таймаут: страховка от зависания
setTimeout(() => {
console.error('Не уложились в таймаут, принудительный выход');
process.exit(1);
}, 10000).unref();
Таймаут обязателен. Если один запрос завис (медленный внешний API, вечный цикл) или соединение не закрывается, без таймаута процесс будет ждать вечно — и корректное завершение превратится в зависание. Таймаут (обычно 10–30 секунд) гарантирует выход в любом случае. Всё равно оркестратор через свой лимит убьёт процесс жёстко (SIGKILL) — лучше завершиться самому чисто до этого. unref() не даёт таймеру самому держать процесс живым.
Как это работает при деплое
Полная картина при передеплое под cluster или Kubernetes:
- оркестратор запускает новый экземпляр сервера;
- старому шлёт
SIGTERM; - старый перестаёт брать новые запросы (их уже направляют на новый экземпляр);
- старый дорабатывает свои текущие запросы;
- закрывает соединения и чисто выходит.
Результат — бесперебойный деплой без единого оборванного запроса. Пользователи не замечают перезапуск. Именно этого не хватает, если завершать процесс грубо.
Практический чек-лист
- перехватывайте
SIGTERMиSIGINT; server.close()— прекратить приём новых, доделать текущие;- закрывайте соединения с БД, кэшем, очередями;
- всегда ставьте таймаут на случай зависания;
- логируйте этапы завершения — пригодится при отладке деплоя;
- сюда же выносите завершение при критических ошибках — чистый выход перед перезапуском.
Graceful shutdown — признак продакшн-готового сервиса. Его часто забывают, пока обрывы при деплое не начнут раздражать пользователей.
Смежные темы: обработка ошибок — выход при сбое; cluster — перезапуск воркеров; process и сигналы; логирование. Полный список — в уроках Node.js.