RAKUS Developers Blog | ラクス エンジニアブログ

株式会社ラクスのITエンジニアによる技術ブログです。

OPcacheのpreload機能が思うように動かない理由はこれです!

こんにちは、あるいはこんばんは。すぱ..すぱらしいサーバサイドのエンジニアの(@taclose)です☆

今回の記事はOPcacheのpreloadが出来るようになろう! という内容です。

尚、OPcacheのpreloadの基本設定とかについては以下の記事を参考にしてください。

tech-blog.rakus.co.jp

今回は上記記事では話していなかったpreloadのよくありそうな失敗話になります。

PHPerKaigi2023では語れなかった部分だったので是非見てもらえればと思います。

そして preloadとrequire_onceとかとの関係性について理解を深めて、更なる改善につなげてもらえればと思います!

では早速はじめていきましょう!

失敗談:preloadしても変化がない!

前回も張った画像ですが、まずこの図ですね。

preloadの説明

この図の通りでいうなら、「お?preload使えば動的にロードなくなるんだ!」ってなるのですが、実際どうなるか?

手順は以下です。

  • preloadの設定解除して count(get_included_files()) を出力
  • apacheを再起動
  • preloadの設定して count(get_included_files()) を出力

この数の差がpreloadで事前ロードが成功した数になります。

私のとある開発環境では300ファイルのinclude数が150ファイルぐらいまで減らす事ができました。

「そのpreload出来なかった150ファイルは一体なんぞや!」

これを今日は深掘っていきますよ!

解決策

説明用のファイルはこちら

今回のこの問題を説明する用に簡単なPHPファイルを用意しました。皆さんも手元で試してもらうと良いかな!

<?php
# index.php
require_once("autoload.php");

print_r(get_included_files());

echo "loaded : index.\n";
$a = new A();
$a->call();

print_r(get_included_files());
<?php
# autoload.php
spl_autoload_register(
  function ($className) {
    echo "spl_autoload: $className.\n";
    require_once($className.".php");
  }
);
<?php
# A.php
class A extends AP {
  public static string $BName = B::name;
  public function call() {
    echo "call : ".__class__.", B#name : ".self::$BName."\n";
  }
}
<?php
# AP.php
class AP {
  public function call() {
    echo "call : ".__class__."\n";
  }
}
<?php
# B.php
class B {
  public const name = "B#name";
}
<?php
# preload.php
$root_path=dirname(__FILE__)."/";
# 以下例えばのpreload 一旦コメントアウトしておきますね。
#opcache_compile_file($root_path."AP.php");
#opcache_compile_file($root_path."A.php");
#opcache_compile_file($root_path."B.php");
#opcache_compile_file($root_path."index.php");
#opcache_compile_file($root_path."autoload.php");
echo("Preloading Success\n");

試してみよう!

まずはpreload.phpphp.iniに設定しないでやるとどうなるでしょうか?

$ php index.php
Array
(
    [0] => /usr/local/application/works/index.php
    [1] => /usr/local/application/works/autoload.php
)
loaded : index.
spl_autoload: A.
spl_autoload: AP.
spl_autoload: B.
call : A, B#name : B#name
Array
(
    [0] => /usr/local/application/works/index.php
    [1] => /usr/local/application/works/autoload.php
    [2] => /usr/local/application/works/A.php
    [3] => /usr/local/application/works/AP.php
    [4] => /usr/local/application/works/B.php
)

実行時にindex.phpがロードされて、require_onceでautoload.phpがロードされて、各クラスが必要になったタイミングでspl_autoloadがされていってるんだな!っていうのがよくわかりますね。

では、preload.phpA.phpをpreloadするとどうなるでしょうか?

$ php index.php
Preloading Success
Array
(
    [0] => /usr/local/application/works/index.php
    [1] => /usr/local/application/works/autoload.php
)
loaded : index.
spl_autoload: A.
spl_autoload: AP.
spl_autoload: B.
call : A, B#name : B#name
Array
(
    [0] => /usr/local/application/works/index.php
    [1] => /usr/local/application/works/autoload.php
    [2] => /usr/local/application/works/A.php
    [3] => /usr/local/application/works/AP.php
    [4] => /usr/local/application/works/B.php
)

変わってないじゃないか!!!!ログをみてみると...

$ cat /var/log/messages | tail -n1
Mar 28 20:19:18 auto7-dev045 php: PHP Warning:  Can't preload unlinked class A: Unknown parent AP in /usr/local/application/works/A.php on line 3

なるほど、preloadというのは親クラスとかも一緒にpreloadしてあげないといけないんですね!

よし、ではAP.phpもpreloadして再度トライ!

$ php index.php
Preloading Success
Array
(
    [0] => /usr/local/application/works/index.php
    [1] => /usr/local/application/works/autoload.php
)
loaded : index.
spl_autoload: A.
spl_autoload: B.
call : A, B#name : B#name
Array
(
    [0] => /usr/local/application/works/index.php
    [1] => /usr/local/application/works/autoload.php
    [2] => /usr/local/application/works/A.php
    [3] => /usr/local/application/works/B.php
)

おーい!もっかいログを見てみましょう!

$ cat /var/log/messages | tail -n1
Mar 28 20:27:23 auto7-dev045 php: PHP Warning:  Can't preload class A with unresolved initializer for static property $BName in /usr/local/application/works/A.php on line 3

なになに、static propertyでつかってる$BNAMEがわかりませんと...

static propertyとかで使われてる値も一緒にpreloadしないとダメ! って事ですね!

よし、じゃB.phpもpreloadしましょう!再度トライだ!

$ php index.php
Preloading Success
Array
(
    [0] => /usr/local/application/works/index.php
    [1] => /usr/local/application/works/autoload.php
)
loaded : index.
call : A, B#name : B#name
Array
(
    [0] => /usr/local/application/works/index.php
    [1] => /usr/local/application/works/autoload.php
)

成功ですね!!!あとはautoload.phpだけか!よし、これもpreloadしましょう!

$ php index.php
Preloading Success
Array
(
    [0] => /usr/local/application/works/index.php
    [1] => /usr/local/application/works/autoload.php
)
loaded : index.
call : A, B#name : B#name
Array
(
    [0] => /usr/local/application/works/index.php
    [1] => /usr/local/application/works/autoload.php
)

なぜ!!ログを見てみましょう!ってログも何も出てないよ!なぜか?答えは

preloadしていてもrequire_onceとかしちゃうと結局ロードされちゃいます!!

ですので、index.phpのrequire_onceを削ってみましょう!

こんな感じです。

<?php
# index.php
print_r(get_included_files());
echo "loaded : index.\n";

$a = new A();
$a->call();
print_r(get_included_files());

結果は・・・・?

$ php index.php
Preloading Success
Array
(
    [0] => /usr/local/application/works/index.php
)
loaded : index.
call : A, B#name : B#name
Array
(
    [0] => /usr/local/application/works/index.php
)

大成功!ついにincludesファイルが0個になりました!!

まとめ

はい、まとめです。preloadって事前ロードしてくれる機能ではあるのですが、内部で使われているクラスに漏れがあると、そのクラス自体のpreloadも出来ない!

そして、require_onceなどしているものは結局preloadされてしまう!という事なんですね。

皆さんもこれを参考にしながらpreloadの設定頑張ってくださーい!!

Copyright © RAKUS Co., Ltd. All rights reserved.