Howtojs

Graceful shutdown

5 марта 2021 г. • ☕️☕️ 10 мин.

Итак, мы запустили процесс node.js. Это может быть сервер, бесконечно принимающий HTTP запросы, либо функция, которой предстоит проделать много работы.

Как можно завершить процесс?

Закончились задачи для выполнения в очереди Event Loop (событийный цикл)

Event Loop имеет очередь задач. Задачи запускаются одна за другой.

Новые задачи могут появиться, если:

  • мы добавим их во время выполнения программы (setTimeout, …)
  • произойдет внешнее событие (сетевое соединение, …)

Процесс завершиться сам, если задачи закончились, за исключением ситуаций, когда мы создаем специальные объекты (server, interval…), которые не дают завершиться процессу в ожидании новых событий.

const server = net.createServer((socket) => {
  socket.end('goodbye\n')
})

server.listen() // не дает завершиться процессу в ожидании новых событий

Отправлен сигнал SIGKILL процессу

kill -s SIGKILL 1414

Процесс будет убит системой. Исполняемый код будет прерван.

Отправлен сигнал SIGINT или SIGTERM процессу

kill -s SIGINT 1414

Процесс будет остановлен, так как Node.js имеет стандартный обработчик этих сигналов. Исполняемый код будет прерван.

Нажата комбинация клавиш ctrl+c на клавиатуре

Сработает сигнал SIGINT.

Произошло событие unhandledRejection

async function main() {
  throw new Error("unhandledRejection")}

main()

Данное событие приходит, когда ошибка в Promise не была обработана catch. Процесс будет остановлен. Исполняемый код будет прерван.

Произошло событие uncaughtException

function main() {
  throw new Error("unhandledRejection")}

main()

Данное событие приходит, когда ошибка в не была обработана catch, без использования Promise. Процесс будет остановлен. Исполняемый код будет прерван.

Что здесь плохого?

Ключевая фраза исполняемый код будет прерван.

Обрыв обработки внешних соединений

Клиентские запросы, которые обрабатываются в данный момент, будут оборваны. В случае с HTTP это будет означать код ошибки 500.

Код который исполняется будет прерван

Функции выполняющие какие-либо задачи, могут быть прерванны по середине. Такое поведение может привести к неконсистентному состоянию какого-либо хранилища данных.

Зачем нужен Graceful shutdown?

Частью штатного процесса работы приложения являются:

  • масштабирование
  • деплой новой версии

В этом случае необходимы плавная остановка и перезапуск.

Что такое Graceful shutdown?

Плавное завершение процесса, без прерывания какой-либо работы.

  • позволяем функциям доделать работу до конца
  • перестаем принимать новые соединения, но не обрываем те, что в данный момент обрабатываются
  • дожидаемся состояния, когда закончились задачи для выполнения в очереди Event Loop
  • процесс завершается сам

Плюсы очевидны

  • отсутствие неконсистентного состояния в следсвии прерывания исполнения кода
  • отсутствие неожиданных обрывов соединений. В случае с HTTP код ошибки 500.

Как добиться Graceful shutdown?

Закончились задачи для выполнения в очереди Event Loop

Делать ничего не нужно, так как это и есть Graceful shutdown.

Отправлен сигнал SIGKILL процессу

Graceful shutdown сделать не получится, SIGKILL убьет процесс сразу.

События uncaughtException и unhandledRejection

Можно попробовать что-нибудь сделать, но не стоит. Ошибка такого уровня, говорит о неопределенном состоянии приложения.

Лучшее что можно сделать:

  • залоггировать ошибку
  • убить процесс “как есть” process.exit(1)
  • исправить ошибку в коде

Отправлен сигнал SIGINT или SIGTERM процессу

Обработка этих сигналов, является ключевым шагом при обеспечении Graceful shutdown.

Необходимо перехватить эти сигналы и обработать, результатом обработки должно последовать самостоятельное завершение процесса в следствии отсутствия задач в очереди Event Loop.

Graceful shutdown на практике

Представим, что у нас есть процесс, который подключен к БД, обрабатывает HTTP запросы и выполняет регулярные задачи с помощью setInterval, как его остановить?

  • обработать сигнал SIGINT или SIGTERM
  • завершить работу setInterval
  • перестать принимать новые HTTP запросы
  • подождать завершения уже начатых запросов
  • подождать завершения уже запущеного кода в setInterval
  • закрыть соединение с БД
  • позволить процессу завершиться самостоятельно
const server = http.createServer((req, res) => {})

let interval = null

db.conect(err => {
  if (err) throw err

  server.listen()

  server.on("listening", () => {
    interval = setInterval(() => {})
  })
})

process.on("SIGINT", () => {  clearInterval(interval)

  server.close(() => {
    db.close()
  })
})

Для упрощения данного примера, здесь не отслеживаются idle соединения KeepAlive и начатые задачи в setInterval.

Более полный пример, как отслеживать активные запросы и idle соединения может быть найден в одном из готовых npm модулей на github.

Здравый смысл

Несмотря на то, что я расхвалил Graceful shutdown, его поддержка может быть не тривиальна и отнимать драгоценное время на начале проекта. Если на сервере небольшое количесто запросов и всего несколько асинхронных “ночных задач”, то Graceful shutdown можно отложить на потом. Многие библиотеки в npm поддерживают плавное завершение, достаточно прочитать документацию, как это сделать. Так же, есть готовые библиотеки в npm, которые могут упростить интеграцию в существующие приложения.