Завершение HTTP-сервера Node.js без обрыва активных запросов требует правильной обработки сигналов остановки и ожидания завершения асинхронных операций.
Почему завершение сервера Node.js обрывает активные запросы и как это исправить
По умолчанию процесс Node.js реагирует на сигнал остановки (например, Ctrl+C или сигнал от оркестратора вроде PM2) немедленным завершением. В этот момент все открытые TCP-сокеты закрываются принудительно.
Клиентская часть получает ошибку сброса соединения, так как сервер не успевает отправить ответ на текущий запрос или корректно закрыть соединение после завершения обработки.
Исправление заключается в перехвате сигнала остановки и явном вызове метода shutdown() у HTTP-сервера с параметром graceful (по умолчанию true), а также ожиданием завершения всех pending операций.
Как настроить graceful shutdown для обработки текущих соединений
Используйте метод server.on('SIGINT', …) и server.on('SIGTERM', …). Внутри обработчика вызывайте server.close(callback), где callback сработает, когда все соединения будут закрыты.
Внутри callback проверьте количество активных соединений. Если они есть, логгируйте предупреждение о том, что сервер завершает работу с незавершенными запросами, но не прерывайте процесс принудительно.
Добавьте таймаут (например, 30 секунд) после вызова close(). Если за это время соединения не закрылись, можно либо логировать ошибку, либо принудительно завершить процесс через process.exit(1), чтобы избежать зависания процесса в фоне.
Роль таймаутов (timeout) при остановке процесса
Таймаут необходим для предотвращения бесконечного ожидания, если клиент не отвечает или соединение 'висит' в состоянии half-closed.
Если сервер ждет закрытия соединения дольше заданного лимита, это может указывать на проблему с сетью или зависший клиент. В продакшене стоит настроить логику: попытка закрыть -> ожидание таймаута -> принудительный выход.
Пример настройки: setTimeout(() => { console.log('Сервер не смог завершить работу вовремя'); process.exit(1); }, 30000);
Использование signal handlers для перехвата сигналов остановки
Сигналы SIGINT и SIGTERM являются стандартными сигналами прерывания. Node.js предоставляет обработчики для них через события на объекте process.
Рекомендуется обрабатывать оба сигнала в одном месте, чтобы избежать дублирования кода. Часто используют переменную isShuttingDown для предотвращения повторного входа в блок обработки остановки.
Пример структуры: при получении сигнала ставим флаг isShuttingDown = true и вызываем server.close(). Это гарантирует, что новые запросы не будут приняты.
Пример кода с обработкой ошибок и логированием состояния
Создайте сервер Express или http.createServer. Добавьте слушатели событий SIGINT и SIGTERM.
Внутри обработчика вызывайте server.close((err) => { if (err) throw err; console.log('Сервер остановлен'); process.exit(0); });
Добавьте логику отмены pending запросов, если вы используете библиотеки вроде axios или fetch внутри сервера. Используйте методы cancel token или просто игнорируйте новые входящие запросы после флага isShuttingDown.
Ограничения асинхронных операций при завершении работы
Node.js является однопоточным, но асинхронным. При остановке сервера все pending операции в event loop должны быть завершены или отменены.
Если операция занимает слишком много времени (например, долгий запрос к базе данных), она может не успеть завершиться до остановки процесса. Это приводит к потере данных или ошибкам на клиенте.
Решение: настройте таймауты для внешних вызовов и используйте механизмы отмены операций (cancel tokens) в библиотеках, поддерживающих их.
Проверка готовности сервера к остановке через метрики
Мониторинг количества активных соединений помогает понять, насколько долгой будет процедура остановки.
Если количество активных соединений растет и не падает при попытке остановки, это сигнал о проблеме с закрытием сокетов или зависших клиентах.
Используйте метрики из Prometheus или аналогичных систем для отслеживания времени между сигналом остановки и полным завершением процесса.
Рекомендации по тестированию процесса остановки в продакшене
Тестируйте остановку сервера в среде, максимально приближенной к продакшену, включая нагрузку и внешние зависимости.
Имитируйте сигнал SIGTERM через скрипт или оркестратор (например, kubectl exec для отправки сигнала контейнеру).
Проверяйте логи на наличие ошибок ECONNRESET и убедитесь, что все запросы обрабатываются корректно до момента закрытия соединения.
Вопросы и ответы
Можно ли использовать process.exit(0) сразу после получения сигнала SIGINT?
Нет, это приведет к обрыву активных соединений. Всегда вызывайте server.close() и ждите завершения всех операций перед процессом.exit().
Что делать, если сервер не отвечает на сигнал остановки?
Проверьте, обрабатываются ли сигналы SIGINT/SIGTERM в коде. Также убедитесь, что процесс не находится в состоянии блокировки (deadlock) из-за асинхронных операций.
Как отменить pending запросы при остановке сервера?
Используйте механизмы отмены, предоставляемые библиотеками (например, AbortController для fetch), или настройте таймауты на уровне приложения.
По теме
const http = require('http');
const server = http.createServer((req, res) => {
res.writeHead(200);
res.end('Hello World');
});
server.listen(3000, () => {
console.log('Server running on port 3000');
});
// Обработка сигналов остановки
process.on('SIGINT', () => {
console.log('Received SIGINT, shutting down gracefully...');
server.close(() => {
console.log('Closed out all keep-alive connections.');
process.exit(0);
});
});
process.on('SIGTERM', () => {
console.log('Received SIGTERM, shutting down gracefully...');
server.close(() => {
console.log('Closed out all keep-alive connections.');
process.exit(0);
});
});