Запропонувати правкуПокращити цю статтюДопрацюйте відповідь до «Які є підходи до zero-downtime deployment з Docker у production?». Ваші зміни проходять модерацію перед публікацією.Потрібне підтвердженняКонтентЩо ви змінюєте🇺🇸EN🇺🇦UAПереглядЗаголовок (UA)Коротка відповідь (UA)Три родини: 1. **Rolling update** — заміняй task один (або N) за раз. Дефолт у Swarm/K8s. Дешево. 2. **Blue-green** — крути два повні середовища; flip-трафік атомарно. Швидкий rollback, 2x ресурсів. 3. **Canary** — route малий % на нову версію; ramp-up, якщо метрики хороші. Ловить slow-burn-issue. **Cross-cutting-вимоги:** - **Healthcheck** — load-balancer має знати, який container ready. - **Graceful-shutdown** — обробка SIGTERM, drain in-flight-запитів, потім exit. - **Backward-compatible DB-migration** — expand-then-contract; ніколи breaking-зміни у deploy-вікні. - **Connection-draining** — дай клієнтам час закінчити або reconnect. ```bash # Swarm rolling-update docker service update \ --image myorg/api:2.0 \ --update-parallelism 1 \ --update-delay 30s \ --update-failure-action rollback \ api ```Показується над повною відповіддю для швидкого нагадування.Відповідь (UA)Зображення**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 приймати трафік. **Два типи:** 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.Для рев’юераПримітка для модератора (необов’язково)Бачить лише модератор. Прискорює рев’ю.