Когда сайт падает на 5 минут во время деплоя — это не страшно, если он показывает котиков. Но если это интернет-магазин в часы пик или сервис с SLA 99.9% — каждая минута простоя стоит денег и нервов.

Zero-downtime deployment решает ровно эту проблему: обновляешь приложение, а пользователи продолжают работать как ни в чём не бывало.

Почему вообще бывает даунтайм

При классическом деплое картина такая: останавливаешь старый процесс, заливаешь новый код, запускаешь. Между остановкой и запуском — окно, когда сервис недоступен. Может быть 10 секунд, может 3 минуты — зависит от того, насколько тяжёлое приложение.

Ещё хуже, если при деплое что-то пошло не так. Старый код уже убит, новый не стартует — и ты судорожно откатываешься вручную, пока в Slack сыплются алерты.

Zero-downtime deployment — это набор стратегий, которые позволяют менять версию приложения без этого окна недоступности.

Blue-Green deployment

Самая понятная стратегия. Держишь два одинаковых окружения: синее (текущее, работает) и зелёное (новое, готовится).

Процесс такой:

  1. Зелёное окружение поднимаешь с новой версией приложения
  2. Прогоняешь smoke-тесты прямо в проде, но трафик туда ещё не идёт
  3. Переключаешь балансировщик: теперь весь трафик идёт в зелёное
  4. Синее остаётся запущенным ещё какое-то время — на случай быстрого отката

Переключение балансировщика занимает секунды. Пользователи не замечают ничего.

Откат тоже тривиален: переключаешь балансировщик обратно на синее. Буквально одна команда или клик.

Минус — нужно вдвое больше инфраструктуры в момент деплоя. Для маленьких проектов это может быть накладно, для больших — норма.

Пример с Nginx:

upstream app {
    server green-app:3000;  # переключаем с blue на green
}

Либо через переменную окружения в docker-compose, либо через DNS, либо через API балансировщика — вариантов много.

Canary deployment

Название — от шахтёрской практики брать канарейку в шахту. Птица гибнет первой, шахтёры успевают выбраться.

Суть: новую версию получает не весь трафик сразу, а небольшой процент пользователей — скажем, 5%. Смотришь на метрики: ошибки, latency, конверсия. Если всё хорошо — постепенно увеличиваешь процент. 5% → 20% → 50% → 100%.

Это особенно ценно, когда деплоишь что-то рискованное: изменения в бизнес-логике, новый алгоритм рекомендаций, переработанный checkout.

Настроить это можно через Nginx:

upstream app {
    server old-app:3000 weight=95;
    server new-app:3000 weight=5;
}

Либо через Istio/Linkerd в Kubernetes — там это делается через VirtualService и можно автоматизировать постепенный rollout по метрикам.

Главное правило: мониторинг должен быть настроен до того, как начинаешь canary. Смотреть на rate ошибок, p95 latency, и бизнес-метрики. Если что-то пошло не так — откат одной командой.

Rolling deployment

Классика в Kubernetes. Вместо того чтобы разом перезапускать все поды — обновляешь их по одному или группами.

Kubernetes делает это из коробки:

spec:
  strategy:
    type: RollingUpdate
    rollingUpdate:
      maxSurge: 1        # сколько лишних подов можно поднять
      maxUnavailable: 0  # сколько подов может быть недоступно

С maxUnavailable: 0 — в любой момент все поды доступны. Сначала поднимается новый под, проходит readiness probe, только потом убивается старый.

Readiness probe — критически важная часть:

readinessProbe:
  httpGet:
    path: /health
    port: 3000
  initialDelaySeconds: 10
  periodSeconds: 5

Пока новый под не ответил 200 на /health — трафик на него не идёт. Это защищает от ситуации, когда приложение запустилось, но ещё не готово принимать запросы.

Проблема с базой данных

Вот тут начинается самое интересное. Можно сколько угодно изящно деплоить приложение — но если миграция базы ломает совместимость, всё рушится.

Представь: у тебя есть колонка user_name. Ты хочешь переименовать её в username. Простая миграция ALTER TABLE RENAME COLUMN — и старый код, который ещё работает на части подов, начинает падать с ошибками.

Правило для zero-downtime с БД: миграции должны быть обратно совместимы.

Переименование колонки делается в три деплоя:

Деплой 1: добавляешь новую колонку username, пишешь в обе

ALTER TABLE users ADD COLUMN username VARCHAR(255);

Приложение читает из user_name, пишет в обе.

Деплой 2: переключаешь чтение на username Приложение читает из username, пишет в обе.

Деплой 3: удаляешь старую колонку

ALTER TABLE users DROP COLUMN user_name;

Медленно? Да. Зато без простоя и без страха, что откат сломает данные.

To же самое с индексами. CREATE INDEX без CONCURRENTLY блокирует таблицу. С CONCURRENTLY — нет:

CREATE INDEX CONCURRENTLY idx_users_email ON users(email);

Health checks и graceful shutdown

Даже при rolling deployment есть момент, когда старый под убивается. Если в этот момент он обрабатывал запросы — они упадут.

Грамотный graceful shutdown выглядит так:

  1. Kubernetes посылает поду сигнал SIGTERM
  2. Приложение перестаёт принимать новые соединения
  3. Дожидается завершения текущих запросов
  4. Выходит

В Node.js:

process.on('SIGTERM', async () => {
  console.log('SIGTERM received, starting graceful shutdown');
  server.close(() => {
    console.log('HTTP server closed');
    process.exit(0);
  });
  // даём 30 секунд на завершение
  setTimeout(() => process.exit(1), 30000);
});

В Kubernetes нужно настроить terminationGracePeriodSeconds достаточно большим, чтобы приложение успело завершить обработку:

spec:
  terminationGracePeriodSeconds: 60

Ещё важный момент: после получения SIGTERM под продолжает числиться в endpoints ещё несколько секунд, пока kube-proxy обновит правила. Поэтому перед shutdown стоит добавить небольшую задержку:

process.on('SIGTERM', async () => {
  await sleep(5000); // ждём, пока нас уберут из балансировки
  server.close(...);
});

Feature flags как инструмент деплоя

Фиче-флаги — отдельная история, но они прекрасно дополняют zero-downtime deployment.

Идея: деплоишь код с новой фичей, но она выключена. Включаешь её через конфиг или систему управления флагами — без нового деплоя.

Это позволяет:

  • Деплоить код постепенно, не открывая фичу
  • Включить фичу для конкретных пользователей или процента трафика
  • Моментально выключить, если что-то пошло не так

Простая реализация — переменная окружения или запись в Redis. Сложная — LaunchDarkly, Unleash, собственный сервис.

Что нужно для start

Если ты только начинаешь внедрять zero-downtime deployment, вот минимальный чеклист:

Обязательно:

  • Контейнеризация (Docker) — без этого сложно
  • Нормальные health checks (/health endpoint, который проверяет реальное состояние)
  • Graceful shutdown в приложении
  • Хранение состояния вне приложения (сессии в Redis, а не в памяти)

Желательно:

  • Kubernetes или аналог (Docker Swarm, Nomad)
  • Автоматизированные smoke-тесты после деплоя
  • Алерты на rate ошибок в первые минуты после деплоя
  • Автоматический откат при превышении порога ошибок

Для зрелых команд:

  • Canary deployment с автоматическим анализом метрик
  • Progressive delivery (Argo Rollouts, Flagger)
  • Chaos engineering для проверки устойчивости

Реальные цифры

Несколько ориентиров из практики:

  • Rolling deployment в Kubernetes при правильной настройке: 0 секунд даунтайма, деплой занимает 3-10 минут в зависимости от количества подов
  • Blue-green переключение: 0-1 секунда (время обновления DNS или перезагрузки конфига балансировщика)
  • Canary с автоматическим анализом: 15-30 минут на полный rollout, зато максимальная безопасность
  • Время восстановления при проблеме: секунды при blue-green против минут при классическом деплое

Когда это не нужно

Честно: не всем нужен zero-downtime deployment.

Если у тебя сайт-визитка или лендинг с несколькими обновлениями в год — пять минут даунтайма в 3 ночи никто не заметит. Овчинка не стоит выделки.

Но если у тебя интернет-магазин, SaaS, сервис с реальными пользователями в разных часовых поясах — это уже другой разговор. Цена простоя начинает перевешивать стоимость настройки.

В REEXY при разработке интернет-магазинов мы закладываем правильную инфраструктуру с самого начала: контейнеры, health checks, нормальный CI/CD. Это не усложняет проект — просто делает его правильно с первого дня, и потом не приходится переделывать.

Частые ошибки

Сессии в памяти приложения. При rolling deployment запросы одного пользователя могут уйти на разные поды. Если сессия хранится в памяти — пользователя разлогинит. Решение: Redis или другое внешнее хранилище.

Игнорирование readiness probe. Под запустился, но база данных ещё не подключилась, кеш не прогрет. Первые запросы падают. Readiness probe должна проверять реальную готовность, а не просто «процесс жив».

Деструктивные миграции в одном деплое с кодом. Классика жанра. Всегда разделяй: сначала мигрируй базу (обратно совместимо), потом деплой новый код.

Слишком маленький terminationGracePeriodSeconds. Kubernetes убивает под принудительно через SIGKILL, если не уложился в timeout. В-лёте обрываются запросы. Ставь с запасом.

Отсутствие мониторинга после деплоя. Деплой прошёл, CI зелёный — но rate ошибок вырос на 2%. Без алертов это заметишь через час, когда пожаловался первый пользователь.