こんにちは。他の言語で非同期処理やコルーチンをバリバリ書いてきた君なら、PHPの世界に入ったとき、こう感じたことがあるはずです。
「なぜPHPは、外部APIへのリクエストでこうも簡単にブロッキングされるのだろう?」と。
Node.jsのイベントループや、Goのゴルーチン、PythonのAsyncioに慣れた頭にとって、同期的なHTTPクライアントがI/O待ちでプロセス全体をフリーズさせる姿は、少し前時代的に映るかもしれません。しかし、PHP 8.1で導入された Fiber(ファイバー) によって、その世界観は根本から覆りました。
今回は、数あるFiberの活用シーンの中でも、特に実践的な「Fiberを用いたサーキットブレーカーパターン」をテーマに、PHPの内部エンジンがどう動き、どうやって高可用なシステムを作り上げるのかを、一緒に紐解いていきましょう。
ここを深く理解すれば、PHPの裏側が驚くほど美しく見えてきますよ。
—
1. なぜPHPにFiberが必要だったのか?(Zend VMとI/Oの呪縛)
まず、私たちが普段書いているPHPコードが、サーバー(PHP-FPM)上でどう実行されているかを少しだけ低レイヤの視点から思い返してみましょう。
1リクエストが来ると、Zendエンジンが動き出し、コードはオペコード(Opcode)にコンパイルされ、上から下へと実行されていきます。途中で外部API(例えば、決済ゲートウェイやレガシーなマイクロサービス)を叩くと、CURLなどの関数がカーネルのネットワークソケットを叩き、レスポンスが返ってくるまでそのプロセス(またはスレッド)は完全に停止(ブロッキング)します。
もしその外部サービスが死んでいたら?
PHP-FPMのワーカープロセスはタイムアウト(数秒〜数十秒)までスリープし続け、あっという間にプロセスプールが枯渇。Webサイト全体が「504 Gateway Time-out」の海に沈むことになります。
Fiberがもたらす「スタック管理の主導権」
他の言語では「非同期関数(`async/await`)」がこれを解決しますが、PHPのFiberは、言語機能として「独自のコールスタックを持つ軽量なサブルーチン」を提供します。
[メインコンテキスト] –(中断/Suspend)—> [Fiber内部 (I/O待ち)]
^ |
|—(再開/Resume: イベントループ)——–+
Fiberを使えば、「特定の処理を途中で一時停止し、外側のイベントループに制御を戻す。そして外部サービスの準備ができたら、中断した正確な位置から処理を再開する」という協調的マルチタスク(Cooperative Multitasking)が、PHP単体で書けるようになります。
—
2. サーキットブレーカーパターンとは何か?
外部依存の障害からシステムを守るための鉄則、それが「サーキットブレーカー(回路遮断器)」です。家庭のブレーカーを思い浮かべれば一目瞭然ですね。
サーキットブレーカーには3つの状態があります。
1. Closed(通常・閉): すべてのリクエストを外部サービスに送る。エラー率が閾値を超えると、自動的に「Open」に切り替わる。
2. Open(遮断・開): 外部サービスには一切リクエストを送らず、即座にフォールバック(代替処理やエラーレスポンス)を返す。システム全体が連鎖的に死ぬのを防ぐ。
3. Half-Open(半開・試験): 一定時間経過後、本当に回復したかを確認するため、数件だけリクエストを通してみる。成功すれば「Closed」に戻し、失敗すれば再び「Open」にする。
これをFiberの非同期コンテキスト上で巧みに動かすことで、「重い外部APIの障害待ちでイベントループ全体を止めない、極めてレジリエントな(回復力の高い)アーキテクチャ」が完成します。
—
3. 実装:Fiberベースのサーキットブレーカー
百聞は一見に如かず。実際に動くコードを見てみましょう。ここでは、シンプルなイベントループとFiberを組み合わせ、外部API呼び出しを監視するクラス群を構築します。
/
enum CircuitState: string {
case CLOSED = ‘closed’; // 正常稼働
case OPEN = ‘open’; // 遮断中
case HALF_OPEN = ‘half_open’; // 試験的復旧中
}
/
- サーキットブレーカー本体
/
class CircuitBreaker {
private CircuitState $state = CircuitState::CLOSED;
private int $failureCount = 0;
private float $lastFailureTime = 0.0;
public function __construct(
private readonly int $failureThreshold = 3, // 何回失敗したら開くか
private readonly float $recoveryTimeout = 5.0 // 開いてから再試行までの秒数
) {}
public function getState(): CircuitState {
// Open状態から一定時間経っていたら、Half-Openに移行する
if ($this->state === CircuitState::OPEN && (microtime(true) – $this->lastFailureTime) >= $this->recoveryTimeout) {
$this->state = CircuitState::HALF_OPEN;
}
return $this->state;
}
public function recordSuccess(): void {
$this->failureCount = 0;
$this->state = CircuitState::CLOSED;
}
public function recordFailure(): void {
$this->failureCount++;
$this->lastFailureTime = microtime(true);
if ($this->failureCount >= $this->failureThreshold) {
$this->state = CircuitState::OPEN;
}
}
}
Fiberと組み合わせた非同期実行レイヤ
次に、このサーキットブレーカーを組み込み、Fiberを使ってノンブロッキングに外部サービスを叩くクライアントを作ります。
class ResilientApiClient {
public function __construct(
private readonly CircuitBreaker $circuitBreaker,
private readonly string $serviceName
) {}
/
- Fiber上で安全に外部サービスを呼び出す
/
public function call(array $payload, callable $fallback): mixed {
$state = $this->circuitBreaker->getState();
// 1. Open状態なら、外部を叩かずに即座にフォールバックを返す(高速な失敗)
if ($state === CircuitState::OPEN) {
echo “[CircuitBreaker] {$this->serviceName} は遮断中(OPEN)です。フォールバックを実行します。\n”;
return $fallback($payload);
}
// 2. Fiberを使って非同期(モック)リクエストを実行
$fiber = new Fiber(function() use ($payload) {
echo “[Fiber] {$this:serviceName} へリクエスト送信中…\n”;
// 実際のプロダクションではここで Revolt などのイベントループや
// 非同期 HTTP クライアント (Amp/HttpClient や Guzzleの非同期など) と連携します。
// ここでは擬似的にI/O待ち(スリープ)を表現します。
Fiber::suspend(‘io_wait’);
// 擬似的な障害シミュレーション(特定のペイロードで失敗させる)
if (isset($payload[‘fail’]) && $payload[‘fail’] === true) {
throw new \RuntimeException(“外部APIサーバーエラー”);
}
return “APIからの成功レスポンス”;
});
// Fiberの初回実行
$status = $fiber->start();
if ($status === ‘io_wait’) {
// ここでイベントループが他の処理を挟む余地が生まれる
// 擬似的にネットワークの往復時間をシミュレート
usleep(100000); // 100ms
}
try {
// Fiberの処理を再開(Resume)して結果を受け取る
$result = $fiber->resume();
// 成功時の処理
if ($state === CircuitState::HALF_OPEN) {
$this->circuitBreaker->recordSuccess();
echo “[CircuitBreaker] {$this->serviceName} が回復しました (CLOSEDへ移行)\n”;
}
return $result;
} \catch (\Throwable $e) {
// 失敗時の処理
$this->circuitBreaker->recordFailure();
echo “[Exception Caught] {$e->getMessage()}. 失敗回数カウントアップ。\n”;
// 失敗時はフォールバックに逃げる
return $fallback($payload);
}
}
}
—
4. なぜこの設計が美しいのか?(アーキテクチャ的視点)
君がもしこのコードを実際のプロジェクトに組み込んだなら、以下のメリットに気づくはずです。
1. メモリとCPUの効率的な調停:
PHPのFiberは、C言語レベルでコールスタック(`zend_execute_data` 等の文脈)を別のヒープ領域に退避・復元します。余計なスレッドをOSレベルで生成しないため、C10K問題に代表される大量同時リクエストの環境下でも、PHPプロセスは軽快に動作し続けます。
2. 「Fail Fast(素早い失敗)」の徹底:
外部APIが完全に沈んでいるとき、サーキットブレーカーがOpen状態であれば、Fiberを起動するコストすら払わずに数マイクロ秒でフォールバックを返せます。これにより、Webサーバーのコネクションプールが枯渇する悪夢を防げます。
3. コードの可読性と非同期の同居:
コールバック地獄になりがちな非同期コードですが、Fiberの `suspend()` と `resume()` をラップすることで、「同期的な見た目を保ちつつ、内部で美しくコンテキストをスイッチする」という、極めてメンテナンス性の高いコードベースを維持できます。
—
5. 実務におけるさらなる高みへ
この実装はあくまで概念実習ですが、本番環境(Production)で運用する際は、以下のコンポーネントと組み合わせることで真価を発揮します。
- イベントループライブラリ: `Revolt` や `Amp` といった、PHPエコシステムのモダンな非同期イベントループとFiberをネイティブに統合する。
- 状態の共有(Shared State): PHP-FPMは原則リクエストごとにプロセスが独立しているため、サーキットブレーカーの状態(`CLOSED`, `OPEN`)は、APCuやRedisなどのインメモリキャッシュに保存し、複数プロセス間で共有する必要があります。
PHPは「単なるスクリプト言語」から、こうした高度な並行処理パターンを优雅に実装できる「堅牢なシステム言語」へと進化しました。Fiberを味方につけた君のアーキテクチャは、どんな過酷なトラフィックや外部障害をも軽やかにいなす、極めてレジリエントなシステムへと生まれ変わるはずです。
さあ、次のデプロイでは、この知見をあなたのコードに落とし込んでみてください。新しいPHPの景色が、きっとそこに見えるはずです。