0Pricing
PHP Academy · Lekcja

Odporność: circuit breakery i ponowienia

Utrzymuj sprawność systemów podczas częściowych awarii

Odporność: circuit breakery i ponowienia to bezpłatna lekcja PHP Academy na CoddyKit. To lekcja 4 z 4. Możesz przeczytać całą lekcję poniżej za darmo — a potem ćwiczyć ją interaktywnie w przeglądarce z wbudowanym edytorem kodu i tutorem AI dostępnym 24/7. To część ścieżki edukacyjnej PHP Academy, a Twój postęp synchronizuje się między webem a aplikacją CoddyKit. Kurs PHP Academy zawiera 4 lekcji w sumie.

Wzorce odporności

W systemie rozproszonym częściowe awarie są normą — jakaś zależność zawsze działa wolno, uruchamia się ponownie albo jest przeciążona. Odporność polega na projektowaniu każdej usługi tak, aby jedna niesprawna zależność nie pociągnęła za sobą usługi, a następnie całego systemu.

W tej lekcji omówimy podstawowy zestaw narzędzi: timeouty, ponowienia z backoffem, circuit breakery, bulkheady i kontrolowane ograniczanie funkcjonalności — wszystko w PHP.

Najpierw timeouty

Najważniejszym pojedynczym ustawieniem zwiększającym odporność jest timeout. Bez niego wolna usługa downstream blokuje workery PHP-FPM, które czekają; żądania się spiętrzają; kończą się workery; Państwa usługa przestaje działać tylko dlatego, że ktoś inny działał wolno. To podręcznikowy przykład awarii kaskadowej.

W każdym wywołaniu wychodzącym należy zawsze ustawić zarówno timeout nawiązania połączenia, jak i łączny timeout żądania.

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

Ponowienia — ostrożnie

Przejściowe awarie (chwilowy problem, krótkotrwała utrata sieci, 503) często znikają przy drugiej próbie. Ponowienia są jednak niebezpieczne: jeśli będą wykonywane zbyt pochopnie, zwiększą obciążenie już przeciążonej usługi. Zasady:

  • Należy ponawiać tylko operacje idempotentne oraz błędy ponawialne (timeouty, 5xx, 429).
  • Należy ograniczyć liczbę prób.
  • Nigdy nie należy ponawiać błędów klienta 4xx — ponownie zakończą się niepowodzeniem.

Wykładniczy backoff i jitter

Ponowienia wykonywane przez wielu klientów w stałych odstępach synchronizują się, tworząc lawinę żądań, która falami uderza w odzyskującą sprawność usługę. Rozwiązaniem jest wykładniczy backoff (podwajanie opóźnienia przy każdej próbie) połączony z jitterem (losowością), aby rozłożyć obciążenie w czasie.

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

Pętla ponowień

Połączenie wszystkich elementów: ograniczona liczba prób, ponawianie wyłącznie w przypadku błędów ponawialnych, oczekiwanie między próbami z użyciem backoffu i jittera oraz zwrócenie ostatniego błędu, jeśli wszystkie próby zakończą się niepowodzeniem.

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

Circuit breaker

Ponowienia pomagają przy chwilowych problemach, ale jeśli zależność naprawdę nie działa, ponawianie każdego żądania tylko marnuje czas i zasoby. Mechanizm circuit breaker śledzi błędy i po przekroczeniu określonego progu otwiera się — przerywa wywołania i natychmiast zwraca błąd zamiast czekać na niedziałającą usługę.

Ma trzy stany: zamknięty (wywołania są przepuszczane, a błędy zliczane), otwarty (wywołania są natychmiast odrzucane) i półotwarty (kilka próbnych wywołań sprawdza, czy usługa odzyskała sprawność).

Maszyna stanów circuit breakera

Przejścia wyglądają następująco: stan zamknięty → otwarty, gdy liczba błędów przekroczy próg. Otwarty → półotwarty po okresie schładzania. Półotwarty → zamknięty po pomyślnym wywołaniu testowym albo z powrotem otwarty w razie błędu. Stan musi być współdzielony między procesami PHP (Redis/APCu), ponieważ każde żądanie jest obsługiwane przez nowy proces.

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

Bulkheady

Nazwa wzorca bulkhead pochodzi od przegród w statku. Wzorzec ten izoluje zasoby, aby awaria jednego obszaru nie mogła zatopić całej jednostki. Jeśli wszystkie workery mogą wywoływać powolną usługę Reports, awaria Reports może zająć każdego workera i pozbawić zasobów usługę Checkout.

Należy podzielić zasoby — użyć osobnych pul połączeń, osobnych pul workerów lub kolejek dla każdej zależności albo limitów współbieżności dla poszczególnych usług downstream — aby jedna niesprawna zależność wyczerpała tylko przydzielony jej fragment zasobów.

Kontrolowane ograniczanie funkcjonalności i fallbacki

Gdy niekrytyczna zależność jest niedostępna, należy ograniczyć funkcjonalność zamiast zwracać błąd. Można zwrócić wartość z pamięci podręcznej, wartość domyślną albo zapewnić okrojoną wersję działania. Otwarty stan circuit breakera jest naturalnym wyzwalaczem ścieżki fallbacku.

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

Klucze idempotencji do bezpiecznych ponowień

Ponowienia są bezpieczne tylko w przypadku operacji idempotentnych. W przypadku działania nieidempotentnego, takiego jak "obciążenie karty", należy dołączyć klucz idempotencji, którego serwer używa do deduplikacji: jeśli ponowienie nadejdzie po tym, jak pierwsza próba już się powiodła (ale odpowiedź zaginęła), serwer zwróci pierwotny wynik zamiast obciążyć kartę dwukrotnie.

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

Połączenie wzorców

Wzorce można łączyć, a kolejność ma znaczenie. Odporne wywołanie wychodzące zazwyczaj ma następujące warstwy:

  • Timeout dla każdej pojedynczej próby (warstwa najbardziej wewnętrzna).
  • Ponowienie z backoffem opakowujące wywołanie z timeoutem, aby obsłużyć przejściowe problemy.
  • Circuit breaker opakowujący ponowienie, aby szybko zareagować na długotrwałą awarię.
  • Bulkhead ograniczający ilość zasobów, jaką ta zależność może zużyć.
  • Fallback jako warstwa najbardziej zewnętrzna, przechwytująca wszystko, co wydostanie się na zewnątrz.

Istnieją biblioteki oferujące Resilience4PHP-owe wrappery, ale zrozumienie warstw ma większe znaczenie niż wybór konkretnego pakietu.

Szybkie sprawdzenie

Wybór właściwego wzorca.

Podsumowanie

Odporność na częściowe awarie:

  • Timeouty w każdym wywołaniu zapobiegają wyczerpaniu workerów i awariom kaskadowym.
  • Ponowienia tylko dla operacji idempotentnych i błędów ponawialnych, z użyciem wykładniczego backoffu i jittera.
  • Circuit breakery szybko zwracają błąd (zamknięty → otwarty → półotwarty), gdy zależność nie działa.
  • Bulkheady izolują zasoby, aby jedna awaria nie pozbawiła ich wszystkich pozostałych elementów.
  • Kontrolowane ograniczanie funkcjonalności i fallbacki pozwalają zachować działanie kluczowych ścieżek.

Połączone warstwowo wzorce te zamieniają nieuniknione awarie w ograniczone i możliwe do opanowania zdarzenia.

Często zadawane pytania

Czy lekcja „Odporność: circuit breakery i ponowienia” jest bezpłatna?

Tak — pełny tekst „Odporność: circuit breakery i ponowienia” jest dostępny za darmo tutaj w sieci. Aby ćwiczyć ją interaktywnie (wbudowany edytor kodu i tutor AI dostępny 24/7) i odblokować resztę kursu PHP Academy, przejdź na CoddyKit PRO. Kurs PHP Academy zawiera 4 lekcji w sumie.

Co nauczysz się w „Odporność: circuit breakery i ponowienia”?

Utrzymuj sprawność systemów podczas częściowych awarii Ćwiczysz PHP Academy z praktycznym kodem, który uruchamiasz bezpośrednio w przeglądarce, a tutor AI dostępny 24/7 odpowiada na Twoje pytania podczas pracy nad lekcją.

Czy potrzebuję doświadczenia, aby zacząć PHP Academy?

Nie wymagamy żadnego doświadczenia. PHP Academy w CoddyKit jest strukturyzowany dla początkujących i zaawansowanych użytkowników, więc możesz zacząć tutaj lub od początku i uczyć się w swoim tempie. To lekcja 4 z 4.

Ile czasu zajmuje lekcja „Odporność: circuit breakery i ponowienia”?

Większość lekcji CoddyKit trwa około 5–10 minut. Każda lekcja to mały, interaktywny krok, dzięki czemu robisz systematyczne postępy i zawsze wracasz dokładnie do tego samego miejsca — na webie i w aplikacji.

Czy mogę pisać i uruchamiać kod w tej lekcji PHP Academy?

Tak. Każda lekcja PHP Academy zawiera wbudowany edytor kodu, więc piszesz i uruchamiasz prawdziwy kod bezpośrednio w przeglądarce i od razu otrzymujesz sprzężenie zwrotne od AI — bez konfiguracji na komputerze.

Wszystkie lekcje w tym kursie

  1. Od monolitu do mikroserwisów
  2. Komunikacja usług: REST i gRPC
  3. Bramy API i wykrywanie usług
  4. Odporność: circuit breakery i ponowienia
← Powrót do PHP Academy