Які є підходи до zero-downtime deployment з Docker у production?
Zero-downtime deployment значить апгрейдити running-сервіс без дроп запитів, breaking-сесій або повернення помилок під час rollout. З Docker три deploy-стратегії покривають більшість випадків: rolling-update, blue-green і canary. Стратегія половина картинки, інша половина це healthcheck, graceful-shutdown і DB-migration-дисципліна.
Теорія
TL;DR
- Rolling-update: поступово заміняй replica. Найдешевше, повільніший cutover, partial-state під час rollout.
- Blue-green: крути два повні середовища; flip-трафік відразу. Atomic, instant-rollback, 2x ціна.
- Canary: shift малу частку спершу, ramp up, якщо healthy. Ловить subtle-регресії.
- Required-ingredient:
- Healthcheck (LB має знати, хто ready)
- Graceful-shutdown (обробка SIGTERM, finish in-flight, exit)
- DB-migration це expand-then-contract
- Connection-draining
Порівняння стратегій
| Стратегія | Atomicity | Rollback-speed | Ціна ресурсів | Найкраще для |
|---|---|---|---|---|
| Rolling-update | Поступово (N за раз) | Повільно (re-deploy старого) | 1.0-1.2x | Дефолт для stateless-сервісів |
| Blue-green | Atomic-flip | Instant (flip назад) | 2x під час deploy | High-confidence-release |
| Canary | Поступово (% traffic) | Stop-ramp + drain | 1.05-1.5x | Ризикові зміни, ловити slow-регресії |
Що йде не так без proper plumbing
- Без healthcheck: load-balancer route'ить у container, що не закінчив startup, повертає 502.
- Без graceful-shutdown: in-flight-запити дропаються, коли старий container kill'нуть.
- Breaking DB-migration: новий код очікує нову колонку; старий код краш'иться, коли колонку drop під час cutover.
- Без connection-draining: long-lived-connection (WebSocket, HTTP/2) sever'аться.
- Неправильна restart-policy: replica крашиться і ніколи не повертається.
Фіксь кожен piece перед тим, як strategy-choice має значення.
Healthcheck
Healthcheck каже orchestrator (або load-balancer), коли replica ready приймати трафік.
Два типи:
- Liveness: «Чи process alive?» Якщо ні, restart.
- Readiness: «Чи process ready для трафіку?» Якщо ні, прибрати з LB-rotation.
Readiness це той, що enable zero-downtime. Під час startup, readiness має повертати false до того, як DB-connection, cache і warmup закінчені.
HEALTHCHECK \
CMD curl -f http://localhost:8080/health || exit 1App реалізує /health, що повертає 200 лише, коли ready.
Graceful-shutdown
Коли orchestrator зупиняє container:
- Шле SIGTERM.
- Чекає до
stop_grace_period(дефолт 10s). - Шле SIGKILL.
Під час SIGTERM-to-SIGKILL-вікна, app має:
- Зупинити прийом нових connection (close listening-socket).
- Закінчити in-flight-запити.
- Cleanly shutdown DB-connection, flush-логи, exit.
Go-приклад:
sigs := make(chan os.Signal, 1)
signal.Notify(sigs, syscall.SIGTERM)
<-sigs
ctx, cancel := context.WithTimeout(context.Background(), 25 * time.Second)
defer cancel()
server.Shutdown(ctx) // чекає in-flight-запитиNode.js:
process.on('SIGTERM', async () => {
server.close() // зупинити прийом нових
await pool.end() // закрити DB-pool
process.exit(0)
})Stavь stop_grace_period: 30s, якщо твій shutdown може зайняти стільки.
DB-migration-дисципліна
Погано: breaking-migration під час deploy
Deploy app v2 + run ALTER TABLE users DROP COLUMN old_field одночасно. App v1 ще query old_field, помилки під час cutover-вікна. Весь deploy виглядає зламаним.
Добре: expand-then-contract через кілька deploy
- Expand (deploy 1): додай нову структуру (нова колонка, нова таблиця). v1 ще працює, бо стара структура ціла.
- Migrate-code (deploy 2): app v2 read/write обидва старе і нове. v1 і v2 coexist під час cutover.
- Contract (deploy 3): як весь v1 gone, drop старе.
Три deploy для однієї логічної зміни, але кожен безпечний.
Приклади
Стратегія 1: Rolling-update (Swarm)
docker service create \
--name api \
--replicas 4 \
--update-parallelism 1 \
--update-delay 30s \
--update-failure-action rollback \
--update-monitor 30s \
--update-max-failure-ratio 0.0 \
--health-cmd 'curl -f http://localhost:8080/health' \
--health-interval 10s \
--health-start-period 30s \
-p 8080:8080 \
myorg/api:1.0
# Update до v2
docker service update --image myorg/api:2.0 apiЩо відбувається:
- Swarm зупиняє 1 replica (шле SIGTERM, чекає, kill).
- Старт 1 нової replica з v2.
- Чекає, поки healthcheck pass.
- Чекає 30s monitor-period.
- Якщо healthy: повторити для next-replica.
- Якщо unhealthy: stop і rollback.
Бери --update-parallelism 2, щоб update 2 одночасно (швидше, трохи більший ризик).
Стратегія 2: Blue-green (Compose + reverse-proxy)
# compose.yaml, blue active
services:
traefik:
image: traefik:v3
command:
- --providers.docker
- --entrypoints.web.address=:80
ports: ["80:80"]
volumes: ["/var/run/docker.sock:/var/run/docker.sock:ro"]
api-blue:
image: myorg/api:1.0
labels:
- traefik.enable=true
- 'traefik.http.routers.api.rule=Host(`api.example.com`)'
- traefik.http.services.api.loadbalancer.server.port=8080Deploy v2:
# Підняти green БЕЗ трафіку
docker run -d --name api-green --network=trafnet myorg/api:2.0
# Smoke-test green напряму
docker run --rm --network=trafnet curlimages/curl curl -f http://api-green:8080/health
# Cutover: переключи labels на green
# (найпростіше через docker-compose з новим файлом або Swarm-сервіси)
# Traefik підхоплює зміну за секунди
# Drain in-flight на blue
sleep 30
# Stop blue
docker stop api-blue && docker rm api-blueRollback:
# Revert labels назад на blue (який ще навколо)
# Або, якщо blue видалено:
docker run -d --name api-blue --network=trafnet myorg/api:1.0
# Cut traffic назадСтратегія 3: Canary (Traefik weighted-routing)
# Два сервіси з weighted-load-balancer
http:
services:
api:
weighted:
services:
- name: api-stable
weight: 90
- name: api-canary
weight: 10# Deploy canary
docker run -d --name api-canary --network=trafnet \
--label "traefik.http.routers.canary.rule=Host(\`api.example.com\`)" \
myorg/api:2.0
# Дивись метрики 30 хв
# Якщо healthy, ramp до 50/50, потім 0/100
# Якщо проблеми, set canary-weight на 0 і прибратиKubernetes / Argo Rollouts / Flagger автоматизують це з metric-driven-аналізом («якщо error-rate > 1% за 5 хв, rollback»).
Connection-draining (Swarm/Compose)
services:
api:
image: myorg/api:1.0
stop_grace_period: 30s # скільки чекати між SIGTERM і SIGKILL
healthcheck:
test: ["CMD", "curl", "-f", "http://localhost:8080/health"]
interval: 10s
timeout: 3s
start_period: 30sКомбіновано з graceful-shutdown-handler у app, in-flight-запити закінчуються під час 30s-вікна.
Long-lived-connection (WebSocket, gRPC-stream)
Ці не gracefully-drain за 30 секунд, клієнти тримають їх безкінечно.
Варіанти:
- Реалізуй reconnect у клієнті. Server-side:
Connection: closeдля HTTP/1.1,GOAWAYдля HTTP/2/gRPC, server-side close для WebSocket. Client reconnect, лендиться на нову replica. - Long grace-period: set
stop_grace_period: 10m, щоб connection drain natural'но за 10 хвилин. - Sticky-pool «старих» replica, що не в rotation, але приймають existing-connection; нові connection на нові replica. Trickier orchestrate.
DB-migration у production
-- Крок 1 (deploy 1): expand
ALTER TABLE users ADD COLUMN email_canonical VARCHAR(255);
-- Старий код: ігнорує. Новий код: пише в обидва старе і нове.
-- Backfill (між deploy)
UPDATE users SET email_canonical = LOWER(email) WHERE email_canonical IS NULL;
-- Крок 2 (deploy 2): код migrate повністю на нову колонку
-- App читає з email_canonical, пише в обидва для безпеки
-- Крок 3 (deploy 3): contract
ALTER TABLE users DROP COLUMN email;Три release. Кожен безпечний deploy. Кожен безпечний rollback.
Комбінування стратегій
Реальні команди мікс'ять:
- Rolling-update для більшості release (дешево, просто).
- Blue-green для high-confidence (atomic, легкий rollback).
- Canary для ризикових (ловити slow-регресії перед тим, як всі юзери побачать).
Реальне застосування
- Default microservice-deploy: rolling-update з N=2-4 replica, 1-at-a-time, healthcheck-gated.
- Quarterly-major-release: blue-green для clean-rollback-history.
- Ризикова feature: canary на 5% протягом 24 годин, ramp, якщо метрики OK.
- Public-facing API з WebSocket: long-grace-period + client reconnect-logic + rolling-update.
- Database-heavy-сервіси: expand-then-contract-migration завжди.
Типові помилки
Без healthcheck або неправильний healthcheck
Healthcheck, що лише hits TCP-port, недостатньо, app може слухати, але не ready. Реалізуй /health, що верифікує DB, downstream-сервіси і config.
App ігнорує SIGTERM
Багато framework'ів потребують explicit signal-handler. Default Node.js process exit миттєво на SIGTERM. Додай handler.
Sticky-session ламаються через deploy
Якщо session живе in-memory, прив'язана до однієї replica, redeploy log'aut'ує юзерів. Externalize-сесії (Redis, JWT).
Без rollback-plan
«Просто push старий image» звучить просто, поки schema-migration частково applied. Май rehearsed-rollback перед deploy.
Плутаниця deploy-стратегії з downtime
Rolling-update без graceful-shutdown ще має downtime per replica. Strategy + plumbing разом = zero-downtime.
Питання для поглиблення
Q: Що таке stop_grace_period?
A: Час між SIGTERM і SIGKILL під час зупинки container. Stavь високо для graceful-shutdown, щоб закінчити (дефолт 10s; для HTTP-сервісів зі slow-запитами, 30-60s).
Q: Чи healthcheck має бути public?
A: Ні, orchestrator і LB hit його internally. Фактично, public-health-endpoint може leak корисну інфу attacker'у. Bind на localhost або private-interface, або вимагай auth-token.
Q: Як знаю, чи мій deploy був zero-downtime?
A: Запусти synthetic-load-test (k6, ab, vegeta) під час deploy. Дивись error-rate. Якщо 0% під час rollout, був zero-downtime.
Q: (Senior) Як обробляти deploy, що потребує long-running-migration?
A: Decouple migration від deploy. Run migration-job як one-shot container перед deploy нової app-version. App-version, що потребує новий schema, deploy лише після того, як migration completes. Tool типу Flyway, Liquibase, golang-migrate дозволяють script'нути це. Combine з feature-flag, щоб нові code-path лишалися dark, поки migration верифікований.
Q: (Senior) Як observability змінюється з цими стратегіями?
A: Треба ідентифікувати, яка версія обслуговує дане request. Додай image-tag/digest як metric-label і log-field. Під час canary можна порівняти метрики між stable і canary-cohort (error-rate, latency, saturation) і тригерити автоматичний rollback, якщо canary diverges. Це core-ідея за progressive-delivery-tool (Flagger, Argo Rollouts): strategy автоматизована metric-SLO, не людьми, що дивляться на dashboard.
Коротка відповідь
Для співбесідиКоротка відповідь допоможе вам впевнено відповідати на цю тему під час співбесіди.
Коментарі
Ще немає коментарів