Антон Ларичев
Введение
Graceful shutdown в Node.js — это механизм корректного завершения приложения, при котором ни один пользовательский запрос не теряется. В продакшне, где каждый потерянный запрос — это потенциально потерянный клиент, правильная реализация graceful shutdown критически важна. В сочетании с health checks и стратегией zero-downtime deploy вы получаете систему, которая обновляется без простоя.
В этой статье разберём практический чек-лист: от обработки сигналов завершения до настройки readiness probe в Kubernetes. Все примеры — на Node.js с Docker и Kubernetes.
Как реализовать graceful shutdown в Node.js
При получении сигнала SIGTERM (который отправляет Docker или Kubernetes при остановке контейнера) приложение должно выполнить три действия: прекратить приём новых соединений, дождаться завершения текущих запросов и закрыть внешние подключения.
import { createServer } from 'http';
const server = createServer((req, res) => {
// Обработка запроса
res.writeHead(200);
res.end('OK');
});
let isShuttingDown = false;
// Обработчик graceful shutdown
function shutdown(signal: string) {
console.log(`Получен сигнал ${signal}, начинаем завершение...`);
isShuttingDown = true;
// Прекращаем приём новых соединений
server.close(() => {
console.log('Все соединения закрыты, завершаем процесс');
process.exit(0);
});
// Принудительное завершение через таймаут
setTimeout(() => {
console.error('Таймаут graceful shutdown, принудительное завершение');
process.exit(1);
}, 10000);
}
process.on('SIGTERM', () => shutdown('SIGTERM'));
process.on('SIGINT', () => shutdown('SIGINT'));
server.listen(3000);
Как закрыть соединения с базой данных и Redis при завершении
Одного server.close() недостаточно. Нужно закрыть все внешние ресурсы — пулы соединений с базой данных, Redis-клиенты, очереди сообщений:
import { Pool } from 'pg';
import { createClient } from 'redis';
const pgPool = new Pool({ connectionString: process.env.DATABASE_URL });
const redis = createClient({ url: process.env.REDIS_URL });
async function shutdown(signal: string) {
console.log(`Получен ${signal}, начинаем drain...`);
isShuttingDown = true;
server.close(async () => {
try {
// Закрываем пул PostgreSQL
await pgPool.end();
console.log('PostgreSQL пул закрыт');
// Закрываем Redis
await redis.quit();
console.log('Redis соединение закрыто');
process.exit(0);
} catch (err) {
console.error('Ошибка при закрытии ресурсов:', err);
process.exit(1);
}
});
setTimeout(() => process.exit(1), 15000);
}
Настройка health checks для Docker и Kubernetes
Health checks — это механизм, который сообщает оркестратору о состоянии приложения. В Kubernetes используются три типа проб: liveness probe, readiness probe и startup probe.
Эндпоинт health check в приложении
import express from 'express';
const app = express();
let isShuttingDown = false;
let isReady = false;
// Liveness — приложение живо
app.get('/health/live', (req, res) => {
res.status(200).json({ status: 'alive' });
});
// Readiness — приложение готово принимать трафик
app.get('/health/ready', (req, res) => {
if (isShuttingDown || !isReady) {
return res.status(503).json({ status: 'not ready' });
}
res.status(200).json({ status: 'ready' });
});
// Отмечаем готовность после инициализации
async function bootstrap() {
await pgPool.connect(); // Проверяем соединение с БД
await redis.connect();
isReady = true;
console.log('Приложение готово к приёму трафика');
}
Readiness probe в Kubernetes: как настроить правильно
apiVersion: apps/v1
kind: Deployment
metadata:
name: api-server
spec:
replicas: 3
template:
spec:
containers:
- name: api
image: myapp:latest
ports:
- containerPort: 3000
livenessProbe:
httpGet:
path: /health/live
port: 3000
initialDelaySeconds: 5
periodSeconds: 10
failureThreshold: 3
readinessProbe:
httpGet:
path: /health/ready
port: 3000
initialDelaySeconds: 3
periodSeconds: 5
failureThreshold: 2
startupProbe:
httpGet:
path: /health/live
port: 3000
failureThreshold: 30
periodSeconds: 2
terminationGracePeriodSeconds: 30
Ключевые параметры: readinessProbe с failureThreshold: 2 быстро выводит под из балансировки, а terminationGracePeriodSeconds задаёт максимальное время для graceful shutdown.
Чек-лист zero-downtime deploy для продакшна
Zero-downtime deploy — это стратегия обновления, при которой пользователи не замечают процесс деплоя. Вот полный чек-лист:
Приложение
- Обрабатывает SIGTERM и выполняет graceful shutdown
- Отдаёт 503 на readiness probe при завершении
- Закрывает все соединения (БД, Redis, очереди)
- Имеет таймаут принудительного завершения
- Эндпоинты
/health/liveи/health/readyреализованы
Docker
FROM node:20-alpine
WORKDIR /app
COPY package*.json ./
RUN npm ci --only=production
COPY dist/ ./dist/
# Используем tini для корректной передачи сигналов
RUN apk add --no-cache tini
ENTRYPOINT ["/sbin/tini", "--"]
HEALTHCHECK --interval=10s --timeout=3s --retries=3 \
CMD wget -qO- http://localhost:3000/health/live || exit 1
CMD ["node", "dist/main.js"]
Важный момент: без init-процесса (tini или --init в docker run) Node.js не получит SIGTERM, и контейнер будет убит через SIGKILL после таймаута.
Kubernetes
spec:
strategy:
type: RollingUpdate
rollingUpdate:
maxUnavailable: 0
maxSurge: 1
template:
spec:
containers:
- name: api
lifecycle:
preStop:
exec:
command: ["sh", "-c", "sleep 5"]
Параметр maxUnavailable: 0 гарантирует, что ни один под не будет удалён, пока новый не пройдёт readiness probe. А preStop hook с задержкой даёт время на распространение изменений в endpoint-ах кластера.
Частые ошибки при настройке безостановочного деплоя
Нет обработки SIGTERM. Приложение не слушает сигналы — контейнер убивается через SIGKILL через 30 секунд, все текущие запросы теряются.
Liveness вместо readiness. Если использовать только liveness probe без readiness, Kubernetes не знает, готов ли под к трафику. Новый под начинает получать запросы до полной инициализации.
Слишком короткий terminationGracePeriodSeconds. Если у вас есть длительные запросы (загрузка файлов, генерация отчётов), 30 секунд по умолчанию может не хватить. Увеличьте до 60-120 секунд.
Отсутствие preStop hook. Без задержки перед завершением Kubernetes может отправлять трафик на под, который уже начал завершаться. Добавьте sleep 5 в preStop.
Node.js как PID 1 в Docker. Без tini или --init Node.js запускается как PID 1 и не обрабатывает сигналы стандартным образом.
Заключение
Graceful shutdown, health checks и zero-downtime deploy — это три столпа надёжного продакшна. Реализация graceful shutdown в Node.js требует обработки SIGTERM, закрытия соединений и таймаута. Health checks через liveness и readiness probe дают оркестратору полную картину состояния приложения. А правильная стратегия rolling update с maxUnavailable: 0 и preStop hook обеспечивает безостановочный деплой.
Используйте чек-лист из этой статьи перед каждым выводом сервиса в продакшн — это сэкономит часы отладки и предотвратит потерю пользовательских запросов.






Комментарии
0