>100 Views
March 20, 26
スライド概要
Laravel OctaneはFrankenPHPを どう高速化しているのか? ソースコードから読み解く、高速化の仕組み 1
Laravel Octane(オクタン)とは Laravelの常駐実行を実現する拡張パッケージ 主な価値: 起動済みアプリの再利用 対応ランタイム: Swoole / RoadRunner / FrankenPHP ※ モダンWebサーバーの簡単セットアップも提供する 2
Octaneの性能メリット PHP-FPM と Octane の比較 Laravelを毎回起動するか 起動済み状態を再利用するか 結果として 毎回のブートコスト が変わる 3
PHP-FPM Octane 毎回 bootstrap からアプリを初期化 諸々の設定読込やルート解決も毎回 やり直す 初回の起動した Laravel アプリをワ ーカーが保持 2回目以降は ブートを省ける 4
速さの秘訣は起動済みアプリケーションのキャッシュ 起動済みアプリをどこに保持し、どう安全に使い回すか php artisan octane:frankenphp サーバー起動コマンドから追っていきます 5
自己紹介 発表者 ma@me 所属 最近の業務 不具合分析 不具合対応 品質改善業務をメインに色々 やってます。最近はNewRelic が友達 6
閑話休題 改めて。起動済みアプリをどこに保持し、どう安全に使い回すか php artisan octane:frankenphp サーバー起動コマンドから追っていきます 8
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() ↓ レスポンス → 次を待つ ↺ 9
コード解説対象のフロー 起動時(1回のみ) bin/frankenphp-worker.php OCTANE + FRANKENPHP 起動時(1回のみ) ↓ bin/frankenphp-worker.php ↓ Laravel bootstrap → Worker::$app に保持 10
bin/frankenphp-worker.php のフロー 起動時(1回のみ) │ ├─ Worker インスタンス生成 │ new Worker(new ApplicationFactory(...), new FrankenPhpClient()) │ ├─ Laravel 起動・保持 │ $worker->boot() → Worker::$app に保存 │ └─ リクエストループ(while) │ ├─ frankenphp_handle_request() でリクエスト受信 │ ├─ $worker->handle() → clone → 処理 → flush │ └─ レスポンス返却 → ↺ 次を待つ 終了時: $worker->terminate() 11
octane:frankenphp 実行直後
各スレッドで frankenphp-worker.php が読み込まれる
new Worker new ApplicationFactory($basePath), new FrankenPhpClient());
boot
$worker =
(
// Laravel を1回だけ起動
$worker->
();
// リクエストループ
// clone → 処理 → flush
(
・・・
while
frankenphp_handle_request(...) {
$worker->handle($request, $context);
})
);
$worker->
terminate();
12
StartFrankenPhpCommand の役割
StartFrankenPhpCommand::handle() が順に実行される
public function handle(...): int
{
$this->ensureFrankenPhpWorkerIsInstalled(); // public/ にコピー
$this->ensureFrankenPhpBinaryIsInstalled(); // バイナリ確認
// 環境変数でワーカー数や最大リクエスト数を渡す
$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
13
コード解説対象のフロー Laravel bootstrap → Worker::$app に保持 OCTANE + FRANKENPHP 起動時(1回のみ) ↓ bin/frankenphp-worker.php ↓ Laravel bootstrap → Worker::$app に保持 14
起動済みアプリの保存場所 Worker::$app
bin/frankenphp-worker.phpの主要箇所
Worker::boot() で、起動済みアプリをワーカーのプロパティに保持
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));
}
15
OCTANE + FRANKENPHP リクエストごと(繰り返し) frankenphp_handle_request() 受信 ↓ Worker::handle() — clone → 処理 → flush() ↓ レスポンス → 次を待つ ↺ 16
リクエスト毎の使いまわし方 clone + flush
1度保存した$this->app(起動済みアプリ)を使い回すために、
毎回サンドボックスを複製して、次のリクエストを待つ
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); // 元のクリーンな状態に戻す
}
}
17
要注意ポイント clone を見て警戒した方は正解 clone $this->app $sandbox->flush(); $sandbox = 18
落とし穴①:ステート汚染 clone はシャローコピーで、ネストしたオブジェクトは参照が共有される。 flush() が解放するのはサンドボックスのバインディングのみ // サービスプロバイダーでシングルトン登録 $this->app-> ( ::class, fn() => singleton SomeService new SomeService()); // リクエスト処理中に状態を書き換えると… $service = ( ::class); $service->data[] = $request-> ('item'); // ← $this->app 側にも影響する app SomeService input staticで不定なRequestなどのユーザー入力値を扱う場合は要注意 19
落とし穴②:リクエスト跨ぎのメモリリーク flush() はstatic プロパティ・グローバル変数をリセットしない Worker が長寿命なほど消費メモリが増加する // static プロパティへの蓄積 class EventCollector { public static array $log = []; public static function record(string $event): void { self::$log[] = $event; // リクエストごとに積み上がる } } 対策: MAX_REQUESTS(デフォルト 500)でワーカーを定期再起動 20
これが `octane:frankenphp` 実行から 起動済みアプリケーションを保持するまでの流れ 21
FrankenPHPの仕組み 22
Caddy(Go) と PHP Worker の繋ぎ方 実行 入口 Caddy / Go HTTP リクエスト を受ける 橋渡し → CGo → frankenphp_handle_request() PHP Worker Laravel を処理し てレスポンスを返 す 受信は Go、アプリ実行は PHP Worker、その接続点が CGo / frankenphp_handle_request() 23
ZTS(Zend Thread Safety)で動作する マルチスレッドな Go のスレッド上で PHP Worker を動かすため、ZTS(Zend Thread Safety)版 で動作する ここが PHP-FPM や Swoole との差異 ZTS 版ではキャッシュ機能に制限事項が生まれる 24
前提:ZTS ではスレッド間のメモリが分離される WORKER #2 (スレッド) WORKER #1 (スレッド) PHP HEAP ( ZMM ) PHP HEAP ( ZMM ) OctaneArrayStore [ 'site-config' → {...} ] 参照不可 OctaneArrayStore [ ] ← Worker #1 の値は見 えない ZTS は各スレッドに独立したメモリプール(TSRM)を持つ OctaneArrayStore の格納先は PHP のネイティブな配列(スレッドローカ ル) 25
Cache::store('octane') の範囲制限 Octane が開発者向けに提供するオプション機能 リクエスト越しに繰返し使う値をワーカー内でキャッシュするための仕組み // 例: DBから取る設定値をワーカーメモリにキャッシュ $config = :: ('octane')-> ('site-config', 60, fn() => :: () ); Cache store SiteConfig first remember 26
Cache::store('octane') の保存先 ZTS のスレッドローカルメモリの制約により、Cache::store('octane') は スレッドローカルな値になる Cache::store('octane')->put(...) ↓ Swoole FrankenPHP ↓ ↓ OctaneStore OctaneArrayStore ↓ ↓ Swoole\Table PHP array プロセス間共有メモリ スレッドローカル 27
スレッド間共有メモリ:Swoole vs FrankenPHP 共有メモリの仕組み Cache::store('octane') の Swoole FrankenPHP Swoole\Table (プロセス間共 有メモリ) なし(スレッドローカルの ArrayStore) OctaneStore (Swoole\Table OctaneArrayStore (PHPの 実体 経由) array) ワーカー間でキャッシュ共 有 ○ できる ✗ できない 28
ワーカー間共有のもう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');
29
Octane Tables と FrankenPHP Swoole Octane Tables 共有の仕組 み ✅ 利用可能 Swoole\Table (プロセス間共有メ モリ) FrankenPHP ✗ 利用不可 ZTS のスレッドローカルメモリのため 実現不可 Swoole 使用時、Worker間で任意のデータを共有できる FrankenPHP 使用時、任意のデータをWorker間で共有したい場合 Redis / APCu など外部ストアの代替手段が必要 30
比較まとめ:PHP-FPM vs Octane PHP-FPM Octane アプリ起動 リクエストごとに毎回 bootstrap 起動済みを Worker が保持・再利用 ブートコスト 毎回発生 初回のみ メモリ分離 プロセスごとに独立 clone + flush() でサンドボックス ステート汚染リス ク なし(毎回初期化) あり(シャローコピーの参照共有) メモリリークリス ク なし(毎回破棄) あり(static / グローバル変数が蓄 積) 処理 31
比較まとめ:Swoole vs FrankenPHP Swoole FrankenPHP 実行モデル マルチプロセス ZTS マルチスレッド(Go + CGo) Cache::store('octane') の実 体 OctaneStore ( Swoole\Table ) OctaneArrayStore (PHP ワーカー間キャッシュ共有 ○ できる Octane Tables ✅ 利用可能 ワーカー間共有の代替 Swoole\Table / Octane Tables array) ✗ できない(スレッドローカ ル) ✗ 利用不可 Redis / APCu など外部ストア 32
パフォーマンス計測 33
計測環境・条件 環境 計測条件 メモリ 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
warm 計測結果(ルート: /) warm = ワーカー起動済み・定常状態 p95 で約 60倍、スループットで約 78倍 高速 PHP-FPM + Nginx Octane (FrankenPHP) p95 896 ms 15 ms 平均 881 ms 10 ms RPS 22 1,745 35
cold 計測結果(ルート: /) cold = 初回起動 〜 起動済みになるまでの過渡状態 cold でも p95 で約 8倍、スループットで約 46倍 高速 PHP-FPM + Nginx Octane (FrankenPHP) p95 449 ms 58 ms 平均 230 ms 5 ms RPS 4 201 * Octane の p95 が大きめなのは、初回 boot(200ms)を受けるため 36
導入に有利な条件 ボトルネックにLaravelブートが含まれる 高並列APIなどで低レイテンシ要求が強い 常駐ワーカー前提の設計・運用ができる 要検討 SLOをすでに満たしている ボトルネックがDBや外部API 先にPHP-FPM側の改善余地が大きい 37
まとめ Octaneは要件が合えば強い メインの価値は起動ブートの削減 導入判断は性能課題と運用前提の両方で決める 38
ご清聴ありがとうございました 39