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
, которые могут упростить интеграцию в существующие приложения.