PHP Academy · レッスン

レジリエンス:サーキットブレーカーとリトライ

部分的な障害が発生してもシステムを健全に保ちます。

レッスン 4/413 ステップ

「レジリエンス:サーキットブレーカーとリトライ」はCoddyKit上の無料PHP Academyレッスンです。 これはレッスン4/4です。 下記で完全なレッスンを無料で読むことができます。その後、ブラウザ内の組み込みコードエディタと24時間対応のAIチューターでハンズオン演習できます。 これはPHP Academy学習パスの一部であり、ウェブとCoddyKitアプリ全体で進捗が同期されます。 PHP Academyコースには全4レッスンが含まれています。

レジリエンスパターン

分散システムでは、部分障害が通常の状態です。依存先のどこかが常に遅延したり、再起動したり、過負荷になったりします。レジリエンスとは、1つの不調な依存先によってサービス自体が停止し、さらにシステム全体まで停止することがないよう、各サービスを設計することです。

このレッスンでは、タイムアウト、バックオフ付きリトライ、サーキットブレーカー、バルクヘッド、グレースフルデグラデーションという、PHPで使える基本ツールを扱います。

まずはタイムアウト

レジリエンスに関する設定の中で、最も重要なのはタイムアウトです。これがないと、遅い下流サービスを待つPHP-FPMワーカーが拘束され、リクエストが滞留し、ワーカーを使い果たします。そして、遅かったのは別のサービスなのに、自分のサービスが停止します。これは典型的なカスケード障害です。

すべての外向き呼び出しに、接続タイムアウトとリクエスト全体のタイムアウトの両方を必ず設定してください。

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

リトライは慎重に

一時的な障害(瞬断、短時間のネットワーク切断、503など)は、2回目の試行で成功することがよくあります。しかし、リトライは危険でもあります。早くリトライしすぎると、すでに苦しい状態にあるサービスへの負荷を増幅してしまいます。ルールは次のとおりです。

  • 冪等な操作と、リトライ可能なエラー(タイムアウト、5xx、429)に対してのみリトライします。
  • 試行回数に上限を設けます。
  • 4xxのクライアントエラーは決してリトライしません。再び失敗するだけだからです。

指数バックオフとジッター

多くのクライアントが固定間隔でリトライすると、タイミングが同期し、回復中のサービスに波状攻撃を仕掛けるThundering Herdが発生します。これを防ぐには、指数バックオフ(試行ごとに待機時間を2倍にする)とジッター(ランダム化)を組み合わせ、負荷を分散させます。

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

リトライループ

ここまでの要素を組み合わせます。試行回数に上限を設け、リトライ可能なエラーの場合だけリトライし、試行間ではジッターを加えたバックオフで待機します。すべての試行が失敗したら、最後の失敗を呼び出し元に返します。

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

サーキットブレーカー

リトライは瞬断には有効ですが、依存先が完全に停止している場合、すべてのリクエストをリトライすると時間とリソースを無駄にします。サーキットブレーカーは失敗を追跡し、失敗がしきい値を超えるとOpen状態になり、呼び出しを短絡させます。停止したサービスを待つのではなく、すぐに失敗させる仕組みです。

状態は3つあります。Closed(呼び出しを通し、失敗を数える)、Open(呼び出しを即座に拒否する)、Half-Open(少数の試行呼び出しで回復を確認する)です。

ブレーカーの状態遷移

状態遷移は次のとおりです。失敗がしきい値を超えるとClosed → Openになります。クールダウン期間の後にOpen → Half-Openになります。試行が成功すればHalf-Open → Closed、失敗すれば再びOpenになります。各リクエストが新しいプロセスで処理されるため、状態はPHPプロセス間で共有しなければなりません(RedisやAPCuを使います)。

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

バルクヘッド

船の隔壁に由来するバルクヘッドパターンは、ある領域の障害が船全体を沈めないように、リソースを分離します。すべてのワーカーが遅いReportsサービスを呼び出せる状態だと、Reportsの障害によって全ワーカーが消費され、Checkoutが処理できなくなる可能性があります。

依存先ごとに別の接続プール、別のワーカープールやキューを用意したり、下流サービスごとに同時実行数を制限したりして、リソースを分割してください。そうすれば、1つの依存先の障害で使い果たされるのは、その依存先に割り当てた範囲だけになります。

グレースフルデグラデーションとフォールバック

重要度の低い依存先が利用できない場合は、エラーにするのではなく機能を縮退させます。キャッシュされた値、デフォルト値、または機能を絞った体験を提供してください。サーキットブレーカーがOpen状態になったことは、フォールバック経路へ切り替える自然なきっかけになります。

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

安全なリトライのための冪等性キー

リトライが安全なのは、冪等な操作だけです。「カードに課金する」のような非冪等な操作では、サーバーが重複排除に使う冪等性キーを付けます。最初の試行がすでに成功していたにもかかわらずレスポンスだけが失われ、その後リトライが届いた場合、サーバーは二重に課金せず、元の結果を返します。

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

全体を組み合わせる

これらのパターンは組み合わせて使えますが、順序が重要です。堅牢な外向き呼び出しは、通常次のように入れ子にします。

  • 各試行にタイムアウトを設定します(最も内側)。
  • 一時的な瞬断に備え、タイムアウト付きの呼び出しをバックオフ付きリトライで包みます。
  • 長時間の障害を素早く検知できるよう、リトライをサーキットブレーカーで包みます。
  • この依存先が消費できる容量をバルクヘッドで制限します。
  • 最も外側にフォールバックを置き、上へ伝播してきたすべてのエラーを受け止めます。

Resilience4PHPのようなラッパーライブラリもありますが、特定のパッケージよりも、この階層構造を理解することが重要です。

クイックチェック

適切なパターンを選ぶこと。

まとめ

部分障害を乗り越える方法:

  • すべての呼び出しにタイムアウトを設定し、ワーカーの枯渇とカスケード障害を防ぎます。
  • リトライは冪等な操作とリトライ可能なエラーに限り、指数バックオフとジッターを使用します。
  • サーキットブレーカーは、依存先が停止したときに素早く失敗させます(Closed → Open → Half-Open)。
  • バルクヘッドはリソースを分離し、1つの障害ですべてがリソース不足にならないようにします。
  • グレースフルデグラデーションとフォールバックによって、主要な処理を継続させます。

これらを組み合わせることで、避けられない障害を、封じ込めて復旧可能な事象に変えられます。

無料で開始

AI チューターと学ぶ PHP — 無料

ブラウザでリアルコードを書いて実行し、24/7 の AI チューターから瞬時にサポートを受け、ウェブまたはアプリで続きから学習できます。

コース
49
レッスン
195

よくある質問

「レジリエンス:サーキットブレーカーとリトライ」レッスンは無料ですか?

はい。「レジリエンス:サーキットブレーカーとリトライ」の完全なテキストはこのウェブで無料で読めます。インタラクティブに演習し(組み込みコードエディタと24時間対応のAIチューター)、PHP Academyコースの残りをアンロックするには、CoddyKit PROにアップグレードしてください。 PHP Academyコースには全4レッスンが含まれています。

「レジリエンス:サーキットブレーカーとリトライ」で何を学びますか?

部分的な障害が発生してもシステムを健全に保ちます。 ブラウザで直接実行するハンズオンコードでPHP Academyを演習し、24時間対応のAIチューターがレッスンを進める中での質問に答えます。

PHP Academyを始めるのに経験は必要ですか?

事前経験は必要ありません。CoddyKitのPHP Academyは初級者から上級者向けに構成されているため、ここから始めるか最初から始めて、自分のペースで進むことができます。 これはレッスン4/4です。

「レジリエンス:サーキットブレーカーとリトライ」レッスンにはどのくらい時間がかかりますか?

ほとんどのCoddyKitレッスンは約5~10分かかります。各レッスンはコンパクトでインタラクティブなので、着実に進歩し、ウェブとアプリ全体で正確に前回の場所から再開できます。

このPHP Academyレッスンでコードを書いて実行できますか?

はい。すべてのPHP Academyレッスンに組み込みコードエディタが含まれているため、ブラウザでリアルコードを書いて実行し、即座のAIフィードバックを取得できます。ローカル設定は不要です。

このコースのすべてのレッスン

  1. モノリスからマイクロサービスへ
  2. サービス間通信:RESTとgRPC
  3. APIゲートウェイとサービスディスカバリ
  4. レジリエンス:サーキットブレーカーとリトライ
← PHP Academyに戻る