Как обновить сервис, не разбудив пользователей?
Представьте: пятница, вечер, вы решили быстро залить багфикс на прод. Нажимаете deploy — и ваш мессенджер, онлайн-кинотеатр или CRM ложится на 30 секунд. Тысячи пользователей в этот момент видят 502 Bad Gateway. А если это платежный шлюз? Считайте убытки.
Zero-downtime деплой (развертывание с нулевым временем простоя) — это не роскошь, а стандарт для современных TypeScript-сервисов. В этой статье мы разберем реальные техники, которые используют в продакшене: от graceful shutdown до сине-зеленого деплоя и rolling update. Вы получите готовые к применению паттерны на TypeScript.
Почему обычный деплой TypeScript-приложения опасен?
Когда вы запускаете npm run build && node dist/index.js, происходит несколько проблем:
- Разрыв соединений — старый процесс убивается, пока новые запросы еще идут.
- Потеря данных в памяти — несохраненные сессии, кэш, очереди.
- Грязный старт — новый процесс не успел прогреться, а уже получает трафик.
В TypeScript (Node.js) все усугубляется асинхронностью: вы не знаете, какие промисы еще выполняются. Но решение есть.
Техника #1: Graceful Shutdown (вежливое завершение)
База zero-downtime — корректная обработка сигналов ОС. Когда оркестратор (Kubernetes, PM2) решает остановить ваш сервис, он посылает SIGTERM. Ваш код должен перестать принимать новые запросы, дождаться завершения текущих и только потом умереть.
Реализация на TypeScript с Express
import express, { Request, Response } from 'express';
import http from 'http';
const app = express();
let server: http.Server;
app.get('/health', (req: Request, res: Response) => {
res.status(200).json({ status: 'ok' });
});
// Сигнал от Docker/K8s
process.on('SIGTERM', async () => {
console.log('Получен SIGTERM, начинаем graceful shutdown...');
// 1. Перестаем слушать новые соединения
server.close(() => {
console.log('HTTP сервер закрыт');
// 2. Закрываем соединения с БД, Redis и т.д.
// await db.disconnect();
process.exit(0);
});
// 3. Даем время текущим запросам (таймаут 30 сек)
setTimeout(() => {
console.error('Принудительное завершение по таймауту');
process.exit(1);
}, 30000);
});
const PORT = process.env.PORT || 3000;
server = app.listen(PORT, () => {
console.log(`Сервис запущен на порту ${PORT}`);
});
Важно: в TypeScript используйте http.Server из модуля http, а не тип Express. Метод server.close() корректно обрабатывает keep-alive соединения.
Техника #2: Rolling Update (постепенное обновление)
Если у вас несколько реплик сервиса (запущено 3-5 инстансов), rolling update обновляет их по одному. Пока один перезагружается, другие принимают трафик.
Пример для Kubernetes Deployment
# deployment.yaml
apiVersion: apps/v1
kind: Deployment
metadata:
name: my-typescript-service
spec:
replicas: 3
strategy:
type: RollingUpdate
rollingUpdate:
maxUnavailable: 1 # сколько подов может быть недоступно
maxSurge: 1 # сколько новых подов можно создать сверх плана
template:
spec:
containers:
- name: app
image: myregistry/service:v2
ports:
- containerPort: 3000
livenessProbe:
httpGet:
path: /health
port: 3000
initialDelaySeconds: 5
periodSeconds: 10
readinessProbe:
httpGet:
path: /ready
port: 3000
initialDelaySeconds: 3
periodSeconds: 5
Добавьте в ваше TypeScript-приложение эндпоинт /ready, который возвращает 200 только когда сервис полностью инициализирован (БД подключена, кэш прогрет):
let isReady = false;
async function initialize() {
await connectToDatabase();
await warmUpCache();
isReady = true;
}
app.get('/ready', (req: Request, res: Response) => {
if (isReady) {
res.status(200).json({ ready: true });
} else {
res.status(503).json({ ready: false });
}
});
initialize();
Техника #3: Blue-Green Deployment (сине-зеленый деплой)
Это элегантный, но ресурсоемкий метод. У вас две идентичные среды (Blue — текущая, Green — новая). Вы переключаете трафик с Blue на Green мгновенно, а старую среду убиваете позже.
Плюсы: идеальный откат (просто переключите обратно), никаких простоев.
Минусы: нужно вдвое больше ресурсов (серверов, памяти).
Реализация через прокси-слой (nginx, HAProxy)
# nginx.conf
upstream backend {
server blue.service.internal:3000 weight=100;
# при деплое меняем на:
# server green.service.internal:3000 weight=100;
}
server {
listen 80;
location / {
proxy_pass http://backend;
proxy_http_version 1.1;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection 'upgrade';
}
}
Автоматизируйте с помощью скрипта на TypeScript (запускается в CI/CD):
import { exec } from 'child_process';
async function switchTraffic(to: 'blue' | 'green') {
const config = `upstream backend {
server ${to}.service.internal:3000;
}`;
// Обновляем конфиг nginx и релоадим
await writeFile('/etc/nginx/conf.d/service.conf', config);
exec('nginx -s reload', (err) => {
if (err) throw err;
console.log(`Трафик переключен на ${to}`);
});
}
// В пайплайне:
// await switchTraffic('green');
// await healthCheck('green.service.internal');
// await killOld('blue');
Техника #4: Canary Deployment (канареечное развертывание)
Трафик направляется на новую версию постепенно: 5% → 20% → 50% → 100%. Если метрики падают, откат происходит автоматически.
Для TypeScript-сервисов используйте Service Mesh (Istio, Linkerd) или библиотеки вроде @opentelemetry/api для сбора метрик.
// Пример простого canary-роутинга на Express
const CANARY_PERCENT = 0.05; // 5% трафика
app.use((req, res, next) => {
const isCanary = Math.random() < CANARY_PERCENT;
if (isCanary) {
req.headers['x-canary'] = 'true';
// прокси на новую версию
return proxy.web(req, res, { target: 'http://new-service:3001' });
}
next();
});