0Pricing
PHP Academy · Lezione

Resilienza: circuit breaker e retry

Mantenga i sistemi operativi anche in caso di guasti parziali

Resilienza: circuit breaker e retry è una lezione PHP Academy gratuita su CoddyKit. Questa è la lezione 4 di 4. Puoi leggere la lezione completa qui gratuitamente — poi esercitati direttamente nel browser con un editor di codice integrato e un tutor IA disponibile 24/7. Fa parte del percorso di apprendimento PHP Academy, e i tuoi progressi si sincronizzano tra il web e l'app CoddyKit. Il corso PHP Academy include 4 lezioni in totale.

Pattern di resilienza

In un sistema distribuito, il guasto parziale è la norma: c'è sempre qualche dipendenza lenta, in fase di riavvio o sovraccarica. La resilienza consiste nel progettare ogni servizio in modo che una dipendenza malfunzionante non trascini con sé il servizio e, di conseguenza, l'intero sistema.

Questa lezione tratta gli strumenti fondamentali: timeout, nuovi tentativi con backoff, circuit breaker, bulkhead e degrado controllato, tutti applicati da PHP.

Prima di tutto, i timeout

L'impostazione di resilienza più importante in assoluto è il timeout. Senza un timeout, un downstream lento tiene occupati i worker PHP-FPM in attesa; le richieste si accumulano; i worker si esauriscono; il servizio si arresta perché qualcun altro era lento. Questo è il classico esempio di guasto a cascata.

Si impostino sempre sia un timeout di connessione sia un timeout totale della richiesta per ogni chiamata in uscita.

<?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.

Nuovi tentativi, con cautela

I guasti temporanei (un'interruzione momentanea, una breve perdita di rete, un 503) spesso si risolvono al secondo tentativo. Tuttavia, i nuovi tentativi sono pericolosi: se vengono eseguiti troppo rapidamente, si amplifica il carico su un servizio già in difficoltà. Regole:

  • Riprovare solo le operazioni idempotenti e gli errori per cui il nuovo tentativo è appropriato (timeout, 5xx, 429).
  • Limitare il numero di tentativi.
  • Non riprovare mai in caso di errori client 4xx: fallirebbero semplicemente di nuovo.

Backoff esponenziale e jitter

I nuovi tentativi a intervalli fissi effettuati da molti client si sincronizzano formando una valanga di richieste che colpisce a ondate il servizio in fase di ripristino. La soluzione consiste nel backoff esponenziale (raddoppiare il ritardo a ogni tentativo) e nel jitter (randomizzazione), così da distribuire il carico nel tempo.

<?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 ciclo di nuovi tentativi

Si combinano i vari elementi: tentativi limitati, nuovi tentativi solo in caso di errori appropriati, attesa con backoff e jitter tra un tentativo e l'altro, e restituzione dell'ultimo errore se tutti i tentativi falliscono.

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

Il circuit breaker

I nuovi tentativi aiutano in caso di interruzioni momentanee, ma se una dipendenza è veramente inattiva, riprovare ogni richiesta fa solo sprecare tempo e risorse. Un circuit breaker tiene traccia dei guasti e, quando superano una determinata soglia, si apre: interrompe le chiamate in anticipo e restituisce immediatamente un errore invece di attendere un servizio non disponibile.

Ha tre stati: Closed (le chiamate passano e i guasti vengono conteggiati), Open (le chiamate vengono rifiutate immediatamente) e Half-Open (alcune chiamate di prova verificano il ripristino).

Macchina a stati del breaker

Le transizioni sono le seguenti: da Closed a Open quando i guasti superano la soglia; da Open a Half-Open dopo un periodo di raffreddamento; da Half-Open a Closed in caso di sonda riuscita, oppure di nuovo a Open in caso di errore. Lo stato deve essere condiviso tra i processi PHP (Redis/APCu), poiché ogni richiesta viene gestita da un processo nuovo.

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

Bulkhead

Il pattern bulkhead, il cui nome deriva dai compartimenti delle navi, isola le risorse in modo che un guasto in un'area non possa affondare l'intera nave. Se tutti i worker possono chiamare il servizio Reports lento, un'interruzione di Reports può consumare ogni worker e lasciare Checkout senza risorse.

Si partizionino le risorse: pool di connessioni separati, pool o code di worker separati per ogni dipendenza oppure limiti di concorrenza per ogni downstream. In questo modo una dipendenza in errore esaurisce solo la propria porzione di risorse.

Degrado controllato e fallback

Quando una dipendenza non critica non è disponibile, si degrada il servizio invece di restituire un errore. Si può fornire un valore memorizzato nella cache, un valore predefinito o un'esperienza ridotta. Lo stato aperto del circuit breaker è il punto di attivazione naturale del percorso di 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'];
    }
}

Chiavi di idempotenza per nuovi tentativi sicuri

I nuovi tentativi sono sicuri solo per le operazioni idempotenti. Per un'azione non idempotente come «addebitare una carta», si associa una chiave di idempotenza che il server usa per deduplicare le richieste: se un nuovo tentativo arriva dopo che il primo ha già avuto successo (ma la risposta è andata persa), il server restituisce il risultato originale invece di effettuare un doppio addebito.

<?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

Combinare i vari elementi

I pattern si combinano e l'ordine è importante. Una chiamata in uscita robusta presenta in genere questo annidamento:

  • Timeout su ogni singolo tentativo (all'interno).
  • Nuovo tentativo con backoff che avvolge la chiamata con timeout per gestire le interruzioni temporanee.
  • Circuit breaker che avvolge il nuovo tentativo, così un'interruzione prolungata fa scattare rapidamente il breaker.
  • Bulkhead che limita la quantità di capacità che questa dipendenza può consumare.
  • Fallback all'esterno, per intercettare qualsiasi errore che risale la catena.

Esistono wrapper in stile Resilience4PHP, ma comprendere la stratificazione è più importante di qualsiasi pacchetto specifico.

Verifica rapida

Scegliere il pattern corretto.

Riepilogo

Gestire i guasti parziali:

  • I timeout su ogni chiamata impediscono l'esaurimento dei worker e i guasti a cascata.
  • I nuovi tentativi vanno eseguiti solo per operazioni idempotenti ed errori appropriati, con backoff esponenziale e jitter.
  • I circuit breaker restituiscono rapidamente un errore (Closed → Open → Half-Open) quando una dipendenza non è disponibile.
  • I bulkhead isolano le risorse, così un singolo guasto non può privare tutto il sistema delle risorse.
  • Il degrado controllato e i fallback mantengono attivi i flussi principali.

Applicati insieme, questi pattern trasformano i guasti inevitabili in eventi circoscritti e recuperabili.

Domande Frequenti

La lezione «Resilienza: circuit breaker e retry» è gratuita?

Sì — il testo completo di «Resilienza: circuit breaker e retry» è gratuito qui sul web. Per esercitarvi in modo interattivo (un editor di codice integrato e un tutor IA 24/7) e sbloccare il resto del corso PHP Academy, passa a CoddyKit PRO. Il corso PHP Academy include 4 lezioni in totale.

Cosa imparerò in «Resilienza: circuit breaker e retry»?

Mantenga i sistemi operativi anche in caso di guasti parziali Eserciti PHP Academy con codice pratico che esegui direttamente nel browser, e un tutor IA 24/7 risponde alle tue domande mentre lavori sulla lezione.

Ho bisogno di esperienza per iniziare PHP Academy?

Non è richiesta alcuna esperienza precedente. PHP Academy su CoddyKit è strutturato per principianti e studenti avanzati, quindi puoi iniziare da qui o dall'inizio e procedere al tuo ritmo. Questa è la lezione 4 di 4.

Quanto tempo richiede la lezione «Resilienza: circuit breaker e retry»?

La maggior parte delle lezioni CoddyKit richiede circa 5–10 minuti. Ogni lezione è breve e interattiva, quindi fai progressi costanti e riprendi esattamente da dove hai lasciato su web e app.

Posso scrivere ed eseguire codice in questa lezione PHP Academy?

Sì. Ogni lezione PHP Academy include un editor di codice integrato, quindi scrivi ed esegui codice reale direttamente nel tuo browser e ricevi feedback istantaneo dall'IA — nessuna configurazione locale necessaria.

Tutte le lezioni di questo corso

  1. Dal monolite ai microservizi
  2. Comunicazione tra servizi: REST e gRPC
  3. API Gateway e individuazione dei servizi
  4. Resilienza: circuit breaker e retry
← Torna a PHP Academy