Laravel OctaneはFrankenPHPをどう高速化しているのか?ソースコードから読み解く、高速化の仕組み

1K Views

March 20, 26

スライド概要

シェア

またはPlayer版

埋め込む »CMSなどでJSが使えない場合

ダウンロード

関連スライド

各ページのテキスト
1.

Laravel OctaneはFrankenPHPを どう高速化しているのか? ソースコードから読み解く、高速化の仕組み 1

2.

Laravel Octane(オクタン)とは Laravelの常駐実行を実現する拡張パッケージ 主な価値: 起動済みアプリの再利用 対応ランタイム: Swoole / RoadRunner / FrankenPHP これらの簡単セットア ップも提供する 2

3.

対応ランタイムについて ランタイム 概要 Swoole PHP拡張。マルチプロセスで動作する非同期ランタイム RoadRunner Go製のPHPアプリケーションサーバー FrankenPHP Go製WebサーバーCaddy上で動くPHPランタイム このスライドでは FrankenPHP を中心に解説します 3

4.

Octaneの性能メリット PHP-FPM と Octane の比較 Laravelを毎回起動するか 起動済み状態を再利用するか 結果として 毎回のブートコスト が変わる 4

5.

Laravelのブート処理概要 これが毎回のブートコストにかかってくる サービスコンテナの初期化 設定ファイル(.env / config/)の読み込み サービスプロバイダの登録・起動(register / boot) ルーティング定義の読み込み ミドルウェアスタックの構築 ファサード・エイリアスのバインド 5

6.

PHP-FPM Octane 毎回 bootで アプリを初期化 諸々の設定読込やルート解決も毎回 やり直す 初回の起動した Laravel アプリをワ ーカーが保持 2回目以降は ブートを省ける 6

7.

速さの秘訣は起動済みアプリケーションのキャッシュ 起動済みアプリをどこに保持し、どう安全に使い回すか php artisan octane:frankenphp サーバー起動コマンドから追っていきます 7

8.

自己紹介 発表者 ma@me 所属 最近の業務 不具合分析 不具合対応 品質改善業務をメインに色々 やってます。最近はNewRelic が友達 8

9.

閑話休題 改めて。起動済みのLaravelをどこに保持し、どう安全に使い回すか php artisan octane:frankenphp サーバー起動コマンドから追っていきます 9

10.

octane:frankenphpの起動フロー Laravelの起動処理を受け持つindex.phpが差し代わる PHP-FPM OCTANE + FRANKENPHP リクエスト 起動時(1回のみ) ↓ 毎回 ↓ public/index.php bin/frankenphp-worker.php ↓ ↓ Laravel bootstrap Laravel bootstrap → Worker::$app に保持 ↓ リクエストごと(繰り返し) レスポンス frankenphp_handle_request() 受信 ↓ Worker::handle() — clone → 処理 → flush() ↓ レスポンス → 次を待つ ↺ 10

11.

起動時のコード解説 OCTANE + FRANKENPHP 起動時(1回のみ) ↓ bin/frankenphp-worker.php ↓ Laravel bootstrap → Worker::$app に保持 11

12.
[beta]
octane:frankenphp 実行時の処理

StartFrankenPhpCommand::handle() が順に実行される

public function handle(...): int
{

・・・
// 環境変数でワーカー数や最大リクエスト数を渡す
$server =
(['frankenphp', 'run', '-c', 'Caddyfile'], env: [
'CADDY_SERVER_WORKER_COUNT' => $this->
(),
'MAX_REQUESTS'
=> $this->
('max-requests'),
// ここに環境変数が連なる
]);

new Process

workerCount
option

// Caddy と PHP Worker を起動して繋げる
$this->
($server, ...);
}

return

runServer

12

13.
[beta]
ワーカースレッド起動後の処理(frankenphpworker.php)
// bin/frankenphp-worker.php

new Worker new ApplicationFactory($basePath), new FrankenPhpClient());
boot

$worker =
(
// Laravel を1回だけ起動して保持
$worker->
();

// リクエストループ
// clone → 処理 → flushの繰り返しs
(
・・・
(...) {
// アプリのclone・リクエスト処理・レスポンス送信
$worker->
($request, $context);
})
);

while
frankenphp_handle_request
handle
$worker->

terminate();
13

14.
[beta]
Worker::boot() の実装

$worker->boot() の中身。起動済みアプリを $app プロパティに保持

class Worker implements WorkerContract
{
protected $app; // ここに保持される

// ここで$appに起動済みのアプリを保持する
(
$initialInstances = []):
{
$this->app = $app = $this->appFactory->
(
($initialInstances, [
::class => $this->client])
);

public function boot array
array_merge

$this->
}

void
createApplication
Client

dispatchEvent($app, new WorkerStarting($app));

}

14

15.

OCTANE + FRANKENPHP リクエストごと(繰り返し) frankenphp_handle_request() 受信 ↓ Worker::handle() — clone → 処理 → flush() ↓ レスポンス → 次を待つ ↺ 15

16.
[beta]
リクエスト毎の使いまわし方 clone + flush
//Woker::handle() の実装から抜粋

public function handle(Request $request, RequestContext $context): void
{

// $this->app を clone して使い捨て用の作業コピー(sandbox)を作る
// → 処理中に状態が変わっても $this->app は汚れない
::
($sandbox =
$this->app);

CurrentApplication set
clone
$gateway = new ApplicationGateway($this->app, $sandbox);
try {
$response = $gateway->handle($request);
// ...
} finally {
$sandbox->flush(); // sandbox を後始末して捨てる
CurrentApplication::set($this->app); // 元のクリーンな状態に戻す
}
}

16

17.

毎回後始末もされているので、クリーンで安全...? 17

18.

要注意ポイント clone を見て警戒した方は正解 clone $this->app $sandbox->flush(); $sandbox = 18

19.

落とし穴①:ステート汚染 clone はシャローコピーで、ネストしたオブジェクトは参照が共有される。 flush() が解放するのはサンドボックスのバインディングのみ // サービスプロバイダーでシングルトン登録 $this->app-> ( ::class, fn() => singleton SomeService new SomeService()); // リクエスト処理中に状態を書き換えると… $service = ( ::class); $service->data[] = $request-> ('item'); // ← $this->app 側にも影響する app SomeService input staticで不定なRequestなどのユーザー入力値を扱う場合は要注意 19

20.

落とし穴②:リクエスト跨ぎのメモリリーク flush() はstatic プロパティ・グローバル変数をリセットしない Worker が長寿命なほど消費メモリが増加する // static プロパティへの蓄積 class EventCollector { public static array $log = []; public static function record(string $event): void { self::$log[] = $event; // リクエストごとに積み上がる } } 対策: MAX_REQUESTS(デフォルト 500)でワーカーを定期再起動 20

21.

これが `octane:frankenphp` 実行から 起動済みアプリケーションを保持するまでの流れ 21

22.

FrankenPHPの仕組み 22

23.

Caddy(Go) と PHP Worker の繋ぎ方 実行 入口 Caddy / Go HTTP リクエスト を受ける 橋渡し → CGo → frankenphp_handle_request() PHP Worker Laravel を処理し てレスポンスを返 す 受信は Go、アプリ実行は PHP Worker、その接続点が CGo / frankenphp_handle_request() 23

24.

FrankenPHP上でのPHPはZend Thread Safety (ZTS)で動作する マルチスレッドな Go のスレッド上で PHP Worker を動かすため、ZTS(Zend Thread Safety)版 で動作する ここが PHP-FPM や Swoole との差異 ZTS 版ではOctane提供のキャッシュ機能に制限事項が生まれる 24

25.

前提:ZTS ではスレッド間のメモリが分離される WORKER #2 (スレッド) WORKER #1 (スレッド) PHP HEAP ( ZMM ) PHP HEAP ( ZMM ) OctaneArrayStore [ 'site-config' → {...} ] 参照不可 OctaneArrayStore [ ] ← Worker #1 の値は見 えない ZTS は各スレッドに独立したメモリプール(TSRM)を持つ OctaneArrayStore の格納先は PHP のネイティブな配列(スレッドローカ ル) 25

26.

Cache::store('octane') の範囲制限 Octane が開発者向けに提供するオプション機能 リクエスト越しに繰返し使う値をワーカー内でキャッシュするための仕組み // 例: DBから取る設定値をワーカーメモリにキャッシュ $config = :: ('octane')-> ('site-config', 60, fn() => :: () ); Cache store SiteConfig first remember 26

27.

Cache::store('octane') の保存先 ZTS のスレッドローカルメモリの制約により、Cache::store('octane') は スレッドローカルな値になる Cache::store('octane')->put(...) ↓ Swoole FrankenPHP ↓ ↓ OctaneStore OctaneArrayStore ↓ ↓ Swoole\Table PHP array プロセス間共有メモリ スレッドローカル 27

28.
[beta]
ワーカー間共有のもう1つの手段:Octane Tables
Swoole の Swoole\Table を Octane から扱いやすくしたもの
ワーカー間で共有できる固定サイズのメモリテーブル
config/octane.php で事前定義して使う
// config/octane.php
'tables' => [
'example:1000' => [
'name' => ['type' =>
'votes' => ['type' =>
],
],

Table::TYPE_STRING, 'size' => 1024],
Table::TYPE_INT],

// 利用時
::
$row =

Octane table('example')->set('row-1', ['name' => 'Alice', 'votes' => 5]);
Octane::table('example')->get('row-1');
28

29.

Octane Tables と FrankenPHP Swoole Octane Tables 共有の仕組 み ✅ 利用可能 Swoole\Table (プロセス間共有メ モリ) FrankenPHP ✗ 利用不可 ZTS のスレッドローカルメモリのため 実現不可 Swoole 使用時、Worker間で任意のデータを共有できる FrankenPHP 使用時、任意のデータをWorker間で共有したい場合 APCu(ZTSで同プロセス内共有)や Redis などを利用する 29

30.

⭕️ Octaneのキャッシュ機能がフル活用できない ❌ だからFrankenPHPが遅い FrankenPHPめっちゃ早いよ! (宣伝)どこまで違う?!PHP実行環境パフォーマンス対決 - mod_php vs php-fpm vs Swoole vs FrankenPHP https://www.docswell.com/s/1313108/ZGGREJ-2025-07-17-000927 30

31.

そもそも Octane はプロセスベース向 けに設計された Octane はもともと Swoole・RoadRunner(プロセスモデル)向けに作られ た キャッシュ機構(Cache::store('octane') / Octane Tables)は プロセス間共有メモリを前提に設計されている FrankenPHP 対応は後から追加 → スレッドモデルのため恩恵を受けにくい 31

32.

比較まとめ:Swoole vs FrankenPHP Octaneのキャッシュ機能で差異あり Swoole FrankenPHP 実行モデル マルチプロセス ZTS マルチスレッド(Go + CGo) Cache::store('octane') の実体 OctaneStore ( Swoole\Table ) OctaneArrayStore (PHP Cache::store('octane') のワーカ ○ できる Octane Tables ✅ 利用可能 ワーカー間共有の手段 Swoole\Table / Octane ー間共有 Tables array) ✗ できない(スレッドローカ ル) ✗ 利用不可 APCu(ZTSで同プロセス内 共有)/ Redis 32

33.

パフォーマンス計測 33

34.

計測環境・条件 環境 計測条件 メモリ 16 GB boot 遅延 スタック Laravel Octane (FrankenPHP) + Docker 200ms(両環境共通・意図 的に付加) warm テス ト ab -n 1000 -c 20 ワーカー数 / MAX_REQUESTS 4 / 500 cold テス ト ab -n 20 -c 1 計測ツール ApacheBench 2.3 -n 総リクエスト数 -c 同時並列リクエスト数 34

35.

計測指標の見方 指 標 意味 p50 全リクエストの50%が収まるレスポンスタイム。典型的な応答速度 p95 全リクエストの95%が収まるレスポンスタイム。外れ値を除いた「ほぼ最悪」のケ ース 平 均 全リクエストの平均レスポンスタイム 35

36.

計測結果(ルート: /) PHP-FPM + Nginx Octane (FrankenPHP) 初回起動含む Octane (FrankenPHP) 起動済み p50 218 ms 2 ms 7 ms p95 449 ms 58 ms 15 ms 平均 230 ms 5 ms 10 ms 36

37.

導入に有利な条件 ボトルネックにLaravelブートが含まれる キャッシュを活かせば性能要件を満たせる見込みがある 要検討 ボトルネックがDBや外部APIなど、Laravelブート以外にある場合 キャッシュを活かしても改善見込みがない 37

38.

まとめ Octaneは要件が合えば強い メインの価値は起動ブートの削減 導入判断は性能課題と運用前提の両方で決める 38

39.

ご清聴ありがとうございました 39