Деплой без простоя: техники и пошаговая настройка
Практика4 минРедакция Innovialabs
Коротко
Новые экземпляры должны запуститься и пройти проверку готовности до остановки старых, старые — завершить текущие запросы перед выходом, а схема базы — работать с обеими версиями одновременно.
Что на самом деле нужно для деплоя без простоя
Релиз без простоя — это не один приём, а четыре правила, которые работают вместе:
- Поэтапная замена: новые экземпляры запускаются раньше, чем удаляются старые, поэтому мощность никогда не падает до нуля.
- Health checks: трафик идёт только на экземпляры, которые сообщают о готовности.
- Graceful shutdown и слив соединений: старый экземпляр перестаёт принимать новые запросы, дорабатывает текущие и только потом завершается.
- Обратно совместимые миграции: во время выкатки старый и новый код работают параллельно с одной базой, поэтому схема должна подходить обоим.
Если нет хотя бы одного пункта, пользователи будут видеть ошибки при каждом деплое — даже на идеальной инфраструктуре.
Rolling restart
При rolling update оркестратор (Kubernetes, Docker Swarm, Nomad или скрипт за балансировщиком) заменяет экземпляры небольшими партиями:
- Запускает один новый экземпляр.
- Ждёт, пока он пройдёт readiness-проверку.
- Добавляет его в балансировщик.
- Убирает один старый экземпляр из балансировщика, даёт ему слить соединения и останавливает.
- Повторяет, пока все экземпляры не перейдут на новую версию.
Ключевые настройки: сколько лишних экземпляров допустимо во время обновления и сколько может быть недоступно. Для нулевого простоя — ноль недоступных и минимум один лишний.
Альтернативы с той же целью: blue-green (два полных окружения, трафик переключается разом) и canary (сначала на новую версию идёт небольшая доля трафика). Обе тоже опираются на health checks и слив соединений.
Health checks: liveness и readiness
Эти проверки нельзя смешивать:
- Readiness отвечает на вопрос «можно ли мне сейчас получать трафик?». Она должна падать во время старта, прогрева кэшей и при остановке.
- Liveness отвечает на вопрос «я завис и меня нужно перезапустить?». Держите её простой и не проверяйте в ней базу, иначе сбой базы перезапустит все экземпляры разом.
Readiness-эндпоинт, который всегда возвращает 200, — самая частая причина, почему «бесшовный» деплой всё равно теряет запросы.
Graceful shutdown и слив соединений
Останавливая контейнер, оркестратор отправляет SIGTERM, ждёт grace period и затем отправляет SIGKILL. Приложение должно использовать это окно. Минимальный пример на Node.js:
const express = require('express');
const app = express();
let shuttingDown = false;
app.get('/healthz/ready', (req, res) => {
res.status(shuttingDown ? 503 : 200).end();
});
const server = app.listen(3000);
process.on('SIGTERM', () => {
shuttingDown = true;
setTimeout(() => {
server.close(() => process.exit(0));
}, 5000);
});
Важен порядок: сначала пометить экземпляр как неготовый, подождать несколько секунд, чтобы балансировщик перестал слать на него трафик, затем закрыть сервер — открытые запросы при этом успеют завершиться. Фоновые воркеры должны перестать брать новые задачи и доделать или вернуть текущую.
Рабочий пример для Kubernetes
apiVersion: apps/v1
kind: Deployment
metadata:
name: api
spec:
replicas: 3
selector:
matchLabels:
app: api
strategy:
type: RollingUpdate
rollingUpdate:
maxUnavailable: 0
maxSurge: 1
template:
metadata:
labels:
app: api
spec:
terminationGracePeriodSeconds: 30
containers:
- name: api
image: registry.example.com/api:1.4.2
ports:
- containerPort: 3000
readinessProbe:
httpGet:
path: /healthz/ready
port: 3000
periodSeconds: 5
failureThreshold: 2
maxUnavailable: 0 сохраняет полную мощность, maxSurge: 1 добавляет по одному поду, а grace period должен быть больше задержки слива плюс самого долгого запроса.
Обратно совместимые миграции базы данных
Используйте паттерн expand and contract. Пример: переименование name в full_name.
| Релиз | База данных | Код |
|---|---|---|
| 1. Expand | Добавить full_name, nullable | Писать в обе колонки, читать name |
| 2. Backfill | Скопировать данные пачками | Без изменений |
| 3. Switch | Без изменений | Читать и писать только full_name |
| 4. Contract | Удалить name | Без изменений |
Отсюда правила:
- Никогда не переименовывайте и не удаляйте колонку в том же релизе, где код перестаёт её использовать.
- Новые колонки добавляйте nullable или со значением по умолчанию.
- Тяжёлое копирование данных выполняйте пачками, а не одним блокирующим запросом.
- Запускайте миграции отдельным шагом до выкатки, а не при старте каждого экземпляра.
Частые ошибки
- Readiness-проверка не учитывает состояние остановки.
- Grace period короче самого долгого запроса или задачи.
- Разрушающая миграция выкатывается вместе с кодом.
- Сессии хранятся в памяти экземпляра, и пользователей разлогинивает при его остановке.
- Не проверен откат: предыдущая версия тоже должна работать с новой схемой.