Kitaru
- — 5 months ago

これまでシステム開発終盤の負荷試験で思うような性能が出ずに、「(;;´ )ε( `;;)・・・う~ん」と悩まされることが何度かありました。
しかしそういった場合でも、比較的ローコストで改善する方法がないかと考えていたところ、PHP7.4のpreloadと呼ばれる新機能でパフォーマンスが向上したという記事を目にしました。
「プリロード」、、、なんて素敵な響き、きっと速くなるに違いない!と思い、自分でも検証してみることにしました。
ちなみにpreloadとは、サーバ起動時に指定したスクリプトを読み込み、それをコンパイルしたものをメモリに載せて、全てのリクエストにおいてそのキャッシュを利用するという機能のことです。
やること
「preloadなし」と「preloadあり」でLaravelのベンチマークをとる。
環境
- CentOS 7.7
- Nginx 1.17.9
- php-fpm 7.4.4
- Laravel 7
VirtualBox+Dockerで環境を構築しました。
事前準備
abコマンド
abコマンドで性能測定するので次のようにhttpd-toolsが必要になります。
# yum install -y httpd-tools
検証用スクリプト
固定のjson文字列を返却するだけのシンプルなAPIで検証することにしました。
app/Http/Controllers/TestController.php
<?php
namespace App\Http\Controllers;
class TestController extends Controller
{
public function hello()
{
return response()->json([
'message' => 'Hello World!',
]);
}
}
routes/web.php
<?php
・・・
Route::get('test/hello', 'TestController@hello');
preloadするためのスクリプト
PHP RFCにあるヘルパー関数をほぼそのまま流用します。 https://wiki.php.net/rfc/preload#proposal
<?php
function _preload($preload, string $pattern = "/\.php$/", array $ignore = []) {
if (is_array($preload)) {
foreach ($preload as $path) {
_preload($path, $pattern, $ignore);
}
} else if (is_string($preload)) {
$path = $preload;
if (!in_array($path, $ignore)) {
if (is_dir($path)) {
if ($dh = opendir($path)) {
while (($file = readdir($dh)) !== false) {
if ($file !== "." && $file !== "..") {
_preload($path . "/" . $file, $pattern, $ignore);
}
}
closedir($dh);
}
} else if (is_file($path) && preg_match($pattern, $path)) {
if (!opcache_compile_file($path)) {
trigger_error("Preloading Failed", E_USER_ERROR);
}
}
}
}
}
_preload(["/var/www/html/preload/vendor/laravel"]);
ヘルパー関数が行っていることは、キャッシュ対象のディレクトリを指定して、そのディレクトリ以下にあるphpスクリプトを再帰的に調べた上でopcache_compile_file関数を呼んでいるだけです。
このopcache_compile_file関数がpreloadで重要な役割を果たします。 https://www.php.net/manual/ja/function.opcache-compile-file.php
この関数は、PHP スクリプトをコンパイルして opcode キャッシュに追加しますが、スクリプトは実行しません。 これを使うと、ウェブサーバーの再起動後にファイルをキャッシュしておき、 後でリクエストがあったときにそのキャッシュを使えるようになります。
サーバを再起動しない限り永久にキャッシュされ続けるので、キャッシュ対象は変更が少ないvendor/laravel以下のファイルにしています。
実は当初、キャッシュ対象ディレクトリを1階層上のvendorにしていたのですが、毎回必ず5,191個目のファイル(vendor/sebastian/resource-operations/build/generate.php)を読み込んだ時点でopcache_compile_file関数がphpエラーを吐かずに終了してしまうようで、そのせいでphp-fpmコンテナが起動できなくなりました。
問題のgenerate.phpを読み込まないように除外しても、別のファイルの読み込みで失敗するようだったので、キャッシュ対象を限定してvendor/laravelのみをキャッシュすることにしました。
ログレベル(opcache.log_verbosity_level)を上げると稼働ログは出力されるようになりましたがエラーは出力されないままで、OPcacheのメモリサイズ(opcache.memory_consumption)や読み込みファイル数(opcache.max_accelerated_files)の上限を上げても無理だったので、opcache_compile_file関数のバグのような気がするのですが、どうなのでしょうか。
この辺についてわかっている人がいたら教えて頂きたい。。。
とりあえずvendor/laravel(908ファイル)だけであれば、全てキャッシュすることができました。
OPcacheの設定
ヘルパー関数はphp.iniのopcache.preloadに指定します。こうすることによってサーバが起動した際にヘルパー関数が実行されて、phpスクリプトがキャッシュされます。
# cat /etc/php.ini
・・・
[opcache]
opcache.preload=/var/www/html/preload.php
opcache.preload_user=www-data
・・・
最初ハマってしまったのですが、opcache.preloadの設定しかしていなかったため、サーバ起動時に次のようなエラーが発生しました。
Fatal Error "opcache.preload_user" has not been defined
PHPマニュアルによると、次のようにセキュリティ上の理由からopcache.preload_userにはroot以外のユーザを指定しないといけないようです。
opcache.preload_user string
root ユーザでコードをあらかじめロードすることは、セキュリティ上の理由から禁止されています。 このディレクティブは、コードを別のユーザでロードする手助けをします。
今回はNginx+php-fpmで動かしているので、www-dataユーザでヘルパー関数を実行することにしました。
実行結果
preloadなし
同時に100ユーザが、1ユーザあたり10リクエスト(合計1,000リクエスト)を発行した場合を想定して検証用スクリプトを実行しました。
preloadは行っていませんが、OPcacheは有効にしています。
$ ab -n 1000 -c 100 http://192.168.56.101/test/hello
This is ApacheBench, Version 2.3 <$Revision: 1430300 $>
Copyright 1996 Adam Twiss, Zeus Technology Ltd, http://www.zeustech.net/
Licensed to The Apache Software Foundation, http://www.apache.org/
Benchmarking 192.168.56.101 (be patient)
Completed 100 requests
Completed 200 requests
Completed 300 requests
Completed 400 requests
Completed 500 requests
Completed 600 requests
Completed 700 requests
Completed 800 requests
Completed 900 requests
Completed 1000 requests
Finished 1000 requests
Server Software: nginx/1.17.9
Server Hostname: 192.168.56.101
Server Port: 80
Document Path: /test/hello
Document Length: 26 bytes
Concurrency Level: 100
Time taken for tests: 9.755 seconds
Complete requests: 1000
Failed requests: 0
Write errors: 0
Total transferred: 963000 bytes
HTML transferred: 26000 bytes
Requests per second: 102.51 [#/sec] (mean)
Time per request: 975.520 [ms] (mean)
Time per request: 9.755 [ms] (mean, across all concurrent requests)
Transfer rate: 96.40 [Kbytes/sec] received
Connection Times (ms)
min mean[+/-sd] median max
Connect: 0 0 0.6 0 3
Processing: 28 905 191.9 930 1478
Waiting: 28 905 191.9 929 1478
Total: 31 905 191.4 930 1478
Percentage of the requests served within a certain time (ms)
50% 930
66% 961
75% 989
80% 1021
90% 1094
95% 1141
98% 1195
99% 1299
100% 1478 (longest request)
1秒あたり102.51リクエストを処理しています。
別途、1回のリクエストでLaravelがインクルードしているファイル数を調べたところ、410ファイルを読み込んでいました。
preloadあり
$ ab -n 1000 -c 100 http://192.168.56.101/test/hello
This is ApacheBench, Version 2.3 <$Revision: 1430300 $>
Copyright 1996 Adam Twiss, Zeus Technology Ltd, http://www.zeustech.net/
Licensed to The Apache Software Foundation, http://www.apache.org/
Benchmarking 192.168.56.101 (be patient)
Completed 100 requests
Completed 200 requests
Completed 300 requests
Completed 400 requests
Completed 500 requests
Completed 600 requests
Completed 700 requests
Completed 800 requests
Completed 900 requests
Completed 1000 requests
Finished 1000 requests
Server Software: nginx/1.17.9
Server Hostname: 192.168.56.101
Server Port: 80
Document Path: /test/hello
Document Length: 26 bytes
Concurrency Level: 100
Time taken for tests: 8.429 seconds
Complete requests: 1000
Failed requests: 0
Write errors: 0
Total transferred: 963000 bytes
HTML transferred: 26000 bytes
Requests per second: 118.63 [#/sec] (mean)
Time per request: 842.933 [ms] (mean)
Time per request: 8.429 [ms] (mean, across all concurrent requests)
Transfer rate: 111.57 [Kbytes/sec] received
Connection Times (ms)
min mean[+/-sd] median max
Connect: 0 0 0.7 0 3
Processing: 25 805 211.2 784 1388
Waiting: 22 805 211.2 784 1388
Total: 25 805 210.9 784 1390
Percentage of the requests served within a certain time (ms)
50% 784
66% 803
75% 818
80% 829
90% 978
95% 1355
98% 1370
99% 1377
100% 1390 (longest request)
1秒あたり118.63リクエストを処理しています。
何度か実行しましたが、「preloadあり」のほうが「preloadなし」より10%~15%ほど性能が良かったです。
インクルードしているファイル数も236に減っていました。
まとめ
preloadによりLaravelのパフォーマンスは向上しました。
しかし、OPcacheを無効→有効にすると10倍ほどパフォーマンスが向上していたので、それと比較すると思ったほどのインパクトはないというのが正直なところです。
Laravelの機能をもっと多く利用したアプリだと効果が大きいのかもしれませんが、opcache_compile_file関数で落ちてしまう問題や、preloadで読み込んだはずの一部のスクリプトがなぜか実行時にも読み込まれていたりと、枯れた技術ではないという印象でした。
現時点だと、よほどパフォーマンスを重視したいという理由以外で、本番環境で使用することはなさそうです。
う~ん、、、まぁ近々正式リリースされるPHP8のJITは爆速らしいので、そちらも検証してみたいです。
Kitaru
Programmer
PHP7.4のpreloadでLaravel7を高速化!? PHP7.4のpreloadでLaravel7を高速化!? 2020-07-31 14:08:42- CentOS7にNextCloudを導入する 1 year ago
- DockerでCodeigniter4 betaを動かす 1 year ago
- Laravel mix vue No.1 - Docker Environment - Dockerでlaravel環境 (laradockを使わない) 9 months ago
- Asterisk16について調べてみた 1 year ago
- Windows10 Home+WSL2でLinux(CentOS7)を動かす 5 months ago
- PHP7.4のpreloadでLaravel7を高速化!? 5 months ago
- 負荷試験ツール「Gatling」を試してみた。【Windows 10 編】 8 months ago
- PipenvによるWindows上でのPython仮想環境の構築について 9 months ago
- CentOS6を使ったルーティング 1 year ago
- 【Docker環境】FluentdとMySQLのbulk insertを使用したログ収集【前編】 8 months ago
- Unity初心者がawsサーバーとWebSocketを使ってのリアルタイム同期通信について学ぶ① 2 weeks ago
- Dockerコンテナ(Fluentd)のTimeZoneをUTCからJSTに変更する 1 month ago
- 端末でのサマータイム確認でログインできない 1 month ago
- laravelを経験した後のEC-CUBE3の所感 2 months ago
- Amazon Auroraのレプリケーション 2 months ago
- Asteriskの音声ガイダンスを日本語化 2 months ago
- AsteriskでCDR(通信履歴)をODBC接続でMySQLへ保存する 3 months ago
- AsteriskのRealtime DatabaseでODBCを使わずにMySQLへ接続する 3 months ago
- Laravel で開発をして思ったこと② 3 months ago
- Asteriskの通話を録音する 3 months ago