Saltar al contenido

Despliegues canary: lanzamientos graduales bien hechos

Los despliegues canary envían una parte del tráfico a la nueva versión y revierten solos si suben los errores: cómo implementarlos de forma segura.

3 min de lectura
Diagrama de división de tráfico que muestra el 5% de las solicitudes dirigidas a la versión canary

Los despliegues blue-green cambian el 100% del tráfico de una sola vez. Eso está bien cuando tus pruebas de humo lo detectan todo, pero algunos errores solo aparecen bajo patrones de tráfico reales. Los despliegues canary adoptan un enfoque más cauteloso: dirigen un pequeño porcentaje del tráfico a la nueva versión, monitorean las métricas clave y aumentan gradualmente el porcentaje, o revierten automáticamente si algo sale mal.

El proceso canary

Un despliegue canary sigue un lanzamiento progresivo: empieza con el 1-5% del tráfico, observa durante una ventana de tiempo, aumenta el porcentaje si las métricas están sanas y repite hasta llegar al 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: [] },
];

División de tráfico con Nginx

La directiva split_clients de Nginx dirige un porcentaje determinista del tráfico al canary basándose en un identificador del cliente.

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;
  }
}

Comparación automatizada de métricas

El controlador de lanzamiento canary compara las métricas entre las versiones canary y estable. Si la tasa de errores o la latencia del canary es significativamente peor, dispara una reversión automática.

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;
}

Canary en Kubernetes con Istio

El VirtualService de Istio proporciona una división de tráfico granular para los despliegues en Kubernetes.

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

Cuándo usar 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      |

Conclusiones clave

  1. Los despliegues canary reducen el radio de impacto: exponen la nueva versión a una pequeña fracción de usuarios antes del lanzamiento completo
  2. Usa enrutamiento consistente: el mismo usuario debe llegar siempre a la misma versión durante el período canary
  3. Automatiza la comparación de métricas: compara las tasas de error y latencias del canary frente a las de la versión estable, no solo umbrales absolutos
  4. Define los disparadores de reversión antes de empezar: la reversión automática ante la degradación de métricas evita la vacilación humana
  5. Aumenta el tráfico gradualmente: 5% → 25% → 50% → 100% proporciona múltiples ventanas de observación
  6. Canary detecta lo que las pruebas pasan por alto: algunos errores solo se manifiestan bajo patrones de tráfico y datos reales
Wilfredo Rujel

Wilfredo Rujel

Ingeniero de Software Full Stack

Compartir esta publicaciónX