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();
}
Порядок graceful shutdown
  1. 1 Пришёл SIGTERMОркестратор просит остановиться
  2. 2 server.close()Новые запросы не принимаем, текущие доделываем
  3. 3 Закрыть БД, Redis, очередиОсвобождаем внешние соединения
  4. 4 process.exit(0)Чистый выход. Или таймаут → выход принудительно

Ключевой момент — server.close() перестаёт принимать новые соединения, но даёт текущим запросам завершиться. Это не мгновенная смерть, а «доработать начатое и не брать нового». После того как активные запросы обслужены, закрывают соединения с БД, Redis, очередями — и только тогда выходят.

Таймаут: страховка от зависания

setTimeout(() => {
  console.error('Не уложились в таймаут, принудительный выход');
  process.exit(1);
}, 10000).unref();

Таймаут обязателен. Если один запрос завис (медленный внешний API, вечный цикл) или соединение не закрывается, без таймаута процесс будет ждать вечно — и корректное завершение превратится в зависание. Таймаут (обычно 10–30 секунд) гарантирует выход в любом случае. Всё равно оркестратор через свой лимит убьёт процесс жёстко (SIGKILL) — лучше завершиться самому чисто до этого. unref() не даёт таймеру самому держать процесс живым.

Как это работает при деплое

Полная картина при передеплое под cluster или Kubernetes:

  1. оркестратор запускает новый экземпляр сервера;
  2. старому шлёт SIGTERM;
  3. старый перестаёт брать новые запросы (их уже направляют на новый экземпляр);
  4. старый дорабатывает свои текущие запросы;
  5. закрывает соединения и чисто выходит.

Результат — бесперебойный деплой без единого оборванного запроса. Пользователи не замечают перезапуск. Именно этого не хватает, если завершать процесс грубо.

Практический чек-лист

  • перехватывайте SIGTERM и SIGINT;
  • server.close() — прекратить приём новых, доделать текущие;
  • закрывайте соединения с БД, кэшем, очередями;
  • всегда ставьте таймаут на случай зависания;
  • логируйте этапы завершения — пригодится при отладке деплоя;
  • сюда же выносите завершение при критических ошибках — чистый выход перед перезапуском.

Graceful shutdown — признак продакшн-готового сервиса. Его часто забывают, пока обрывы при деплое не начнут раздражать пользователей.


Смежные темы: обработка ошибок — выход при сбое; cluster — перезапуск воркеров; process и сигналы; логирование. Полный список — в уроках Node.js.

Частые вопросы

Что такое graceful shutdown?
Корректное завершение сервера: вместо мгновенной остановки приложение перестаёт принимать новые запросы, доводит до конца уже начатые, закрывает соединения с базой и только потом выходит. Это предотвращает обрыв запросов и порчу данных.
Зачем нужно корректное завершение?
При деплое и перезапуске сервер получает сигнал остановки. Если убить процесс сразу, текущие запросы оборвутся, а незакрытые соединения и незавершённые записи в базу могут повредить данные. Graceful shutdown доводит начатое до конца.
Как перехватить сигнал остановки в Node?
Через process.on для SIGTERM и SIGINT. SIGTERM посылают системы оркестрации и менеджеры процессов при остановке, SIGINT — нажатие Ctrl+C. В обработчике запускают процедуру корректного завершения.
Что делать в обработчике завершения?
Остановить приём новых запросов через server.close, дождаться завершения текущих, закрыть соединения с базой и другими сервисами, затем вызвать process.exit. Обязательно ставят таймаут на случай, если что-то зависло.
Зачем нужен таймаут при завершении?
Чтобы зависший запрос или соединение не заблокировали остановку навсегда. Если корректное завершение не уложилось в отведённое время, процесс принудительно выходит. Иначе оркестратор всё равно убьёт его жёстко через свой таймаут.