皆さんこんにちは
PHP7.4でpreloadが来るっていうので、ワクテカしているわけですが、実際どれくらい早くなるのかしらって思いまして、ちょうど色々検証していたので、Laravelでも軽くやっちゃおうぜって思ったので、やってみます。
あと4日ですね!!
三行で
- preloadが来るよ
- preloadで読み込み済みの状態を作れるよ
- Laravelに導入すると、結構早くなるよ
Preload
PHP7.4でopcacheにpreloadという機能が入ります。これの概要は
- opcacheの設定にpreloadを実行するヘルパーファイルを指定できる
- webサーバを起動するとopcacheがヘルパーファイルを実行する
- ヘルパーファイル内でキャッシュ化したものは、通常のアクセスにおいて、すでに読み込まれたものとしてみなされる
そう、読み込み済みですよ!
簡単に読み込み済みの効果を見るために、以下のような例を考えてみましょう。
preload利用の例
まずは、こんなDockerfileで、動かせるコンテナを作ってみます。
FROM php:7.4.0RC4-cli
RUN apt update && apt install -y vim sudo && docker-php-ext-install opcache &&\
echo 'opcache.preload=/var/www/preload.php' >> /usr/local/etc/php/conf.d/docker-php-ext-opcache.ini &&\
useradd nginx
WORKDIR /var/www
ベースコンテナはちょいと古いのですが、まあ、検証当時のものなんで、許してくださいな。
そして、以下のようなディレクトリ構成のファイル群を用意します。
classes
- A.php
- B.php
index.php
preload.php
内容としては
<?php
class A
{
public function name(): string
{
return return static::class;
}
}
<?php
class B extends A {}
<?php
$b = new B;
echo $b->name() . '<BR>';
さて、サーバのエンドポイントとしてはindex.phpを使うわけですが、この中ではrequireを一回も使っていないし、当然autoloadの設定もしていないので、
sudo -u nginx php -S 0.0.0.0:8080
としてサーバを起動しても、いざアクセスすると「Bなんてクラス知らんのだけど」っていうエラーが出るだけです。
ここで、preloadを使ってみましょう。
先程のDockerfileには予めopcache.preloadの設定が挿入されるようになっています。実際に書かれている設定はこんな感じになっています。
zend_extension=/usr/local/lib/php/extensions/no-debug-non-zts-20190902/opcache.so
opcache.preload=/var/www/preload.php
このopcache.preload
がヘルパーファイルの場所になっているので、そこに以下のようなヘルパーをおいておきましょう。
<?php
$files = glob(__DIR__ . '/classes/**.php');
foreach ($files as $file) {
opcache_compile_file($file);
}
この状態でサーバを起動します。
sudo -u nginx php -S 0.0.0.0:8080
[Sun Nov 24 04:54:36 2019] PHP 7.4.0RC4 Development Server (http://0.0.0.0:8080) started
ちゃんと起動したようです。
それではアクセスしてみましょう。
$ curl http://localhost:8080
B<BR>
普通に動きますね。
preloadは性能に絡めるのか
多くのWebフレームワークは大量のクラスが定義されていますが、PHPでそれを読むにはいちいちファイルを見に行かなければなりません。しかも、PHPでは、基本的に各アクセスごとにプロセスが独立しているため、ファイルの読み込みもアクセスごとに発生します。
opcacheでこの動きを緩和することができますが、その動きは割と複雑です。
- クラスが呼ばれる (静的・インスタンス生成など)
- クラスの定義がない場合、autoloadで設定したルールに従い、ファイルをrequireする
- requireしようとしたファイルのキャッシュがあった場合、ファイルのタイムスタンプよりも新しければ、そのままキャッシュを使う
- キャッシュが古ければ、ファイルを読みに行く(設定によってスキップ可能)
- ファイルを読んだ場合はキャッシュを更新する
preloadしてあれば、1ですむので、それなりに早くなるんじゃないかって思います。
Laravelに入れてみる
たくさんのファイルを読み込むといえばLaravelですね!
そこで、適当にLaravelを入れて、検証してみましょう。
includeしているファイルを見てみる
とりあえず、どのくらいのファイルを読み込んでいるのか見てみます。
$kernel->terminate($request, $response);
$includes = get_included_files();
file_put_contents('/var/www/includes.txt', implode("\n", $includes));
こんな感じのを仕込むと、ファイルのリストが出てきます。350ファイル以上ありますね。
preloadに入れてみる
適当にpreloadに入れるわけですが、viewのキャッシュファイルとかは読んでもしょうがないかなとか思いながら、以下のようなloaderを作ります。
<?php
$includes = file_get_contents('/var/www/includes.txt');
$includes = explode("\n", $includes);
foreach ($includes as $include) {
if (strpos($include, '/storage/framework') !== false) {
echo $include, " - storage \n";
continue;
}
if (strpos($include, 'index.php') !== false) {
echo $include, " - index \n";
continue;
}
if (strpos($include, 'src/config') !== false) {
echo $include, " - config\n";
continue;
}
opcache_compile_file($include);
}
こいつをpreloadした状態でサーバを起動します。
index.phpにerror_log(count($includes));
みたいなコードを突っ込んでやると、読み込みファイル数が363 -> 46 になりました。せっかくなら1とかになってくれるとありがたかったんですが、そううまくは行かないようですね。
検証する
例によってab使って一分間のアクセスレートを見てみましょう。
abはubuntu 18.04 (wsl2のやつ) を使ってます。
preloadなし
$ ab -t 60 -c 1 http://localhost:8080/
...
Concurrency Level: 1
Time taken for tests: 60.004 seconds
Complete requests: 23
Failed requests: 0
Total transferred: 77747 bytes
HTML transferred: 55798 bytes
Requests per second: 0.38 [#/sec] (mean)
Time per request: 2608.884 [ms] (mean)
Time per request: 2608.884 [ms] (mean, across all concurrent requests)
Transfer rate: 1.27 [Kbytes/sec] received
Connection Times (ms)
min mean[+/-sd] median max
Connect: 0 0 0.1 0 0
Processing: 177 2609 1619.2 2894 7586
Waiting: 176 2608 1619.1 2893 7585
Total: 177 2609 1619.2 2894 7586
Percentage of the requests served within a certain time (ms)
50% 2844
66% 3064
75% 3230
80% 3256
90% 3323
95% 3658
98% 7586
99% 7586
100% 7586 (longest request)
2.5秒超えるのか。。。
一応、opecache使ってファイルはキャッシュしているはずなんですが。
preloadあり
$ ab -t 60 -c 1 http://localhost:8080/
...
Concurrency Level: 1
Time taken for tests: 60.160 seconds
Complete requests: 301
Failed requests: 0
Total transferred: 1017565 bytes
HTML transferred: 730226 bytes
Requests per second: 5.00 [#/sec] (mean)
Time per request: 199.867 [ms] (mean)
Time per request: 199.867 [ms] (mean, across all concurrent requests)
Transfer rate: 16.52 [Kbytes/sec] received
Connection Times (ms)
min mean[+/-sd] median max
Connect: 0 0 0.0 0 0
Processing: 148 200 140.9 179 1575
Waiting: 148 199 140.9 179 1574
Total: 148 200 140.9 179 1575
Percentage of the requests served within a certain time (ms)
50% 179
66% 187
75% 193
80% 199
90% 209
95% 224
98% 260
99% 852
100% 1575 (longest request)
ほんまか?
10倍強の速度になっているんだが。
まとめ
速度強化が劇的すぎて、流石にコンテナ特有のボリュームアクセス遅い問題に関係しちゃっているのかもしれないなぁとか思いながらも、ファイルアクセスが減って早くなっているのは事実だなぁって思いました。
将来的にはフレームワークごとにpreloadのヘルパーを用意してくれると、こっちとしては脳死で実装できて楽だなぁって他力本願に考えちゃったりしています。
今回はこんなところです。