Когда сайт падает на 5 минут во время деплоя — это не страшно, если он показывает котиков. Но если это интернет-магазин в часы пик или сервис с SLA 99.9% — каждая минута простоя стоит денег и нервов.
Zero-downtime deployment решает ровно эту проблему: обновляешь приложение, а пользователи продолжают работать как ни в чём не бывало.
Почему вообще бывает даунтайм
При классическом деплое картина такая: останавливаешь старый процесс, заливаешь новый код, запускаешь. Между остановкой и запуском — окно, когда сервис недоступен. Может быть 10 секунд, может 3 минуты — зависит от того, насколько тяжёлое приложение.
Ещё хуже, если при деплое что-то пошло не так. Старый код уже убит, новый не стартует — и ты судорожно откатываешься вручную, пока в Slack сыплются алерты.
Zero-downtime deployment — это набор стратегий, которые позволяют менять версию приложения без этого окна недоступности.
Blue-Green deployment
Самая понятная стратегия. Держишь два одинаковых окружения: синее (текущее, работает) и зелёное (новое, готовится).
Процесс такой:
- Зелёное окружение поднимаешь с новой версией приложения
- Прогоняешь smoke-тесты прямо в проде, но трафик туда ещё не идёт
- Переключаешь балансировщик: теперь весь трафик идёт в зелёное
- Синее остаётся запущенным ещё какое-то время — на случай быстрого отката
Переключение балансировщика занимает секунды. Пользователи не замечают ничего.
Откат тоже тривиален: переключаешь балансировщик обратно на синее. Буквально одна команда или клик.
Минус — нужно вдвое больше инфраструктуры в момент деплоя. Для маленьких проектов это может быть накладно, для больших — норма.
Пример с 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 выглядит так:
- Kubernetes посылает поду сигнал SIGTERM
- Приложение перестаёт принимать новые соединения
- Дожидается завершения текущих запросов
- Выходит
В 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%. Без алертов это заметишь через час, когда пожаловался первый пользователь.