Zero-downtime деплой: почему это критично для вашего JavaScript сервиса?
Представьте: пятница, вечер, вы деплоите новую фичу на Node.js — и через минуту в чат техподдержки летят панические сообщения: «Сайт лежит!», «500 ошибка!», «Пропали данные!». Знакомо? Если ваш JavaScript сервис перестаёт отвечать на время обновления — вы теряете не только пользователей, но и деньги, и репутацию.
Zero-downtime деплой (развёртывание без простоев) — это подход, при котором обновление кода происходит незаметно для пользователя. Все запросы обрабатываются, соединения не рвутся, а новый код вступает в работу плавно. Для Node.js, где всё завязано на асинхронности и событиях, это особенно важно: одно резкое прерывание может привести к утечкам памяти или необработанным промисам.
В этой статье я покажу, как настроить бесшовный деплой JavaScript сервисов на практике. Мы разберём три рабочих стратегии — Rolling Update, Blue-Green Deployment и Graceful Shutdown — с реальными примерами кода. Поехали!
Стратегия 1: Rolling Update — обновление без остановки
Rolling update (постепенное обновление) — самая популярная техника в мире Node.js. Суть проста: у вас работает несколько экземпляров приложения (инстансов), и вы обновляете их по одному. Пока один перезапускается, остальные продолжают принимать трафик.
Как это работает на практике
Допустим, у вас 4 процесса Node.js, запущенных через PM2 (популярный менеджер процессов). Команда pm2 reload app автоматически делает rolling restart: убивает по одному процессу и запускает новый код. Но чтобы это работало без ошибок, нужно правильно настроить graceful shutdown (об этом ниже).
Пример настройки PM2 для zero-downtime
Создайте файл ecosystem.config.js в корне проекта:
module.exports = {
apps: [{
name: 'my-javascript-service',
script: './server.js',
instances: 4, // 4 инстанса
exec_mode: 'cluster', // кластерный режим
wait_ready: true, // ждём сигнал ready
listen_timeout: 5000, // таймаут на готовность
kill_timeout: 5000, // таймаут на убийство
max_restarts: 10,
env: {
NODE_ENV: 'production'
}
}]
};
А в коде сервера (server.js) добавьте обработку сигналов:
const http = require('http');
const app = require('./app'); // ваше Express/Koa приложение
const server = http.createServer(app);
const PORT = process.env.PORT || 3000;
server.listen(PORT, () => {
console.log(`Сервер запущен на порту ${PORT}`);
// Отправляем PM2 сигнал, что процесс готов
if (process.send) {
process.send('ready');
}
});
// Graceful shutdown
process.on('SIGINT', () => {
console.log('Получен SIGINT, начинаем graceful shutdown...');
server.close(() => {
console.log('HTTP сервер закрыт');
process.exit(0);
});
});
process.on('SIGTERM', () => {
console.log('Получен SIGTERM, завершаем работу...');
server.close(() => {
console.log('HTTP сервер закрыт');
process.exit(0);
});
});
Теперь при выполнении pm2 reload ecosystem.config.js PM2 будет по очереди останавливать и запускать инстансы, дожидаясь сигнала ready. Пользователи не заметят переключения.
Плюсы и минусы Rolling Update
- + Не требует дополнительных серверов или прокси.
- + Легко настраивается через PM2 или Kubernetes.
- - Если новый код содержит критическую ошибку, «плохой» инстанс может успеть обработать несколько запросов.
- - Требует аккуратного управления состоянием (базы данных, кэш).
Стратегия 2: Blue-Green Deployment — две среды без риска
Blue-Green deployment — это подход, при котором у вас всегда работают две идентичные среды: синяя (Blue) — текущая стабильная версия, зелёная (Green) — новая версия. Вы переключаете трафик с Blue на Green мгновенно, а если что-то пошло не так — откатываетесь назад.
Как реализовать на JavaScript
Вам потребуется балансировщик нагрузки (например, Nginx или HAProxy), который будет направлять запросы к нужной группе процессов. Предположим, у вас два порта: 3001 (Blue) и 3002 (Green).
Пример конфигурации Nginx для Blue-Green:
upstream backend {
server 127.0.0.1:3001; # Blue (текущая версия)
server 127.0.0.1:3002; # Green (новая версия)
}
server {
listen 80;
server_name example.com;
location / {
proxy_pass http://backend;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
}
}
Чтобы сделать деплой, вы:
- Запускаете новую версию на порту 3002 (Green).
- Проверяете health check (curl http://localhost:3002/health).
- Переключаете Nginx на Green, меняя upstream на
server 127.0.0.1:3002;. - Останавливаете Blue (порт 3001).
Вот скрипт деплоя на Node.js (с использованием child_process):
const { execSync } = require('child_process');
const deploy = async () => {
console.log('Запускаем Green (новую версию)...');
execSync('pm2 start server.js --name green -- --port=3002', { stdio: 'inherit' });
console.log('Проверяем health...');
const health = execSync('curl -s -o /dev/null -w "%{http_code}" http://localhost:3002/health');
if (health.trim() === '200') {
console.log('Green здоров, переключаем трафик...');
// Меняем конфиг Nginx и перезагружаем
execSync('cp nginx-green.conf /etc/nginx/conf.d/default.conf');
execSync('nginx -s reload');
console.log('Останавливаем Blue...');
execSync('pm2 stop blue');
execSync('pm2 delete blue');
console.log('Деплой завершён!');
} else {
console.error('Health check провален, откат...');
execSync('pm2 stop green');
execSync('pm2 delete green');
}
};
deploy().catch(console.error);
Плюсы и минусы Blue-Green
- + Мгновенное переключение — почти zero-downtime.
- + Лёгкий откат: просто переключаете обратно на Blue.
- - Нужно вдвое больше ресурсов (сервера, память).
- - Сложнее с база