Loading...
Усі статті
SRE · 7 min read

Деплої без простою: стратегії Blue-Green проти Canary

Практичне порівняння blue-green та canary rollouts на Kubernetes з Argo Rollouts, автоматизованим аналізом і патернами міграцій БД, які роблять будь-яку з цих стратегій дійсно безпечною.

Чому "Rolling Update" недостатньо

Дефолтна стратегія деплою у Kubernetes — це RollingUpdate, і більшість команд на цьому зупиняються. Це краще, ніж повний простій, але це все ще поганий дефолт для продакшну. У нього немає автоматизованого health-check понад readiness probe, немає traffic-shaping, немає автоматизованого відкату і немає способу валідувати нову версію під реальним навантаженням до того, як перевести на неї всіх користувачів. Якщо новий под зламаний так, що це проявляється лише при 10% трафіку, ви дізнаєтесь це при 100% трафіку.

Реальні zero-downtime деплої вимагають двох речей, що працюють разом: стратегії трафіку (blue-green або canary) і стратегії здоров'я (автоматизованого аналізу, що може перервати rollout). Ця стаття — про те, як правильно побудувати обидві з Argo Rollouts, Kubernetes та service mesh чи ingress, який ви, ймовірно, вже запускаєте.

Blue-Green: перемикач

Blue-green запускає два повні середовища поряд. Blue обслуговує продакшн-трафік, а Green отримує нову версію. Ви деплоїте, робите smoke test проти внутрішнього service name, потім перемикаєте один label або router rule — і Green стає продакшном. Старе Blue тримається теплим для миттєвого rollback.

Коли Blue-Green виграє

  • Потрібні stateful тести перед go-live. Ви можете запускати повні smoke/e2e набори проти Green, використовуючи реальну інфраструктуру.
  • Миттєвий rollback — жорстка вимога. Перемикання label швидше за будь-який canary rewind.
  • Вартість невеликого відсотка поганого rollout неприйнятна. Payment flows, auth, критичні внутрішні API.

Коли це боляче

  • Ви не можете дозволити собі 2x capacity на час перемикання. Blue-green подвоює кількість подів під час переходу.
  • У вас є міграції БД, що не є зворотно сумісними. І Blue, і Green розділяють ту саму БД. Якщо Green потребує зміни схеми, Blue може зламатися.
  • Ваш сервіс великий. Провіження повного Green може займати хвилини scheduling time.

Blue-Green з Argo Rollouts

apiVersion: argoproj.io/v1alpha1
kind: Rollout
metadata:
  name: payments
spec:
  replicas: 10
  strategy:
    blueGreen:
      activeService: payments-active
      previewService: payments-preview
      autoPromotionEnabled: false
      scaleDownDelaySeconds: 600
      prePromotionAnalysis:
        templates:
          - templateName: smoke-tests
        args:
          - name: service-name
            value: payments-preview
      postPromotionAnalysis:
        templates:
          - templateName: error-rate
  selector:
    matchLabels:
      app: payments
  template:
    metadata:
      labels:
        app: payments
    spec:
      containers:
        - name: api
          image: ghcr.io/example/payments:1.14.0
          ports:
            - containerPort: 8080

Дві важливі властивості тут. autoPromotionEnabled: false вимагає ручного promotion (або успішного AnalysisRun) перед тим, як відбудеться перемикання. scaleDownDelaySeconds: 600 тримає старий ReplicaSet живим 10 хвилин після promotion — це ваше вікно для rollback.

Canary: градієнт

Canary-релізи надсилають невеликий зріз трафіку на нову версію, спостерігають за метриками і поступово розширюють. Зроблено правильно — це найбезпечніша стратегія у більшості продакшн-середовищ. Зроблено неправильно (без метрик, без автоматизованого abort, з ручними стрибками ваг) — це гірше, ніж rolling update, бо інженер, що няньчить це, просто клікає "promote" на кожному кроці.

Коли Canary виграє

  • High-traffic сервіси. 10% від мільйона запитів на хвилину — це багато сигналу, щоб виявити регрес.
  • Чутливість до вартості. Ви запускаєте 110% capacity під час rollout, а не 200%.
  • Поступова експозиція ризику. Чудово для user-facing змін, де "виглядає нормально внутрішньо" недостатньо.

Коли це боляче

  • Low-traffic сервіси. 10% від 50 rpm — це 5 rpm. Цього недостатньо сигналу, щоб щось виявити за п'ять хвилин. Для них нормально blue-green або звичайний rolling update.
  • Метрики, які важко ізолювати. Якщо ви не можете обчислити error rate per-version, canary-аналіз — це обман.

Canary з Argo Rollouts та Istio

apiVersion: argoproj.io/v1alpha1
kind: Rollout
metadata:
  name: api
spec:
  replicas: 8
  strategy:
    canary:
      canaryService: api-canary
      stableService: api-stable
      trafficRouting:
        istio:
          virtualService:
            name: api
            routes: [primary]
      steps:
        - setWeight: 5
        - pause: { duration: 2m }
        - analysis:
            templates:
              - templateName: success-rate
              - templateName: latency-p99
            args:
              - name: service-name
                value: api-canary
        - setWeight: 25
        - pause: { duration: 5m }
        - analysis:
            templates:
              - templateName: success-rate
              - templateName: latency-p99
            args:
              - name: service-name
                value: api-canary
        - setWeight: 50
        - pause: { duration: 5m }
        - setWeight: 100

Зверніть увагу, як паузи короткі на початку і довші пізніше. Перші хвилини canary — найінформативніші: якщо нова версія збирається впасти чи кидати 500-ки, ви хочете побачити це негайно.

Автоматизований аналіз: частина, що справді має значення

Жодна стратегія не є безпечною без автоматизованого, metric-driven abort. Argo Rollouts використовує AnalysisTemplate, щоб робити запити до Prometheus (або Datadog, New Relic, CloudWatch тощо) і провалити rollout, якщо сигнал поганий.

apiVersion: argoproj.io/v1alpha1
kind: AnalysisTemplate
metadata:
  name: success-rate
spec:
  args:
    - name: service-name
  metrics:
    - name: success-rate
      interval: 30s
      count: 6
      successCondition: result[0] >= 0.99
      failureLimit: 2
      provider:
        prometheus:
          address: http://prometheus.monitoring:9090
          query: |
            sum(rate(http_requests_total{service="{{args.service-name}}",code!~"5.."}[1m]))
            /
            sum(rate(http_requests_total{service="{{args.service-name}}"}[1m]))

І шаблон latency за тим самим патерном:

apiVersion: argoproj.io/v1alpha1
kind: AnalysisTemplate
metadata:
  name: latency-p99
spec:
  args:
    - name: service-name
  metrics:
    - name: latency-p99
      interval: 30s
      count: 6
      successCondition: result[0] < 0.4
      failureLimit: 2
      provider:
        prometheus:
          address: http://prometheus.monitoring:9090
          query: |
            histogram_quantile(
              0.99,
              sum by (le) (
                rate(http_request_duration_seconds_bucket{service="{{args.service-name}}"}[1m])
              )
            )

Якщо будь-який з них провалиться, Rollout перериває роботу і відновлює стабільний трафік без людини у циклі. У цьому весь сенс.

Міграції БД: тихий убивця

Найпоширеніша причина того, що "zero-downtime" перетворюється на "простій" — це міграція БД, що припустила, що старий код уже пішов. І blue-green, і canary обслуговують старий та новий код застосунку одночасно. Ваша схема має працювати для обох одночасно.

Патерн Expand-and-Contract

Ніколи не відправляйте ламаючу міграцію в одному релізі. Розділіть її на:

  1. Expand. Додайте новий стовпець/таблицю/індекс. Старий код його ігнорує. Новий код пише і в старий, і в новий.
  2. Backfill. Копіюйте дані зі старого в новий. Запускається як фоновий job.
  3. Migrate reads. Новий код читає з нового стовпця. Старий код усе ще читає старий.
  4. Contract. Видаліть старий стовпець у наступному релізі, коли жоден живий под його не використовує.

Так, це чотири релізи замість одного. Це також різниця між плановою зміною і інцидентом о 2 ночі.

-- Release N: expand
ALTER TABLE users ADD COLUMN email_normalized TEXT;
CREATE INDEX CONCURRENTLY idx_users_email_norm ON users(email_normalized);

-- Release N (app code): dual-write
INSERT INTO users (email, email_normalized) VALUES ($1, LOWER($1));

-- Release N+1: backfill
UPDATE users SET email_normalized = LOWER(email) WHERE email_normalized IS NULL;

-- Release N+2: read from new column
SELECT id FROM users WHERE email_normalized = $1;

-- Release N+3: contract
ALTER TABLE users DROP COLUMN email;

Connection draining та graceful shutdown

Навіть з ідеальним traffic shifting, под, що помирає посеред запиту, губить цей запит. Kubernetes надсилає SIGTERM, чекає terminationGracePeriodSeconds, потім надсилає SIGKILL. Вашому застосунку потрібно:

  1. Негайно провалити readiness probe (щоб його видалили з endpoints).
  2. Продовжувати обслуговувати in-flight запити.
  3. Закрити HTTP listener і злити з'єднання.
  4. Завершитись чисто до закінчення grace period.

Мінімальний Node.js shutdown handler:

import http from "node:http";
import express from "express";

const app = express();
let ready = true;
app.get("/ready", (_, res) => (ready ? res.sendStatus(200) : res.sendStatus(503)));
app.get("/live", (_, res) => res.sendStatus(200));

const server = http.createServer(app);
server.listen(8080);

const shutdown = async () => {
  console.log("SIGTERM received, draining");
  ready = false;

  // Give load balancers time to observe unreadiness
  await new Promise((r) => setTimeout(r, 5000));

  server.close((err) => {
    if (err) {
      console.error("Shutdown error", err);
      process.exit(1);
    }
    process.exit(0);
  });

  // Hard timeout
  setTimeout(() => process.exit(1), 25000).unref();
};

process.on("SIGTERM", shutdown);
process.on("SIGINT", shutdown);

І відповідний pod spec:

spec:
  terminationGracePeriodSeconds: 30
  containers:
    - name: api
      lifecycle:
        preStop:
          exec:
            command: ["sleep", "5"]
      readinessProbe:
        httpGet: { path: /ready, port: 8080 }
        periodSeconds: 2
        failureThreshold: 2

preStop hook дає service mesh мить, щоб поширити видалення до того, як процес почне завершуватись. Дрібниця, величезна різниця у продакшні.

Вибір між цими двома

КритерійBlue-GreenCanary
Модель трафікуAll-or-nothingГрадуальний відсоток
Швидкість rollbackМиттєво (label flip)Секунди (traffic reset)
Додаткова capacity2x під час перемикання1.1x під час rollout
Валідація реальними користувачамиЛише після перемиканняПід час rollout
Мінімум корисного трафікуБудь-який~100 rpm+
Толерантність до міграцій БДВажче (обидва env розділяють БД)Так само
СкладністьНижчаВища (потребує метрик)

На практиці ми рекомендуємо canary за замовчуванням для будь-якого сервісу з достатнім трафіком, щоб аналіз був змістовним, і blue-green для low-traffic критичних сервісів (auth, payments, webhooks), де повні pre-promotion smoke-тести цінніші за поступову експозицію.

Наступні кроки

Відвантаження без страху вимагає трьох речей: стратегії трафіку, автоматизованого metric-driven abort і дисципліни БД, що припускає одночасне виконання двох версій коду. YAML — це легко. Дисципліна — складна частина. Почніть з прийняття expand-and-contract на вашій наступній зміні схеми, додайте AnalysisTemplate, що запитує ваш існуючий Prometheus, і запустіть ваш перший canary на некритичному сервісі. Якщо хочете допомоги з підняттям платформи progressive delivery на кількох сервісах, напишіть нам.

у категорії
deploymentsrekuberneteskubernetesargocdargocd
працювати з нами

Хочете, щоб наша команда допомогла з вашою інфраструктурою?

talk to an engineerFree 30-min discovery callBook
close