Saltar al contenido

Ingeniería del caos: romper cosas a propósito

Practica la ingeniería del caos inyectando fallos controlados en producción para hallar debilidades: particiones de red, agotamiento y fallos de dependencias.

5 min de lectura
Panel del sistema mostrando un experimento de caos en curso con inyección de fallos controlados y monitorización del impacto en tiempo real

Todos los sistemas distribuidos tienen modos de fallo que aún no has descubierto. Puedes esperar a que aparezcan como incidentes a las 3 de la madrugada, o puedes encontrarlos deliberadamente inyectando fallos controlados en horario laboral, mientras los ingenieros están despiertos y listos. La ingeniería del caos no consiste en romper cosas al azar: es una práctica disciplinada que formula hipótesis sobre cómo tu sistema maneja los fallos y luego las pone a prueba en condiciones controladas.

La idea detrás de la ingeniería del caos es que los sistemas complejos fallan de formas complejas. Los tests unitarios verifican que los componentes individuales funcionan. Los tests de integración verifican que los componentes funcionan juntos. Los experimentos de caos verifican que el sistema se degrada con elegancia cuando los componentes fallan inesperadamente: los fallos que no puedes predecir leyendo el código.

El marco de experimentación

Todo experimento de caos sigue el método científico: hipótesis, experimento, observación, conclusión. Sin esta estructura, solo estás rompiendo cosas.

markdownmarkdown
## Chaos experiment template
 
### 1. Steady State Hypothesis
Define what "normal" looks like using measurable metrics.
"Our checkout flow processes orders with P99 latency 
under 2 seconds and error rate below 0.1%"
 
### 2. Hypothesis
What do you expect to happen when the failure occurs?
"If the payment service becomes unreachable, the 
checkout flow should retry 3 times and then return 
a user-friendly error within 10 seconds. No orders 
should be double-charged."
 
### 3. Experiment Design
- What failure are you injecting?
- What's the blast radius? (percentage of traffic)
- How long will the experiment run?
- What's the abort condition?
 
### 4. Run the Experiment
Inject the failure and observe.
 
### 5. Analyze Results
Did the system behave as hypothesized?
If not, what failed and why?
 
### 6. Fix and Rerun
Address any issues discovered and run the 
experiment again to verify the fix.

Empezar poco a poco: game days

Antes de automatizar experimentos de caos en producción, empieza con game days facilitados, donde el equipo inyecta fallos manualmente y discute lo que ocurre.

tstypescript
// Game day experiment: kill a service instance
interface GameDayExperiment {
  name: string;
  description: string;
  steadyState: SteadyStateDefinition;
  action: FailureAction;
  duration: Duration;
  abortConditions: AbortCondition[];
  owners: string[];
}
 
const firstExperiment: GameDayExperiment = {
  name: 'Payment service instance failure',
  description:
    'Kill one of three payment service replicas and verify ' +
    'traffic redistributes without user impact.',
  steadyState: {
    metrics: [
      { name: 'checkout_success_rate', operator: 'gte', value: 99.9 },
      { name: 'checkout_p99_latency_ms', operator: 'lte', value: 2000 },
      { name: 'payment_error_rate', operator: 'lte', value: 0.1 },
    ],
    verifyBeforeStart: true,
    verifyDuringExperiment: true,
  },
  action: {
    type: 'kill-pod',
    target: 'payment-service',
    count: 1,
    totalReplicas: 3,
  },
  duration: { minutes: 15 },
  abortConditions: [
    { metric: 'checkout_success_rate', operator: 'lt', value: 99.0 },
    { metric: 'checkout_p99_latency_ms', operator: 'gt', value: 5000 },
  ],
  owners: ['platform-team', 'payments-team'],
};

Patrones comunes de inyección de fallos

Distintos tipos de fallo exponen distintas debilidades. Empieza con los fallos más probables antes de ponerte creativo.

ymlyaml
# Kubernetes-based failure injection examples
 
# 1. Pod failure: kill a running instance
apiVersion: chaos-mesh.org/v1alpha1
kind: PodChaos
metadata:
  name: payment-pod-kill
spec:
  action: pod-kill
  mode: one
  selector:
    namespaces: [production]
    labelSelectors:
      app: payment-service
  duration: "5m"
  scheduler:
    cron: "@every 2h"  # Recurring experiment
 
---
# 2. Network delay: add latency to service calls
apiVersion: chaos-mesh.org/v1alpha1
kind: NetworkChaos
metadata:
  name: database-latency
spec:
  action: delay
  mode: all
  selector:
    namespaces: [production]
    labelSelectors:
      app: order-service
  delay:
    latency: "500ms"
    jitter: "100ms"
  direction: to
  target:
    selector:
      namespaces: [production]
      labelSelectors:
        app: postgres
  duration: "10m"
 
---
# 3. Network partition: isolate a service
apiVersion: chaos-mesh.org/v1alpha1
kind: NetworkChaos
metadata:
  name: cache-partition
spec:
  action: partition
  mode: all
  selector:
    namespaces: [production]
    labelSelectors:
      app: api-gateway
  direction: both
  target:
    selector:
      namespaces: [production]
      labelSelectors:
        app: redis-cache
  duration: "5m"
tstypescript
// Application-level chaos: inject failures in code
// Useful for testing specific error paths
 
class ChaosMiddleware {
  private experiments: Map<string, ExperimentConfig> = new Map();
 
  middleware() {
    return (req: Request, res: Response, next: NextFunction) => {
      for (const [name, config] of this.experiments) {
        if (this.shouldApply(config, req)) {
          switch (config.type) {
            case 'latency':
              return setTimeout(next, config.delayMs);
            case 'error':
              return res.status(config.statusCode).json({
                error: 'Chaos experiment: simulated failure',
              });
            case 'timeout':
              // Don't respond at all — tests client timeout handling
              return;
          }
        }
      }
      next();
    };
  }
 
  private shouldApply(config: ExperimentConfig, req: Request): boolean {
    // Only apply to configured percentage of requests
    return (
      config.enabled &&
      Math.random() * 100 < config.percentageAffected &&
      config.pathPattern.test(req.path)
    );
  }
}

Monitorización durante los experimentos

Necesitas visibilidad en tiempo real del comportamiento del sistema durante los experimentos. Si no puedes observar el impacto, no puedes aprender de él.

tstypescript
// Experiment monitoring dashboard
interface ExperimentMonitor {
  checkSteadyState(): Promise<SteadyStateResult>;
  shouldAbort(): Promise<boolean>;
  captureSnapshot(): Promise<MetricSnapshot>;
}
 
class ExperimentRunner {
  async run(experiment: GameDayExperiment): Promise<ExperimentResult> {
    const monitor = new ExperimentMonitorImpl(experiment);
 
    // Verify steady state before starting
    const baseline = await monitor.checkSteadyState();
    if (!baseline.healthy) {
      return {
        status: 'skipped',
        reason: 'System not in steady state before experiment',
        baseline,
      };
    }
 
    console.log(`Starting experiment: ${experiment.name}`);
    const startSnapshot = await monitor.captureSnapshot();
 
    // Inject the failure
    await this.injectFailure(experiment.action);
 
    // Monitor throughout the experiment
    const observations: MetricSnapshot[] = [];
    const checkInterval = setInterval(async () => {
      const snapshot = await monitor.captureSnapshot();
      observations.push(snapshot);
 
      // Auto-abort if conditions are breached
      if (await monitor.shouldAbort()) {
        console.log('ABORT: conditions breached, rolling back');
        await this.rollback(experiment.action);
        clearInterval(checkInterval);
      }
    }, 10_000); // Check every 10 seconds
 
    // Wait for experiment duration
    await this.wait(experiment.duration);
    clearInterval(checkInterval);
 
    // Remove the failure
    await this.rollback(experiment.action);
 
    // Capture recovery metrics
    await this.wait({ minutes: 2 });
    const recoverySnapshot = await monitor.captureSnapshot();
 
    return {
      status: 'completed',
      baseline: startSnapshot,
      observations,
      recovery: recoverySnapshot,
      hypothesis: experiment.steadyState,
    };
  }
}

Evolucionar hacia el caos continuo

Cuando tu equipo se sienta cómodo con los game days manuales, pasa a experimentos automatizados que se ejecutan de forma continua. Esto detecta regresiones a medida que el sistema evoluciona.

tstypescript
// Continuous chaos experiment pipeline
interface ContinuousChaosConfig {
  experiments: GameDayExperiment[];
  schedule: {
    // Run during business hours when engineers are available
    timezone: string;
    hours: { start: number; end: number };
    daysOfWeek: number[]; // 1-5 for weekdays
  };
  notifications: {
    onStart: string[];      // Slack channels
    onAbort: string[];      // PagerDuty + Slack
    onComplete: string[];   // Slack + email summary
  };
  safetyControls: {
    // Never run during deployments
    pauseDuringDeploys: boolean;
    // Never run during incidents
    pauseDuringIncidents: boolean;
    // Maximum concurrent experiments
    maxConcurrent: number;
    // Global kill switch
    emergencyStop: boolean;
  };
}

Lo que los experimentos de caos suelen descubrir

markdownmarkdown
## Top findings from chaos experiments
 
### 1. Timeouts are wrong
- Default timeouts are too high (30s when 2s is appropriate)
- Some services have no timeout at all
- Cascading timeouts: A calls B calls C, each with 30s 
  timeout = 90s total wait
 
### 2. Retries amplify failures
- Service retries 3x on failure
- 10 callers each retry 3x = 30 requests to a struggling 
  service that can barely handle 10
- Missing exponential backoff and jitter
 
### 3. Circuit breakers don't trip
- Configured but never tested
- Thresholds set too high to ever trigger
- No fallback behavior defined
 
### 4. Health checks lie
- Service reports healthy while database is unreachable
- Liveness probe passes but readiness should fail
- Health endpoint doesn't check critical dependencies
 
### 5. Graceful degradation paths don't exist
- Cache fails → error instead of slower database query
- Search fails → blank page instead of basic list view
- Payment fails → no way to queue order for retry

Conclusiones clave

La ingeniería del caos es una práctica disciplinada que sigue el método científico: define cómo es lo normal con métricas medibles, formula una hipótesis sobre el comportamiento ante fallos, inyecta fallos controlados, observa si el sistema coincide con tu hipótesis y corrige lo que no coincide. Empieza con game days facilitados, donde el equipo mata pods manualmente y observa el impacto, antes de automatizar experimentos: las discusiones en torno a "¿qué debería ocurrir?" y "¿qué ocurrió realmente?" generan más conocimiento sobre resiliencia que cualquier automatización. Ten siempre condiciones de aborto que reviertan automáticamente el experimento si el impacto supera los umbrales aceptables: los experimentos de caos deben descubrir debilidades, no causar caídas, y la monitorización en tiempo real con controles de seguridad automáticos lo hace posible. Los descubrimientos más comunes son timeouts mal configurados, tormentas de reintentos que amplifican los fallos, circuit breakers que nunca saltan y health checks que mienten: estas brechas sistémicas de resiliencia son casi imposibles de encontrar mediante revisión de código o tests unitarios, pero salen a la luz de inmediato cuando inyectas fallos reales en el sistema.

Wilfredo Rujel

Wilfredo Rujel

Ingeniero de Software Full Stack

Compartir esta publicaciónX