Loading...
Alle Artikel
SRE · 8 min read

Zero-Downtime-Deployments: Blue-Green vs. Canary-Strategien

Ein praxisnaher Vergleich von Blue-Green- und Canary-Rollouts auf Kubernetes mit Argo Rollouts, automatisierter Analyse und den Datenbank-Migrationsmustern, die beide Strategien erst wirklich sicher machen.

Warum „Rolling Update" nicht genügt

Die Standard-Deployment-Strategie in Kubernetes ist RollingUpdate, und die meisten Teams hören dort auf. Sie ist besser als ein vollständiger Ausfall, aber als Produktions-Default trotzdem schlecht. Sie hat keinen automatisierten Health Check jenseits der Readiness Probe, kein Traffic Shaping, kein automatisches Rollback und keine Möglichkeit, die neue Version unter realer Last zu validieren, bevor alle Nutzer darauf umgelenkt werden. Wenn der neue Pod so kaputt ist, dass es sich erst bei 10 % Traffic zeigt, merken Sie es bei 100 %.

Echte Zero-Downtime-Deployments brauchen zwei Dinge, die zusammenspielen: eine Traffic-Strategie (Blue-Green oder Canary) und eine Health-Strategie (automatisierte Analyse, die einen Rollout abbrechen kann). Dieser Beitrag zeigt, wie Sie beides sauber mit Argo Rollouts, Kubernetes und einem Service Mesh oder Ingress aufbauen, das Sie vermutlich bereits betreiben.

Blue-Green: Der Umschalter

Blue-Green betreibt zwei vollständige Umgebungen nebeneinander. Blue bedient den Produktionsverkehr, während Green die neue Version erhält. Sie deployen, rauchen gegen einen internen Service-Namen, dann flippen Sie ein einzelnes Label oder eine Router-Regel, und Green wird Produktion. Das alte Blue bleibt warm für sofortiges Rollback.

Wann Blue-Green gewinnt

  • Stateful-Tests werden vor dem Go-Live benötigt. Sie können vollständige Smoke-/E2E-Suites gegen Green auf echter Infrastruktur laufen lassen.
  • Sofortiges Rollback ist harte Anforderung. Ein Label-Flip ist schneller als jeder Canary-Rewind.
  • Die Kosten eines kleinen Bad-Rollout-Anteils sind inakzeptabel. Payment-Flows, Auth, kritische interne APIs.

Wann es wehtut

  • Sie können sich 2x Kapazität für die Dauer des Umschaltens nicht leisten. Blue-Green verdoppelt Ihre Pod-Anzahl während der Transition.
  • Sie haben Datenbank-Migrationen, die nicht rückwärtskompatibel sind. Blue und Green teilen dieselbe DB. Wenn Green ein Schema-Change benötigt, kann Blue brechen.
  • Ihr Service ist groß. Ein vollständiges Green zu provisionieren kann Minuten an Scheduling kosten.

Blue-Green mit 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

Zwei wichtige Eigenschaften. autoPromotionEnabled: false erfordert eine manuelle Promotion (oder einen erfolgreichen AnalysisRun), bevor umgeschaltet wird. scaleDownDelaySeconds: 600 hält die alte ReplicaSet nach der Promotion 10 Minuten am Leben – das ist Ihr Rollback-Fenster.

Canary: Der Gradient

Canary-Releases leiten einen kleinen Anteil des Traffics auf die neue Version, beobachten die Metriken und weiten schrittweise aus. Richtig gemacht ist das in den meisten Produktionsumgebungen die sicherste Strategie. Falsch gemacht (keine Metriken, kein automatisches Abort, manuelle Weight Bumps) ist es schlechter als ein Rolling Update, weil der babysittende Engineer bei jedem Schritt einfach „Promote" klickt.

Wann Canary gewinnt

  • Hoch frequentierte Services. 10 % von einer Million Requests pro Minute sind reichlich Signal, um Regressionen zu erkennen.
  • Kostensensibilität. Sie fahren 110 % Kapazität während des Rollouts, nicht 200 %.
  • Graduelle Risikoexposition. Ideal für nutzerseitige Änderungen, bei denen „intern sieht es gut aus" nicht genügt.

Wann es wehtut

  • Niedrig frequentierte Services. 10 % von 50 rpm sind 5 rpm. Das ist in fünf Minuten nicht genug Signal, um etwas zu erkennen. Dafür sind Blue-Green oder plain Rolling Update in Ordnung.
  • Schwer isolierbare Metriken. Wenn Sie die Error-Rate pro Version nicht berechnen können, ist Canary-Analyse eine Lüge.

Canary mit Argo Rollouts und 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

Beachten Sie, wie die Pausen anfangs kurz und später länger sind. Die ersten Minuten eines Canary sind am informativsten – wenn die neue Version crasht oder 500er wirft, wollen Sie es sofort sehen.

Automatisierte Analyse: Der Teil, der wirklich zählt

Keine Strategie ist sicher ohne automatisiertes, metrikgetriebenes Abort. Argo Rollouts nutzt AnalysisTemplate, um Prometheus (oder Datadog, New Relic, CloudWatch usw.) abzufragen und den Rollout bei schlechtem Signal fehlschlagen zu lassen.

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

Und ein Latenz-Template nach demselben Muster:

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

Schlägt eines davon fehl, bricht der Rollout ab und stellt den stabilen Traffic ohne Mensch im Loop wieder her. Genau darum geht es.

Datenbank-Migrationen: Der stille Killer

Der mit Abstand häufigste Grund, warum „Zero Downtime" zu „Ausfall" wird, ist eine Datenbank-Migration, die angenommen hat, der alte Code sei bereits verschwunden. Sowohl Blue-Green als auch Canary servieren alten und neuen Application Code gleichzeitig. Ihr Schema muss für beide gleichzeitig funktionieren.

Das Expand-and-Contract-Muster

Shippen Sie niemals eine breaking Migration in einem einzigen Release. Teilen Sie sie auf in:

  1. Expand. Die neue Spalte/Tabelle/den neuen Index hinzufügen. Alter Code ignoriert sie. Neuer Code schreibt in alt und neu.
  2. Backfill. Daten von alt nach neu kopieren. Läuft als Background Job.
  3. Reads migrieren. Neuer Code liest aus der neuen Spalte. Alter Code liest noch aus der alten.
  4. Contract. Die alte Spalte in einem späteren Release entfernen, sobald kein laufender Pod sie mehr nutzt.

Ja, das sind vier Releases statt einem. Es ist auch der Unterschied zwischen einer geplanten Änderung und einem Incident um zwei Uhr morgens.

-- 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 und Graceful Shutdown

Selbst bei perfektem Traffic Shifting reißt ein Pod, der mitten im Request stirbt, diesen Request mit. Kubernetes sendet SIGTERM, wartet terminationGracePeriodSeconds, dann sendet es SIGKILL. Ihre App muss:

  1. Ihre Readiness Probe sofort fehlschlagen lassen (damit sie aus den Endpoints entfernt wird).
  2. In-flight Requests weiter bedienen.
  3. Den HTTP-Listener schließen und Verbindungen drainen.
  4. Sauber beenden, bevor die Grace Period endet.

Minimaler 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);

Und die passende Pod-Spec:

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

Der preStop-Hook gibt dem Service Mesh einen Moment, das Entfernen zu propagieren, bevor der Prozess mit dem Herunterfahren beginnt. Kleinigkeit, enormer Unterschied in Produktion.

Die Wahl zwischen beiden

KriteriumBlue-GreenCanary
Traffic-ModellAlles-oder-nichtsGraduelle Prozentsätze
Rollback-GeschwindigkeitSofort (Label-Flip)Sekunden (Traffic-Reset)
Zusätzliche Kapazität2x während Umschalten1,1x während Rollout
Echte NutzervalidierungErst nach UmschaltenWährend Rollout
Nutzbarer Mindest-TrafficBeliebig~100 rpm+
DB-Migrations-ToleranzSchwieriger (geteilte DB)Gleich
KomplexitätGeringerHöher (Metriken erforderlich)

In der Praxis empfehlen wir Canary als Default für jeden Service mit genug Traffic, damit die Analyse aussagekräftig ist, und Blue-Green für Low-Traffic-Kritik-Services (Auth, Payments, Webhooks), wo vollständige Pre-Promotion-Smoke-Tests wertvoller sind als graduelle Exposition.

Nächste Schritte

Furchtlos shippen erfordert drei Dinge: eine Traffic-Strategie, automatisiertes metrikgetriebenes Abort und eine Datenbank-Disziplin, die davon ausgeht, dass zwei Code-Versionen gleichzeitig laufen. Das YAML ist einfach. Die Disziplin ist der harte Teil. Beginnen Sie damit, Expand-and-Contract auf Ihre nächste Schema-Änderung anzuwenden, ergänzen Sie ein AnalysisTemplate, das Ihr bestehendes Prometheus abfragt, und fahren Sie Ihren ersten Canary auf einem unkritischen Service. Wenn Sie Unterstützung beim Aufbau einer Progressive-Delivery-Plattform über mehrere Services hinweg wünschen, nehmen Sie Kontakt auf.

abgelegt unter
deploymentsrekuberneteskubernetesargocdargocd
mit uns arbeiten

Soll unser Team Ihrer Infrastruktur helfen?

talk to an engineerFree 30-min discovery callBook
close