低速化で学ぶ Laravel チューニングの勘所

2022-06-29
Laravel

Laravel.shibuya がなんと Final !! お世話になったので、少しでも盛り上げたく、 LT することになりました。このブログポストでは、スライドだけでは分かりづらい内容を少し文章で補足しています。

https://laravel-shibuya.connpass.com/event/250113/

スライド

PHP の設定による低速化

長いこと PHP を扱ってきたプログラマーなら当たり前のことが、最近 PHP を触り始めた人にはわからなかったりします。地味ですが、PHP ウェブアプリケーションの性能を劣化させる方法を4つほど紹介しました。

1. preload 無効化

そもそも、preload を使ってない現場が多いと思いますが、CPU バウンドなウェブアプリケーションであればパフォーマンスに大きな効果を発揮します。そもそも、preload って何よ?っていう方は、拙スライドを参考にどうぞ。

ソースコードから理解するPreloadとJITの話/preload_and_jit - Speaker Deck

2. opcache.jit

PHP 8.0 以降は、JIT 最適化がデフォルトで有効です。こちらも CPU バウンドな処理に効果を発揮します。しつこいようですが、拙スライドで仕組みを解説してますので、わからない方はそちらを参考にしてください。

3. opcache 無効化

自分でコンテナ環境を作る方は、よく分かっていると思いますが、PHP の公式 Docker イメージは、OPCache がそもそもインストールされていません。ご自身で pecl install opcache する必要があります。

これを無効化することで、50%弱の速度劣化を引き起こすことが出来ます。とても効き目がありますので、もし不安のある方は自分のウェブアプリケーションが OPCache が有効な状態で動いていることを確認しましょう。確認方法は簡単です。

1
2
3
4
5
6
7
8
9
$ php -v
PHP 8.1.7RC1 (cli) (built: Jun 1 2022 08:43:27) (NTS)
Copyright (c) The PHP Group
Zend Engine v4.1.7RC1, Copyright (c) Zend Technologies
with Zend OPcache v8.1.7RC1, Copyright (c), by Zend Technologies
$ php -i | grep opcache.enable
opcache.enable => On => On
opcache.enable_cli => Off => Off
opcache.enable_file_override => Off => Off

OPCacheがインストールされていれば、with Zend OPCache という記述が見られます。また、有効化されていれば opcache.enable => On と表示されます。

4. Xdebug 有効化

Xdebug は PHP ウェブアプリケーション開発になくてはならないデバッガーですが、余計なオーバーヘッドが発生するためパフォーマンスに影響があります。本番環境で Xdebug を有効にする意味はありませんので、心配な方はチェックしてみましょう。確認方法は、OPCache の時と同じです。

1
2
3
4
5
PHP 8.1.7RC1 (cli) (built: Jun  1 2022 08:43:27) (NTS)
Copyright (c) The PHP Group
Zend Engine v4.1.7RC1, Copyright (c) Zend Technologies
with Zend OPcache v8.1.7RC1, Copyright (c), by Zend Technologies
with Xdebug v3.1.5-dev, Copyright (c) 2002-2022, by Derick Rethans

with Xdebug と表示されている場合は、Xdebug が少なくともインストールされています。本番動作時は無効化されている場合もありますので、さらに php -i で設定を確認してみてください。まあ、誤って有効化してしまう可能性があるので、ini ファイルで Xdebug のモジュールを読み込まないようにするのが安全だとは思います。

Laravel の書き方による低速化

1. eager loading 廃止

今回、もっとも効果が高かった低速化施策はコレでした。With メソッドを使うのをやめて、1レコードずつ関連するテーブルの情報を都度検索することで、著しく低速化させることが可能です。いわゆる N + 1 というやつです。

1
2
3
4
posts = \App\Models\Post::take(20);
//->with('user')
//->with('linkPosts')
//->with('linkedPosts')->get();

2. limit 句廃止

「そんなことするやついない」と思われるかもしれませんが、意外と見かけるのがコレです。わかりにくいですが、書き換えた方の処理だとデータベースから posts テーブルの情報を全件取得したあとに Collection クラスの take メソッドを使って 20 件に絞っています。ネットワーク上を posts テーブルのデータが全件旅するのを想像すると、ゾッとしますね。

1
2
3
//$posts = \App\Models\Post::take(20)->get();
$posts = \App\Models\Post::get()->take(20);
return view(...)->with('posts', $posts);

3. attribute casting の乱用

Laravel の Eloquent Model には Attirbute Casting という仕組みがあります。特にデフォルトで用意されている updated_at, created_at は、プロパティとして参照しただけで、内部でキャスト処理が実行されて、Carbon インスタンスが生成されます。この生成処理が馬鹿に出来ないほど高コストです。

tinker を使って、手元で計測してみましたが、5000 件程度の Collection に対して、created_at, updated_at のプロパティアクセス2回行うと、200 msec 程度の時間が生成だけで使われてしまいます。こんな行をループ内に何個も書いてしまったら……。ちょっと怖いですね。

1
2
3
4
foreach($posts as $post){
$post->updated_at;
$post->created_at;
}

まとめ

現場で普通に書かれていそうなコードでも、50 msec だった応答速度を、簡単に 1.5 sec くらいまで下げることが出来ました。実際のソースコードはもっと複雑でしょうから、さらに性能劣化が起きてしまっても不思議ではありません。

でも、普段からちょっと気をつけるだけで、性能劣化を起こしにくいソースコードにすることは可能です。少なくとも、今回挙げたような例については、確認してみることをおすすめします。結構、簡単ですよ。