【入門編】分散トランザクションにおけるFiberの役割と補償トランザクションの設計パターン – PHPコア・内部エンジンと高速化・並行処理の極意解析バイブル

こんにちは。日々のアーキテクチャ設計やパフォーマンスチューニング、本当にお疲れ様です。

他のモダンなプログラミング言語、例えばGoやNode.js、あるいはJavaなどで非同期処理やリアクティブプログラミングを経験してきた優秀なエンジニアほど、PHPの世界に入った時に「えっ、ここでスレッドがブロックされるの?」「リクエストライフサイクルの中でどうやって並行処理を回すの?」という壁にぶつかりがちですよね。

特に、マイクロサービスアーキテクチャにおける分散トランザクション(Sagaパターンなど)をPHPで実装しようとした時、従来の同期的なブロッキングI/Oのままでは、ネットワークの遅延がそのままWebサーバー全体のボトルネックになってしまいます。

今回は、PHP 8.1で導入された Fiber(ファイバー) を駆使し、単なる「非同期の実現」に留まらず、分散環境における補償トランザクション(Compensating Transaction)を美しく、かつエンジンレベルで効率的に制御する設計の極意を、PHPの裏側の挙動とともに紐解いていきます。

ここを理解すれば、PHPの裏側が驚くほど綺麗に見えるようになりますよ。一緒に深く潜っていきましょう。

—

1. なぜ分散トランザクションにFiberが必要なのか?

マイクロサービスの世界では、ひとつのユーザーリクエスト(例えば「ECサイトの注文確定」)が、以下のような複数の独立したサービスを叩くことになります。

1. 在庫サービス:在庫の引き当て
2. 決済サービス:クレジットカードのオーソリ
3. 配送サービス:配送伝票の発行

モノリスなアプリケーションであればデータベースのACIDトランザクション(`BEGIN` から `COMMIT` / `ROLLBACK`)で一網打尽にできますが、サービス間でDBが分かれている分散環境では、いわゆる「2PC(2相コミット)」はレイテンシの観点や可用性の面から現代のWebアーキテクチャでは敬遠されます。

そこで採用されるのが Sagaパターン です。各サービスのローカル・トランザクションを順番に実行し、もし途中で失敗(例:決済エラー)したら、それまでに成功したサービスに対して逆の操作(補償トランザクション)を非同期かつ迅速に実行して整合性を担保します。

ここで、従来のPHPのように「1つの外部APIを叩くたびにプロセスが完全にブロックされる」書き方をしていると、全体のレイテンシは各サービスの往復時間の「総和」になり、FPMのワーカープールがすぐに枯渇してしまいます。

かといって、コールバック地獄や複雑なPromiseチェーンは、コードの可読性を致命的に破壊します。ここで登場するのが、PHPの低レイヤを拡張する Fiber です。

—

2. PHPエンジンから見た Fiber の正体

まず、PHPの実行モデルにおけるFiberの位置づけを正確に押さえましょう。

PHPは伝統的に「シェアード・ナッシング(Shared-Nothing)」の単一リクエスト実行モデルです。Zendエンジンはコールスタック(C言語の関数コールフレーム)をOSのスレッドや単一のスタック上で直接管理してきました。

Fiberは、この「実行コンテキスト(コールスタック、変数の状態、実行ポインタ)」をユーザーランド(PHPコード側)で完全に独立したオブジェクトとして切り出し、一時停止(Suspend)と再開(Resume)を自在に行えるようにした仕組みです。

[Web Request]
↓
[Event Loop]
│ (Fiber A: 在庫引き当て) ──> [一時停止 / I/O待ち]
│ (Fiber B: 決済処理) ──> [一時停止 / I/O待ち]
│ (Fiber C: 配送準備) ──> [一時停止 / I/O待ち]
↓
[全Fiber完了 or 異常検知して補償トランザクションへ]

OSスレッドを増やすことなく、PHPのプロセス内で数千・数万の協調的マルチタスク(Cooperative Multitasking)を実現できるため、ネットワークI/O(HTTPクライアントなど)の待ち時間を極限まで効率化できるのです。

—

3. 実装:FiberベースのSagaオーケストレーターと補償トランザクション

言葉だけではイメージしにくいと思いますので、実際のPHPコードを見ていきましょう。ここでは、イベントループとFiberを組み合わせ、途中で失敗した際に自動で補償トランザクション(ロールバックの逆順実行)をトリガーするオーケストレーターの設計モデルを示します。

  • 分散トランザクションの各ステップ(正方向と補償方向)をカプセル化するクラス
  • /
    class SagaStep
    {
    public function __construct(
    private string $name,
    private callable $action, // 本番処理
    private callable $compensate // 補償処理(ロールバック用)
    ) {}

    public function getName(): string
    {
    return $this->name;
    }

    public function execute(): mixed
    {
    return ($this->action)();
    }

    public function compensate(): mixed
    {
    return ($this->compensate)();
    }
    }

    /

    • Fiberとイベントループを統括する分散トランザクション・オーケストレーター

    /
    class DistributedSagaOrchestrator
    {
    / @var SagaStep[] /
    private array $steps = [];
    / @var SagaStep[] 成功したステップをスタックとして保持(逆順補償のため) /
    private array $completedSteps = [];

    public function addStep(SagaStep $step): self
    {
    $this->steps[] = $step;
    return $this;
    }

    public function run(): bool
    {
    // 各ステップをFiberでラップして非同期調停を実行する
    foreach ($this->steps as $step) {
    $fiber = new Fiber(function () use ($step) {
    // 実際のネットワークI/OやAPIコールをシミュレート
    // (内部で非同期HTTPクライアントやEvent Loopにsuspendすることを想定)
    echo “[Fiber] 実行中: {$step->getName()}\n”;

    // ここでは分かりやすく同期処理として書いていますが、
    // 非同期I/Oライブラリ(AmpやReactPHP等)と組み合わせることでここでsuspendします
    return $step->execute();
    });

    try {
    // Fiberを開始
    $result = $fiber->start();

    // 実行結果が失敗を表す場合のハンドリング(例として例外をスロー)
    if ($result === false) {
    throw new \RuntimeException(“ステップ {$step->getName()} がビジネスロジックエラーで失敗しました。”);
    }

    // 成功したステップを記録(後で逆順に補償するため)
    $this->completedSteps[] = $step;
    echo “[Saga] 成功: {$step->getName()}\n\n”;

    } \Throwable $e) {
    echo “[エラー検知] {$e->getMessage()}\n”;
    echo “[Saga] 補償トランザクション(ロールバック)を開始します…\n”;

    $this->rollback();
    return false;
    }
    }

    echo “[Saga] すべての分散トランザクションが正常に完了しました。\n”;
    return true;
    }

    private function rollback(): void
    {
    // 成功していたステップを「逆順(LIFO)」に辿って補償トランザクションを実行
    $reverseSteps = array_reverse($this->completedSteps);

    foreach ($reverseSteps as $step) {
    $compensateFiber = new Fiber(function () use ($step) {
    echo “[補償 Fiber] 実行中: {$step->getName()} の取り消し\n”;
    return $step->compensate();
    });

    try {
    $compensateFiber->start();
    echo “[補償完了] {$step->getName()}\n”;
    } \Throwable $e) {
    // 補償トランザクション自体の失敗は、クリティカルなアラート対象(要人手介入)
    echo “[致命的エラー] 補償トランザクションの実行に失敗しました: {$step->getName()} – ” . $e->getMessage() . “\n”;
    }
    }
    }
    }

    // ==========================================
    // 実行シミュレーション
    // ==========================================

    $orchestrator = (new DistributedSagaOrchestrator())
    ->addStep(new SagaStep(
    name: ‘在庫サービス (Inventory)’,
    action: function() {
    // 例:POST /inventory/reserve
    usleep(100000); // ネットワーク遅延をシミュレート (0.1秒)
    return true; // 成功
    },
    compensate: function() {
    // 例:POST /inventory/release (在庫戻し)
    echo ” -> 在庫の引き当てをキャンセルしました。\n”;
    }
    ))
    ->addStep(new SagaStep(
    name: ‘決済サービス (Billing)’,
    action: function() {
    // 例:POST /billing/charge
    usleep(100000);
    // わざとここで失敗させる(例:残高不足や外部APIタイムアウト)
    throw new \RuntimeException(“決済が拒否されました (Card Declined)”);
    },
    compensate: function() {
    // 例:POST /billing/refund
    echo ” -> 決済の返金処理を行いました。\n”;
    }
    ))
    ->addStep(new SagaStep(
    name: ‘配送サービス (Shipping)’,
    action: function() {
    // 決済が失敗するため、ここは実行されないはず
    return true;
    },
    compensate: function() {
    echo ” -> 配送手配をキャンセルしました。\n”;
    }
    ));

    // トランザクション実行
    $orchestrator->run();

    実行結果のイメージ

    [Fiber] 実行中: 在庫サービス (Inventory)
    [Saga] 成功: 在庫サービス (Inventory)

    [Fiber] 実行中: 決済サービス (Billing)
    [エラー検知] 決済が拒否されました (Card Declined)
    [Saga] 補償トランザクション(ロールバック)を開始します…
    [補償 Fiber] 実行中: 在庫サービス (Inventory) の取り消し
    -> 在庫の引き当てをキャンセルしました。
    [補償完了] 在庫サービス (Inventory)

    —

    4. アーキテクトが知るべき「メモリとイベントループ」の深い話

    ここで一歩踏み込んで、Zend VMのメモリ空間とFiberの挙動についてエンジニアとして知っておくべき核心に触れましょう。

    1. メモリリークの温床にしないためのスタック管理
    Fiberが生成されると、PHPの内部では `zend_fiber` 構造体がヒープ上にアロケートされ、専用のスタック領域が確保されます。もしFiber内で巨大なオブジェクトやデータベースのPDOコネクションをスコープ内に保持したまま `Fiber::suspend()` を長期化させると、ガベージコレクション(GC)の対象外となり、メモリフットプリントが跳ね上がります。分散トランザクションを設計する際は、Fiber内に保持する変数は「軽量なDTOや識別子(ID)」に絞り込み、重いリソースは常に外側でプール・管理するべきです。

    2. 真の非同期I/Oとの統合
    上記のサンプルコードでは分かりやすさのために `usleep()` や同期的な例外を使いましたが、実プロダクション環境(Swoole, OpenSwoole, あるいは Revoltをベースにしたイベントループ)では、HTTPリクエストを送信した瞬間に `Fiber::suspend()` を呼び出し、C10K問題(同時接続数1万の壁)を軽々とクリアしながら、イベントループ側でレスポンスの到着を待ち受けます。レスポンスが返ってきたら `Fiber::resume($response)` で即座に処理を再開させます。

    3. 補償トランザクションの冪等性(Idempotency)
    分散システムにおいて、ネットワークの分断やタイムアウトは「起きるもの」として設計しなければなりません。補償トランザクション(ロールバック)が何らかの理由で二重実行されたときのために、各サービス側のAPIは必ず冪等(Idempotent)である必要があります。FiberとSagaオーケストレーターは「どの順番で何を取り消すべきか」を綺麗に整えてくれますが、ネットワークの向こう側の世界を安全に保つのは、一貫したUUIDやリクエストヘッダによる冪等性担保の設計思想です。

    —

    5. おわりに

    PHPにおけるFiberの登場は、かつての「リクエスト毎に使い捨てる泥臭いスクリプト言語」という古いPHPのイメージを完全に覆しました。

    イベントループとFiberを組み合わせ、今回解説したようなSagaパターンによる分散トランザクションを構築すれば、JavaやGoのマイクロサービス群と何ら遜色のない、高スループットかつ堅牢な非同期WebシステムをPHPで美しく書き上げることができます。

    「PHPだから非同期や分散トランザクションの制御が難しい」というのは、もはや昔の誤解です。エンジニアのあなたがエンジン内部のメモリと実行フローを正確に把握していれば、PHPは驚くほど素直に、そして高速にあなたの要求に応えてくれます。

    ぜひ、次の設計の引き出しにこのFiberと補償トランザクションのパターンを取り入れてみてください。あなたの書くコードが、さらに洗練されたものになることを確信しています。

    タイトルとURLをコピーしました