Zum Inhalt springen

Canary-Deployments: Schrittweise Rollouts richtig gemacht

Canary-Deployments leiten einen Teil des Traffics auf die neue Version und rollen bei steigenden Fehlerraten automatisch zurück — so geht es sicher.

3 Min. Lesezeit
Diagramm der Traffic-Aufteilung, das 5% der Anfragen zeigt, die an die Canary-Version geleitet werden

Blue-Green-Deployments schalten 100 % des Traffics auf einen Schlag um. Das ist in Ordnung, wenn deine Smoke-Tests alles abfangen, aber manche Fehler treten nur unter echten Traffic-Mustern auf. Canary-Deployments gehen vorsichtiger vor: Ein kleiner Prozentsatz des Traffics wird auf die neue Version geleitet, zentrale Metriken werden überwacht und der Prozentsatz schrittweise erhöht — oder bei Problemen automatisch zurückgerollt.

Der Canary-Prozess

Ein Canary-Deployment folgt einem progressiven Rollout: Beginne mit 1–5 % des Traffics, beobachte ein Zeitfenster lang, erhöhe den Prozentsatz, wenn die Metriken gesund sind, und wiederhole das Ganze bis 100 %.

tstypescript
interface CanaryStage {
  percentage: number;
  durationMinutes: number;
  metrics: MetricCheck[];
}
 
interface MetricCheck {
  name: string;
  query: string;
  threshold: number;
  comparison: "less_than" | "greater_than";
}
 
const canaryPlan: CanaryStage[] = [
  {
    percentage: 5,
    durationMinutes: 10,
    metrics: [
      { name: "error_rate", query: "rate(http_5xx[5m])/rate(http_total[5m])", threshold: 0.01, comparison: "less_than" },
      { name: "p99_latency", query: "histogram_quantile(0.99, rate(http_duration_bucket[5m]))", threshold: 2.0, comparison: "less_than" },
    ],
  },
  {
    percentage: 25,
    durationMinutes: 15,
    metrics: [
      { name: "error_rate", query: "rate(http_5xx[5m])/rate(http_total[5m])", threshold: 0.01, comparison: "less_than" },
      { name: "p99_latency", query: "histogram_quantile(0.99, rate(http_duration_bucket[5m]))", threshold: 2.0, comparison: "less_than" },
    ],
  },
  {
    percentage: 50,
    durationMinutes: 15,
    metrics: [
      { name: "error_rate", query: "rate(http_5xx[5m])/rate(http_total[5m])", threshold: 0.01, comparison: "less_than" },
      { name: "p99_latency", query: "histogram_quantile(0.99, rate(http_duration_bucket[5m]))", threshold: 2.0, comparison: "less_than" },
    ],
  },
  { percentage: 100, durationMinutes: 0, metrics: [] },
];

Traffic-Aufteilung mit Nginx

Die split_clients-Direktive von Nginx leitet einen deterministischen Prozentsatz des Traffics anhand einer Client-Kennung auf den Canary.

nginxnginx
# ❌ Random routing — same user might flip between versions
upstream canary { server canary-1:3000; server canary-2:3000; }
upstream stable { server stable-1:3000; server stable-2:3000; }
 
# ✅ Consistent routing — same user always hits the same version
split_clients "$remote_addr$uri" $upstream_variant {
  5%    canary;
  *     stable;
}
 
upstream canary { server canary-1:3000; server canary-2:3000; }
upstream stable { server stable-1:3000; server stable-2:3000; }
 
server {
  listen 80;
 
  location / {
    proxy_pass http://$upstream_variant;
    # Add header so downstream services know which version handled the request
    proxy_set_header X-Deployment-Version $upstream_variant;
  }
}

Automatisierter Metrikvergleich

Der Canary-Release-Controller vergleicht die Metriken zwischen der Canary- und der stabilen Version. Wenn die Fehlerrate oder Latenz des Canaries deutlich schlechter ist, löst er einen automatischen Rollback aus.

tstypescript
interface MetricComparison {
  metric: string;
  canaryValue: number;
  stableValue: number;
  degradation: number;
  acceptable: boolean;
}
 
async function compareCanaryMetrics(
  canarySelector: string,
  stableSelector: string,
  window: string
): Promise<MetricComparison[]> {
  const comparisons: MetricComparison[] = [];
 
  // Compare error rates
  const canaryErrors = await queryPrometheus(
    `rate(http_errors_total{deployment="${canarySelector}"}[${window}])`
  );
  const stableErrors = await queryPrometheus(
    `rate(http_errors_total{deployment="${stableSelector}"}[${window}])`
  );
 
  const errorDegradation = stableErrors > 0
    ? (canaryErrors - stableErrors) / stableErrors
    : canaryErrors > 0 ? 1 : 0;
 
  comparisons.push({
    metric: "error_rate",
    canaryValue: canaryErrors,
    stableValue: stableErrors,
    degradation: errorDegradation,
    acceptable: errorDegradation < 0.10, // Less than 10% worse
  });
 
  // Compare p99 latency
  const canaryLatency = await queryPrometheus(
    `histogram_quantile(0.99, rate(http_duration_bucket{deployment="${canarySelector}"}[${window}]))`
  );
  const stableLatency = await queryPrometheus(
    `histogram_quantile(0.99, rate(http_duration_bucket{deployment="${stableSelector}"}[${window}]))`
  );
 
  const latencyDegradation = stableLatency > 0
    ? (canaryLatency - stableLatency) / stableLatency
    : 0;
 
  comparisons.push({
    metric: "p99_latency",
    canaryValue: canaryLatency,
    stableValue: stableLatency,
    degradation: latencyDegradation,
    acceptable: latencyDegradation < 0.15, // Less than 15% worse
  });
 
  return comparisons;
}

Kubernetes-Canary mit Istio

Das VirtualService von Istio bietet eine feingranulare Traffic-Aufteilung für Kubernetes-Deployments.

ymlyaml
# Istio VirtualService — 5% canary split
apiVersion: networking.istio.io/v1beta1
kind: VirtualService
metadata:
  name: api-server
spec:
  hosts:
    - api-server
  http:
    - route:
        - destination:
            host: api-server
            subset: stable
          weight: 95
        - destination:
            host: api-server
            subset: canary
          weight: 5
 
---
apiVersion: networking.istio.io/v1beta1
kind: DestinationRule
metadata:
  name: api-server
spec:
  host: api-server
  subsets:
    - name: stable
      labels:
        version: v1.2.0
    - name: canary
      labels:
        version: v1.3.0

Wann Canary vs. Blue-Green

markdownmarkdown
| Criteria              | Canary                        | Blue-Green              |
|-----------------------|-------------------------------|-------------------------|
| Risk tolerance        | Lower (gradual exposure)      | Higher (all-at-once)    |
| Rollback speed        | Fast (reduce to 0%)           | Instant (switch back)   |
| Infrastructure cost   | Minimal (small canary fleet)  | Double (two full envs)  |
| Metric comparison     | Side-by-side during rollout   | Before/after only       |
| Complexity            | Higher (traffic splitting)    | Lower (DNS/LB switch)   |
| Best for              | High-traffic, risk-averse     | Low-traffic, simple      |

Die wichtigsten Erkenntnisse

  1. Canary-Deployments reduzieren den Blast Radius — die neue Version wird vor dem vollständigen Rollout nur einem kleinen Teil der Nutzer ausgesetzt
  2. Konsistentes Routing verwenden — derselbe Nutzer sollte während der Canary-Phase immer auf derselben Version landen
  3. Metrikvergleich automatisieren — vergleiche die Fehlerraten und Latenzen von Canary und stabiler Version, nicht nur absolute Schwellenwerte
  4. Rollback-Auslöser vor dem Start definieren — ein automatischer Rollback bei Metrikverschlechterung verhindert menschliches Zögern
  5. Traffic schrittweise erhöhen — 5 % → 25 % → 50 % → 100 % bietet mehrere Beobachtungsfenster
  6. Canary findet, was Tests übersehen — manche Fehler zeigen sich nur unter echten Traffic-Mustern und echten Daten
Wilfredo Rujel

Wilfredo Rujel

Full-Stack-Softwareentwickler

Diesen Beitrag teilenX