こんにちは。大きなトラフィックをさばくWebシステムの設計で、日々格闘されていることと思います。Node.jsやGoの並行処理モデル(GoroutineやEvent Loop)に慣れ親しんだエンジニアほど、PHPの「1リクエスト=1プロセス(またはスレッド)で直列に処理する」という世界観に、どこかどぎまぎしたもどかしさを覚えた経験があるのではないでしょうか。
「PHPでも、IO待ちの時間を無駄にせず、綺麗に非同期パイプラインを回したい」
そう思ったとき、PHP 8.1で導入された Fiber(ファイバー) は、私たちの武器を劇的に変えてくれる起爆剤となります。
今回は、数百万件ものログを捌く「大規模ログ収集・分析システム」を題材に、Fiberを用いた非同期データパイプラインをどう構築するか、そしてそれがZendエンジンやOSのレイヤでどう動いているのかを、一緒に紐解いていきましょう。ここを理解すると、PHPの裏側の景色が驚くほどクリアに美しく見えてきますよ。
—
1. なぜログパイプラインにFiberなのか?(Zend VMとIOのジレンマ)
従来のPHPで大量のログ(例えば、外部APIへの送信、S3へのアップロード、Elasticsearchへのバルクインサートなど)を処理しようとすると、必ず「ネットワークのIO待ち」というボトルネックにぶつかります。
[ログ生成] —> (同期HTTP送信: 200ms待機) —> [次のログへ]
この「200ms待機している時間」、CPUはOSからCPU時間を剥奪され、プロセスはただ寝ているだけになります。マルチプロセス(SwooleやRoadRunner、あるいはFPMの複数プロセス)で物量作戦に出るのも手ですが、メモリ消費量が跳ね上がり、コネクションプールの管理も複雑化します。
ここで Fiber(スタックlessではない、スタックフルな協調的緑色スレッド) の出番です。
Fiberを使えば、PHPの単一プロセス(単一スレッド)の中で、複数の処理を「自発的に中断(Suspend)し、必要なデータやIOの準備ができたら再開(Resume)する」という協調的マルチタスク(Cooperative Multitasking)が実現できます。
Zend VMのスタックフレームとFiberの正体
C言語レベル(Zend VM)で言うと、Fiberは「独自の実行スタック(zend_execute_dataとコールスタック)をヒープ上に独立して保持する仕組み」です。
通常、関数がネストして呼び出されると、Zendエンジンはコールスタックを積み上げていきますが、Fiberを使うことで、その実行コンテキストを丸ごとオブジェクトとして変数に閉じ込め、任意のタイミングで実行を一時停止・再開できるようになります。
つまり、OSのコンテキストスイッチの重いオーバーヘッドを回避しつつ、ユーザーランドのコードだけで擬似的な非同期処理のパイプラインを作れるというわけです。
—
2. アーキテクチャの全体像:非同期データパイプライン
今回構築するログ収集・分析パイプラインは、以下の3つのステージをFiberで並行(インターリーブ)実行します。
1. Producer(収穫ステージ): ログファイルやキューから未処理のログチャンクを読み込む。
2. Processor(前処理ステージ): ログのパース、機密情報のマスキング、構造化を行う。
3. Consumer(出力ステージ): ストレージや外部APIへバッチ単位で非同期フラッシュする。
これらをイベントループとFiberを組み合わせて、あたかも同時に動いているかのように調停します。
—
3. 実装:Fiberによる非同期ログパイプライン
では、実際に動くコードを見ていきましょう。ここでは外部ライブラリに頼りすぎず、Fiberの本質が見えやすいようにプレーンなPHP 8.2+の構文でイベントループとFiberを統合したパイプラインを組みます。
/
class LogPipelineContext
{
/ @var Fiber[] 実行待ち、または中断中のファイバー群 /
private array $fibers = [];
/ @var array 処理待ちのログバッファ /
private array $buffer = [];
private const BATCH_SIZE = 3;
/
- ファイバーをパイプラインに登録する
/
public function addFiber(Fiber $fiber): void
{
$this->fibers[] = $fiber;
}
/
- イベントループの駆動
- すべてのFiberが終了するまで、協調的にタスクを切り替えながら実行し続けます。
/
public function run(): void
{
while (!empty($this->fibers)) {
foreach ($this->fibers as $index => $fiber) {
try {
// まだ開始していなければスタート
if (!-$fiber->isStarted()) {
$fiber->start();
}
// サスペンド中ならレジューム(再開)
elseif ($fiber->isSuspended()) {
$fiber->resume();
}
// ファイバーが完全に終了(Terminated)したらプールから除去
if ($fiber->isTerminated()) {
unset($this->fibers[$index]);
}
} catch (\Throwable $e) {
echo “[Error in Fiber]: ” . $e->getMessage() . “\n”;
unset($this->fibers[$index]);
}
}
// CPUの暴走を防ぎ、イベントループの息継ぎ(模擬的なIO待ちポーリング)
usleep(10000); // 10ms
}
}
public function pushBuffer(array $logData): void
{
$this->buffer[] = $logData;
// バッファが閾値に達したら、外部ストレージへの書き込みを模擬的に発火
if (count($this->buffer) >= self::BATCH_SIZE) {
$chunk = $this->buffer;
$this->buffer = [];
$this->flushToStorageAsync($chunk);
}
}
private function flushToStorageAsync(array $chunk): void
{
// ストレージ書き込み(ネットワークIO)をFiberで非同期化する例
$storageFiber = new Fiber(function () use ($chunk) {
echo “-> [Storage] ” . count($chunk) . “件のログを外部ストレージへ非同期送信中…\n;
// 実際のプロダクションではここで非同期HTTPクライアント(AmpやReactPHPなど)のPromiseを待つ
// 今回は模擬的にIO待ち(sleep)の代わりにFiberをサスペンドさせる
$iterations = 0;
while ($iterations < 3) {
$iterations++;
echo "-> [Storage] IO待ち継続中… ({$iterations}/3)\n”;
Fiber::suspend(); // コントロールをイベントループに返す
}
echo “=> [Storage] ログの書き込みが完了しました。\n”;
});
$storageFiber->start();
// パイプラインの管理下に追加
$this->addFiber($storageFiber);
}
}
// ==========================================
// パイプラインの構築と実行
// ==========================================
$pipeline = new LogPipelineContext();
// 1つ目のファイバー:ログ生成・前処理ワーカー A
$pipeline->addFiber(new Fiber(function () use ($pipeline) {
$rawLogs = [
“User 123 logged in from 192.168.1.10”,
“API Error: Timeout on /v1/checkout”,
“Payment success for order #9982”
];
foreach ($rawLogs as $i => $raw) {
echo “[Producer A] ログをパース中: {$raw}\n”;
// 重い正規表現パースやサニタイズ処理を想定
$parsed = [
‘id’ => ‘log-‘ . uniqid(),
‘message’ => $raw,
‘timestamp’ => time(),
];
// 共有バッファへ投入
$pipeline->pushBuffer($parsed);
// 次のログ処理に移る前に、一度コンテキストを譲る(協調的マルチタスク)
Fiber::suspend();
}
}));
// 2つ目のファイバー:ログ生成・前処理ワーカー B(並行して動く別系統のログ)
$pipeline->addFiber(new Fiber(function () use ($pipeline) {
$rawLogs = [
“Database connection pool exhausted”,
“Cache miss for key: user_profile_88”,
];
foreach ($rawLogs as $i => $raw) {
echo “[Producer B] ログをパース中: {$raw}\n”;
$parsed = [
‘id’ => ‘log-‘ . uniqid(),
‘message’ => $raw,
‘timestamp’ => time(),
];
$pipeline->pushBuffer($parsed);
Fiber::suspend();
}
}));
echo “=== ログ収集・分析パイプラインを開始します ===\n”;
$startTime = microtime(true);
// イベントループ起動
$pipeline->run();
$endTime = microtime(true);
printf(“=== すべての処理が完了しました (実行時間: %.4f 秒) ===\n”, $endTime – $startTime);
—
4. このコードがエンジニアの武器になる理由
上記のコードを実行すると、Producer AとProducer B、そして途中で動的に生み出されるStorage(ストレージ書き込み)のFiberが、互いに綺麗に処理をインターリーブ(交互に実行)しながら進んでいくのが分かります。
ここで重要なのは、「ブロッキングなIOや重い処理の合間に、自発的に `Fiber::suspend()` を挟むことで、PHPという単一スレッドの言語でありながら、無駄な待ち時間を完全に排除できる」という点です。
実務のプロダクション環境では、この自前で作ったイベントループの部分を、定評のある非同期エコシステムである Amphp (v3) や ReactPHP、あるいは高性能なアプリケーションサーバーである Swoole / FrankenPHP の非同期機能と組み合わせることになります。ベースとなるプリミティブ(Fiber)の仕組みさえ頭に入っていれば、どんなフレームワークやライブラリの内部挙動も恐れるに足りません。
—
5. アーキテクトからのメッセージ
PHPは「直感的で書きやすいスクリプト言語」であると同時に、Zendエンジンという洗練された仮想マシンの上に成り立っています。
「PHPだから非同期や並行処理は苦手だ」という神話は、もう過去のものになりつつあります。Fiberという強力なプリミティブを手にいれた現代のPHPにおいて、ボトルネックがどこにあり、メモリとCPUのコンテキストがどう遷移しているのかを低レイヤの視点からイメージできるようになれば、あなたの書くコードは他の追随を許さないほど堅牢で高速なものに生まれ変わります。
ぜひ、日々の開発のなかにFiberの視点を取り入れ、美しくスケーラブルな非同期アーキテクトへの階段を駆け上がってください。応援しています。