PHP Academy · Lección

Resiliencia: circuit breakers y reintentos

Mantenga los sistemas estables ante fallos parciales

Lección 4 de 413 pasos

Resiliencia: circuit breakers y reintentos es una lección gratuita de PHP Academy en CoddyKit. Esta es la lección 4 de 4. Puedes leer la lección completa abajo gratuitamente — luego la practicas en el navegador con un editor de código integrado y un tutor de IA 24/7. Forma parte de la ruta de aprendizaje de PHP Academy, y tu progreso se sincroniza en la web y la app de CoddyKit. El curso de PHP Academy incluye 4 lecciones en total.

Patrones de resiliencia

En un sistema distribuido, los fallos parciales son el estado normal: siempre hay alguna dependencia lenta, reiniciándose o sobrecargada. La resiliencia consiste en diseñar cada servicio de modo que una dependencia en mal estado no arrastre consigo al servicio y, después, a todo el sistema.

En esta lección se presenta el conjunto de herramientas esencial: tiempos de espera, reintentos con backoff, circuit breakers, bulkheads y degradación controlada, todo ello desde PHP.

Los tiempos de espera, lo primero

El ajuste de resiliencia más importante es el tiempo de espera. Sin uno, un servicio downstream lento mantiene ocupados a sus workers de PHP-FPM; las solicitudes se acumulan; se queda sin workers y su servicio deja de funcionar porque otro servicio era lento. Este es el ejemplo clásico de fallo en cascada.

Establezca un tiempo de espera de conexión y un tiempo de espera total de la solicitud en cada llamada saliente. Siempre.

<?php
require 'vendor/autoload.php';
use GuzzleHttp\Client;

$http = new Client([
    'connect_timeout' => 0.5, // fail fast if we can't even connect
    'timeout'         => 2.0, // hard cap on the whole call
]);
// A hung dependency now fails in 2s instead of holding a worker forever.

Reintentos: con cuidado

Los fallos transitorios (un parpadeo, una breve interrupción de red o un 503) suelen resolverse en el segundo intento. Sin embargo, los reintentos son peligrosos: si reintenta demasiado pronto, amplifica la carga sobre un servicio que ya tiene problemas. Reglas:

  • Reintente únicamente operaciones idempotentes y errores que admitan reintentos (tiempos de espera, 5xx, 429).
  • Limite el número de intentos.
  • No reintente nunca los errores de cliente 4xx: volverán a fallar.

Backoff exponencial + jitter

Los reintentos a intervalos fijos de muchos clientes se sincronizan y forman una avalancha que golpea al servicio en recuperación en oleadas. La solución es aplicar backoff exponencial (duplicar el retraso en cada intento) junto con jitter (aleatoriedad) para distribuir la carga.

<?php
function backoffDelay(int $attempt, float $base = 0.1, float $cap = 5.0): float {
    $exp = min($cap, $base * (2 ** $attempt));   // 0.1, 0.2, 0.4, ...
    return mt_rand(0, (int)($exp * 1000)) / 1000; // full jitter: 0..exp
}
for ($i = 0; $i < 5; $i++) {
    printf("attempt %d -> wait %.3fs\n", $i, backoffDelay($i));
}

Un bucle de reintento

Así se combinan las piezas: limite los intentos, reintente solo ante errores que admitan reintentos, espere entre intentos usando un backoff con jitter y muestre el último fallo si todos los intentos fracasan.

<?php
function withRetry(callable $op, int $maxAttempts = 4): mixed {
    $attempt = 0;
    while (true) {
        try {
            return $op();
        } catch (\Throwable $e) {
            $attempt++;
            if ($attempt >= $maxAttempts || !isRetryable($e)) {
                throw $e; // give up
            }
            usleep((int)(backoffDelay($attempt) * 1_000_000));
        }
    }
}
function isRetryable(\Throwable $e): bool { return $e->getCode() === 0 || $e->getCode() >= 500; }
function backoffDelay(int $a): float { return min(5.0, 0.1 * (2 ** $a)) * (mt_rand(0, 100) / 100); }

El circuit breaker

Los reintentos ayudan con los fallos breves, pero si una dependencia está realmente caída, reintentar cada solicitud solo desperdicia tiempo y recursos. Un circuit breaker registra los fallos y, cuando superan un umbral, se abre: corta las llamadas y falla al instante en lugar de esperar a un servicio inactivo.

Tiene tres estados: Cerrado (las llamadas pasan y los fallos se contabilizan), Abierto (las llamadas se rechazan de inmediato) y Semia-abierto (unas pocas llamadas de prueba comprueban si se ha recuperado).

Máquina de estados del circuit breaker

Las transiciones son las siguientes: Cerrado → Abierto cuando los fallos superan el umbral. Abierto → Semia-abierto después de un período de enfriamiento. Semia-abierto → Cerrado cuando un sondeo tiene éxito, o de vuelta a Abierto si falla. El estado debe compartirse entre los procesos de PHP (Redis/APCu), ya que cada solicitud se ejecuta en un proceso nuevo.

<?php
final class CircuitBreaker {
    public function __construct(
        private int $threshold = 5,
        private int $coolDown = 30, // seconds
    ) {}

    public function call(callable $op, array &$state): mixed {
        if ($state['status'] === 'open') {
            if (time() - $state['openedAt'] < $this->coolDown) {
                throw new \RuntimeException('Circuit open - failing fast');
            }
            $state['status'] = 'half-open'; // time to probe
        }
        try {
            $result = $op();
            $state = ['status' => 'closed', 'failures' => 0]; // recovered
            return $result;
        } catch (\Throwable $e) {
            $state['failures'] = ($state['failures'] ?? 0) + 1;
            if ($state['failures'] >= $this->threshold || $state['status'] === 'half-open') {
                $state['status'] = 'open';
                $state['openedAt'] = time();
            }
            throw $e;
        }
    }
}

Bulkheads

El patrón bulkhead, cuyo nombre procede de los compartimentos de los barcos, aísla los recursos para que un fallo en una zona no hunda toda la embarcación. Si todos sus workers pueden llamar al servicio Reports lento, una interrupción de Reports puede consumir cada worker y dejar a Checkout sin recursos.

Particione los recursos: use grupos de conexiones separados, grupos de workers o colas separados por dependencia, o límites de concurrencia por servicio downstream. Así, una dependencia defectuosa solo agota su propia parte.

Degradación controlada y fallbacks

Cuando una dependencia no crítica no está disponible, degrade el servicio en lugar de devolver un error. Sirva un valor almacenado en caché, un valor predeterminado o una experiencia reducida. El estado abierto del circuit breaker es el desencadenante natural de la ruta de fallback.

<?php
function getRecommendations(callable $remoteCall, Redis $cache, string $userId): array {
    try {
        $recs = $remoteCall($userId);
        $cache->setex("recs:$userId", 3600, json_encode($recs));
        return $recs;
    } catch (\Throwable $e) {
        // Fallback 1: last-known-good from cache
        if ($cached = $cache->get("recs:$userId")) {
            return json_decode($cached, true);
        }
        // Fallback 2: generic popular items - never block the page
        return ['popular-1', 'popular-2'];
    }
}

Claves de idempotencia para reintentos seguros

Los reintentos solo son seguros en operaciones idempotentes. Para una acción no idempotente como «cobrar una tarjeta», adjunte una clave de idempotencia que el servidor utilice para deduplicar: si llega un reintento después de que el primer intento ya haya tenido éxito (pero se haya perdido la respuesta), el servidor devuelve el resultado original en lugar de cobrar dos veces.

<?php
function chargeWithRetry(callable $http, string $orderId, int $cents): array {
    // Same key across all retries of THIS logical charge
    $key = 'charge-' . $orderId;
    return withRetry(fn() => $http('POST', '/charges', [
        'headers' => ['Idempotency-Key' => $key],
        'json'    => ['order' => $orderId, 'amount' => $cents],
    ]));
}
function withRetry(callable $op) { return $op(); } // see earlier scene

Cómo encajan las piezas

Los patrones se pueden combinar, y el orden importa. Una llamada saliente robusta normalmente se estructura así:

  • Tiempo de espera en cada intento individual (el nivel más interno).
  • Reintento con backoff envolviendo la llamada con tiempo de espera para tratar fallos transitorios.
  • Circuit breaker envolviendo el reintento para que una interrupción prolongada active el corte rápidamente.
  • Bulkhead limitando la capacidad que puede consumir esta dependencia.
  • Fallback en el nivel más externo, capturando todo lo que llegue hasta allí.

Existen bibliotecas con envoltorios al estilo de Resilience4PHP, pero comprender las capas es más importante que cualquier paquete específico.

Comprobación rápida

Elegir el patrón adecuado.

Resumen

Cómo sobrevivir a los fallos parciales:

  • Los tiempos de espera en cada llamada evitan agotar los workers y previenen los fallos en cascada.
  • Use reintentos solo para operaciones idempotentes y errores que admitan reintentos, con backoff exponencial + jitter.
  • Los circuit breakers fallan rápido (Cerrado → Abierto → Semia-abierto) cuando una dependencia está caída.
  • Los bulkheads aíslan los recursos para que un fallo no deje sin recursos a todo lo demás.
  • La degradación controlada y los fallbacks mantienen activos los flujos principales.

Combinados por capas, convierten los fallos inevitables en incidentes contenidos y recuperables.

Gratis para empezar

Aprende PHP con un tutor de IA — gratis

Escribe y ejecuta código real en tu navegador, obtén ayuda instantánea de un tutor de IA disponible 24/7 y continúa donde lo dejaste en la web o en la aplicación.

Cursos
49
Lecciones
195

Preguntas frecuentes

¿La lección «Resiliencia: circuit breakers y reintentos» es gratis?

Sí — el texto completo de «Resiliencia: circuit breakers y reintentos» es gratis para leer aquí en la web. Para practicarla de forma interactiva (editor de código integrado y tutor de IA 24/7) y desbloquear el resto del curso de PHP Academy, actualiza a CoddyKit PRO. El curso de PHP Academy incluye 4 lecciones en total.

¿Qué aprenderé en «Resiliencia: circuit breakers y reintentos»?

Mantenga los sistemas estables ante fallos parciales Practicas PHP Academy con código real que ejecutas directamente en el navegador, y un tutor de IA 24/7 responde tus preguntas mientras trabajas en la lección.

¿Necesito experiencia previa para empezar PHP Academy?

No se requiere experiencia previa. PHP Academy en CoddyKit está estructurado para principiantes hasta estudiantes avanzados, así que puedes empezar aquí o desde el inicio y avanzar a tu ritmo. Esta es la lección 4 de 4.

¿Cuánto tiempo toma la lección «Resiliencia: circuit breakers y reintentos»?

La mayoría de las lecciones de CoddyKit toman alrededor de 5–10 minutos. Cada una es compacta e interactiva, así que avanzas constantemente y retomas exactamente por donde dejaste en la web y la app.

¿Puedo escribir y ejecutar código en esta lección de PHP Academy?

Sí. Cada lección de PHP Academy incluye un editor de código integrado, así que escribes y ejecutas código real directamente en tu navegador y obtienes retroalimentación instantánea de IA — sin configuración local necesaria.

Todas las lecciones de este curso

  1. Del monolito a los microservicios
  2. Comunicación entre servicios: REST y gRPC
  3. Puertas de enlace de API y descubrimiento de servicios
  4. Resiliencia: circuit breakers y reintentos
← Volver a PHP Academy