О нас Руководства Проекты Контакты
Админка
пожалуйста подождите

Постановка задачи

Вам необходимо разворачивать новые версии приложения без прерывания обслуживания пользователей, безопасно выполнять миграции базы данных и иметь возможность быстро откатиться при возникновении проблем.

Сравнение стратегий деплоя

СтратегияПростойСкорость откатаСтоимость ресурсовСложность
Rolling UpdateНулевойБыстроНизкаяНизкая
Blue-GreenНулевойМгновенно2xСредняя
CanaryНулевойБыстроНизкая–средняяВысокая
A/B TestingНулевойБыстроНизкая–средняяВысокая

Стратегия 1: Rolling Update (по умолчанию в Kubernetes)

Как это работает

Kubernetes постепенно заменяет старые pod’ы новыми, обеспечивая поддержание минимальной доступности на всём протяжении процесса.

Time 0: [v1] [v1] [v1] [v1] ← All running v1
Time 1: [v1] [v1] [v1] [v2] ← One v2 starting
Time 2: [v1] [v1] [v2] [v2] ← Two v2 ready
Time 3: [v1] [v2] [v2] [v2] ← Three v2 ready
Time 4: [v2] [v2] [v2] [v2] ← All running v2

Конфигурация

apiVersion: apps/v1
kind: Deployment
metadata:
name: myapp
spec:
replicas: 4
strategy:
type: RollingUpdate
rollingUpdate:
maxUnavailable: 1 # Не более 1 pod может быть недоступен
maxSurge: 1 # Не более 1 дополнительного pod во время rollout
template:
spec:
containers:
- name: app
image: myapp:v2
readinessProbe:
httpGet:
path: /ready
port: 8080
initialDelaySeconds: 5
periodSeconds: 5
failureThreshold: 3
livenessProbe:
httpGet:
path: /health
port: 8080
initialDelaySeconds: 30
periodSeconds: 10

Корректное завершение работы

Убедитесь, что ваше приложение корректно обрабатывает SIGTERM:

// Пример для Node.js
const server = app.listen(8080);
process.on('SIGTERM', () => {
console.log('SIGTERM received, shutting down gracefully');
server.close(() => {
console.log('HTTP server closed');
// Закрыть подключения к базе данных
db.close();
process.exit(0);
});
// Принудительное закрытие через 30 секунд
setTimeout(() => {
console.error('Forced shutdown');
process.exit(1);
}, 30000);
});
# Конфигурация Kubernetes
spec:
terminationGracePeriodSeconds: 30
containers:
- name: app
lifecycle:
preStop:
exec:
# Дать load balancer время исключить pod из ротации
command: ["/bin/sh", "-c", "sleep 10"]

Стратегия 2: Blue-Green Deployment

Как это работает

Запускаются два идентичных окружения. Весь трафик направляется в одно (blue), деплой выполняется во второе (green), затем выполняется переключение.

 ┌──────────────────┐
│ Load Balancer │
└────────┬─────────┘
│
┌───────────┼───────────┐
│ │ │
┌──────▼──────┐ │ ┌──────▼──────┐
│ Blue │ │ │ Green │
│ (v1.0) │◀───┘ │ (v1.1) │
│ ACTIVE │ │ STANDBY │
└─────────────┘ └─────────────┘

Реализация в Kubernetes

# blue-deployment.yaml
apiVersion: apps/v1
kind: Deployment
metadata:
name: myapp-blue
labels:
app: myapp
version: blue
spec:
replicas: 3
selector:
matchLabels:
app: myapp
version: blue
template:
metadata:
labels:
app: myapp
version: blue
spec:
containers:
- name: app
image: myapp:v1.0
---
# green-deployment.yaml
apiVersion: apps/v1
kind: Deployment
metadata:
name: myapp-green
labels:
app: myapp
version: green
spec:
replicas: 3
selector:
matchLabels:
app: myapp
version: green
template:
metadata:
labels:
app: myapp
version: green
spec:
containers:
- name: app
image: myapp:v1.1
---
# service.yaml — переключение через изменение selector
apiVersion: v1
kind: Service
metadata:
name: myapp
spec:
selector:
app: myapp
version: blue # Измените на "green" для переключения
ports:
- port: 80
targetPort: 8080

Скрипт переключения

#!/bin/bash
CURRENT=$(kubectl get service myapp -o jsonpath='{.spec.selector.version}')
if [ "$CURRENT" = "blue" ]; then
NEW="green"
else
NEW="blue"
fi
echo "Switching from $CURRENT to $NEW"
kubectl patch service myapp -p "{\"spec\":{\"selector\":{\"version\":\"$NEW\"}}}"
echo "Traffic now routing to $NEW"

Стратегия 3: Canary Deployment

Как это работает

Сначала новая версия разворачивается для небольшой доли пользователей, затем доля постепенно увеличивается.

Phase 1: [v1][v1][v1][v1][v1][v1][v1][v1][v1][v2] ← 10% canary
Phase 2: [v1][v1][v1][v1][v1][v2][v2][v2][v2][v2] ← 50% canary
Phase 3: [v2][v2][v2][v2][v2][v2][v2][v2][v2][v2] ← 100% promoted

Использование Nginx Ingress

# Основной деплой (stable)
apiVersion: apps/v1
kind: Deployment
metadata:
name: myapp-stable
spec:
replicas: 9
template:
spec:
containers:
- name: app
image: myapp:v1.0
---
# Canary-деплой
apiVersion: apps/v1
kind: Deployment
metadata:
name: myapp-canary
spec:
replicas: 1
template:
spec:
containers:
- name: app
image: myapp:v1.1
---
# Stable Ingress
apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
name: myapp-stable
spec:
rules:
- host: app.example.com
http:
paths:
- path: /
pathType: Prefix
backend:
service:
name: myapp-stable
port:
number: 80
---
# Canary Ingress с весом
apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
name: myapp-canary
annotations:
nginx.ingress.kubernetes.io/canary: "true"
nginx.ingress.kubernetes.io/canary-weight: "10"
spec:
rules:
- host: app.example.com
http:
paths:
- path: /
pathType: Prefix
backend:
service:
name: myapp-canary
port:
number: 80

Canary с Istio

apiVersion: networking.istio.io/v1beta1
kind: VirtualService
metadata:
name: myapp
spec:
hosts:
- myapp
http:
- match:
- headers:
x-canary:
exact: "true"
route:
- destination:
host: myapp
subset: canary
- route:
- destination:
host: myapp
subset: stable
weight: 90
- destination:
host: myapp
subset: canary
weight: 10
---
apiVersion: networking.istio.io/v1beta1
kind: DestinationRule
metadata:
name: myapp
spec:
host: myapp
subsets:
- name: stable
labels:
version: stable
- name: canary
labels:
version: canary

Безопасные миграции базы данных

Принципы

  1. Backward Compatible: старый код должен работать с новой схемой
  2. Forward Compatible: новый код должен работать со старой схемой во время rollout
  3. Небольшие, инкрементальные изменения: никаких миграций «одним махом»

Паттерн: миграция Expand-Contract

Фаза 1: Expand (добавить новый столбец)

-- Миграция 1: добавить новый столбец, сохранить старый
ALTER TABLE users ADD COLUMN full_name VARCHAR(255);
// Код v2: писать в оба, читать из старого
const user = await db.query('SELECT first_name, last_name, full_name FROM users');
user.displayName = user.full_name || `${user.first_name} ${user.last_name}`;
// При сохранении писать в оба
await db.query(
'UPDATE users SET first_name = $1, last_name = $2, full_name = $3',
[firstName, lastName, fullName]
);

Фаза 2: Миграция данных

-- Миграция 2: backfill данных
UPDATE users SET full_name = CONCAT(first_name, ' ', last_name)
WHERE full_name IS NULL;

Фаза 3: Contract (удалить старые столбцы)

// Код v3: читать/писать только новый столбец
const user = await db.query('SELECT full_name FROM users');
-- Миграция 3: удалить старые столбцы (после перевода всех pod’ов на v3)
ALTER TABLE users DROP COLUMN first_name;
ALTER TABLE users DROP COLUMN last_name;

Job для миграции в Kubernetes

apiVersion: batch/v1
kind: Job
metadata:
name: db-migration-v1-2
spec:
backoffLimit: 0
template:
spec:
restartPolicy: Never
initContainers:
- name: wait-for-db
image: busybox
command: ['sh', '-c', 'until nc -z postgres 5432; do sleep 1; done']
containers:
- name: migrate
image: myapp:v1.2
command: ["npm", "run", "db:migrate"]
env:
- name: DATABASE_URL
valueFrom:
secretKeyRef:
name: db-credentials
key: url

Процедуры отката

Откат в Kubernetes

# Просмотреть историю deployment
kubectl rollout history deployment/myapp
# Откат к предыдущей версии
kubectl rollout undo deployment/myapp
# Откат к конкретной ревизии
kubectl rollout undo deployment/myapp --to-revision=2
# Проверить статус отката
kubectl rollout status deployment/myapp

Мгновенный откат Blue-Green

# Просто переключите selector сервиса обратно
kubectl patch service myapp -p '{"spec":{"selector":{"version":"blue"}}}'

Откат базы данных

-- Всегда держите готовую rollback-миграцию
-- down-migration.sql
ALTER TABLE users ADD COLUMN first_name VARCHAR(255);
ALTER TABLE users ADD COLUMN last_name VARCHAR(255);
UPDATE users SET
first_name = SPLIT_PART(full_name, ' ', 1),
last_name = SPLIT_PART(full_name, ' ', 2);

Лучшие практики health checks

Реализуйте три endpoint’а

// /health — Liveness: жив ли процесс?
app.get('/health', (req, res) => {
res.status(200).json({ status: 'ok' });
});
// /ready — Readiness: может ли сервис обрабатывать запросы?
app.get('/ready', async (req, res) => {
try {
await db.query('SELECT 1');
await redis.ping();
res.status(200).json({ status: 'ready' });
} catch (error) {
res.status(503).json({ status: 'not ready', error: error.message });
}
});
// /startup — Startup: завершилась ли инициализация сервиса?
let isStarted = false;
app.get('/startup', (req, res) => {
if (isStarted) {
res.status(200).json({ status: 'started' });
} else {
res.status(503).json({ status: 'starting' });
}
});

Конфигурация probes в Kubernetes

spec:
containers:
- name: app
startupProbe:
httpGet:
path: /startup
port: 8080
failureThreshold: 30
periodSeconds: 10
livenessProbe:
httpGet:
path: /health
port: 8080
initialDelaySeconds: 0
periodSeconds: 10
failureThreshold: 3
readinessProbe:
httpGet:
path: /ready
port: 8080
initialDelaySeconds: 0
periodSeconds: 5
failureThreshold: 3

Подход Senior: дело в состоянии, а не в коде

«Maintenance Mode» неприемлем для систем уровня senior. Реальная сложность — не в замене кода, а в управлении состоянием.

Почему миграции базы данных — самая сложная часть

Правило: изменения базы данных должны быть backward compatible при одновременной работе старого и нового кода.

Сценарий: переименование столбца

  • Наивно: ALTER TABLE users RENAME COLUMN name TO full_name;
  • Результат: старый код (который всё ещё работает во время деплоя) делает запросы к name. В DB теперь full_name. Падение.

Senior-паттерн: Expand and Contract 1. Деплой 1 (Expand): добавить столбец fullname. Код пишет в оба name и fullname. Читает из name. 2. Backfill: запустить скрипт, который копирует name в fullname для старых строк. 3. Деплой 2 (Switch): код читает из fullname. Пишет в оба. 4. Деплой 3 (Contract): код пишет только в full_name. Удалить столбец name.

Этот паттерн требует 3 деплоя вместо 1, но гарантирует ноль потерянных запросов.

Выбор правильной стратегии

СценарийРекомендуемая стратегия
Стандартное обновление приложения, важна стоимостьRolling Update
Критический контур, нужен мгновенный откатBlue-Green
Высокорисковое изменение, нужен постепенный rolloutCanary
A/B-эксперимент с сегментацией пользователейCanary с маршрутизацией по заголовкам

Pro Tips:

  • Rolling Update: обязательно должен быть readinessProbe. Иначе K8s начнёт отправлять трафик в новый pod до загрузки приложения, что приведёт к 502.
  • Blue-Green: стоимость вдвое выше (нужно 2x ресурсов), зато мгновенное переключение и мгновенный откат.
  • Canary: самый безопасный вариант — выкатить на 5% пользователей, мониторить метрики, затем увеличить до 20%, 50%, 100%.

Istio VirtualService для Canary

При использовании Istio можно выполнять weighted routing:

# Istio VirtualService
route:
- destination:
host: my-service
subset: v1
weight: 90
- destination:
host: my-service
subset: v2
weight: 10

Чек-лист деплоя без простоя

  • [ ] Приложение корректно обрабатывает SIGTERM
  • [ ] PreStop hook даёт время на обновление load balancer
  • [ ] Readiness probe проверяет все зависимости
  • [ ] Стратегия rolling update настроена с подходящими значениями
  • [ ] Миграции базы данных являются backward compatible
  • [ ] Для изменений схемы используется паттерн Expand-Contract
  • [ ] Feature flags для новой функциональности
  • [ ] Алерты мониторинга на здоровье деплоя
  • [ ] Runbook для процедур отката
  • [ ] Перед крупными релизами выполнено нагрузочное тестирование
  • [ ] Деплой в окна низкого трафика (если применимо)

Итог

Деплой без простоя в основном зависит от совместимости базы данных. Если изменения схемы ломают предыдущую версию кода, никакая магия Kubernetes не спасёт. Всегда сначала расширяйте (expand), затем сокращайте (contract).

Связанные статьи Wiki

 
 
 
Языки
Темы
Copyright © 1999 — 2026
Зетка Интерактив