Skip to main content

Які є підходи до 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

Порівняння стратегій

СтратегіяAtomicityRollback-speedЦіна ресурсівНайкраще для
Rolling-updateПоступово (N за раз)Повільно (re-deploy старого)1.0-1.2xДефолт для stateless-сервісів
Blue-greenAtomic-flipInstant (flip назад)2x під час deployHigh-confidence-release
CanaryПоступово (% traffic)Stop-ramp + drain1.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 приймати трафік.

Два типи:

  1. Liveness: «Чи process alive?» Якщо ні, restart.
  2. Readiness: «Чи process ready для трафіку?» Якщо ні, прибрати з LB-rotation.

Readiness це той, що enable zero-downtime. Під час startup, readiness має повертати false до того, як DB-connection, cache і warmup закінчені.

dockerfile
HEALTHCHECK --interval=10s --timeout=3s --start-period=30s --retries=3 \ CMD curl -f http://localhost:8080/health || exit 1

App реалізує /health, що повертає 200 лише, коли ready.

Graceful-shutdown

Коли orchestrator зупиняє container:

  1. Шле SIGTERM.
  2. Чекає до stop_grace_period (дефолт 10s).
  3. Шле SIGKILL.

Під час SIGTERM-to-SIGKILL-вікна, app має:

  1. Зупинити прийом нових connection (close listening-socket).
  2. Закінчити in-flight-запити.
  3. Cleanly shutdown DB-connection, flush-логи, exit.

Go-приклад:

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:

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

  1. Expand (deploy 1): додай нову структуру (нова колонка, нова таблиця). v1 ще працює, бо стара структура ціла.
  2. Migrate-code (deploy 2): app v2 read/write обидва старе і нове. v1 і v2 coexist під час cutover.
  3. Contract (deploy 3): як весь v1 gone, drop старе.

Три deploy для однієї логічної зміни, але кожен безпечний.

Приклади

Стратегія 1: Rolling-update (Swarm)

bash
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)

yaml
# 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=8080

Deploy v2:

bash
# Підняти 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-blue

Rollback:

bash
# Revert labels назад на blue (який ще навколо) # Або, якщо blue видалено: docker run -d --name api-blue --network=trafnet myorg/api:1.0 # Cut traffic назад

Стратегія 3: Canary (Traefik weighted-routing)

yaml
# Два сервіси з weighted-load-balancer http: services: api: weighted: services: - name: api-stable weight: 90 - name: api-canary weight: 10
bash
# 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)

yaml
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

sql
-- Крок 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.

Коротка відповідь

Для співбесіди
Premium

Коротка відповідь допоможе вам впевнено відповідати на цю тему під час співбесіди.

Коментарі

Ще немає коментарів