Zero-downtime деплой TypeScript сервисов: пошаговое руководство

Онлайн-тренажер Python для начинающих

Изучайте Python легко и без перегрузки теорией. Решайте практические задачи с автоматической проверкой, получайте подсказки на русском языке и пишите код прямо в браузере — без необходимости что-либо устанавливать.

Начать курс

Как обновить сервис, не разбудив пользователей?

Представьте: пятница, вечер, вы решили быстро залить багфикс на прод. Нажимаете 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();
});

Продвинутые сов

Блоги

Рекомендаций по книгам