前置き
今回は私が2023年10月半ばから末にかけて体験した、 PHP 8.1/8.2 と WordPress 6.3 の組み合わせが引き起こした問題への対処の話です。
起きた現象
それでは、実際に何が起きたかを順を追って説明します。
やっていたことは KUSANAGI 9 の有償版を改修中で、テストをするために KUSANAGI 9 有償版のセットアップをしていました。
KUSANAGI 9 有償版セットアップが終わり、改修した内容のテスト中に異変が発生しました。
具体的には KUSANAGI 有償版のインストール画面が終わり、 WordPress のインストール画面で言語選択するところで、 English 以外を選択すると WordPress が固まってしまうという現象でした。
現象の確認
現象の内容を確認するために、まずはログを確認します。
- /home/kusanagi/(プロファイル名)/log/nginx/error.log
# cat /home/kusanagi/(プロファイル名)/log/nginx/error.log
2023/10/20 02:03:46 [error] 3412#3412: *10 FastCGI sent in stderr: "PHP message: PHP Fatal error: Allowed memory size of 268435456 bytes exhausted (tried to allocate 8192 bytes) in /home/kusanagi/(プロファイル名)/DocumentRoot/wp-includes/functions.php on line 3575" while reading response header from upstream, client: 202.190.118.18, server: (FQDN), request: "POST /wp-admin/install.php?step=1 HTTP/1.1", upstream: "fastcgi://127.0.0.1:9000", host: "(FQDN)", referrer: "http://(FQDN)/wp-admin/install.php"
#
- /var/opt/kusanagi/log/php-fpm/error.log
# cat /var/opt/kusanagi/log/php-fpm/error.log
(前略)
[20-Oct-2023 02:02:19] NOTICE: fpm is running, pid 3702
[20-Oct-2023 02:02:19] NOTICE: ready to handle connections
[20-Oct-2023 02:03:22] WARNING: [pool www] child 3736, script '/home/kusanagi/(プロファイル名)/DocumentRoot/wp-admin/install.php' (request: "POST /wp-admin/install.php?step=1") executing too slow (11.811636 sec), logging
[20-Oct-2023 02:03:22] NOTICE: child 3736 stopped for tracing
[20-Oct-2023 02:03:22] NOTICE: about to trace 3736
[20-Oct-2023 02:03:22] NOTICE: finished trace of 3736
NOTICE: PHP message: PHP Fatal error: Allowed memory size of 268435456 bytes exhausted (tried to allocate 8192 bytes) in /home/kusanagi/(プロファイル名)/DocumentRoot/wp-includes/functions.php on line 3575
#
発生している現象は、 PHP(php-fpm) でメモリを食いつぶしてしまい、それで落ちてしまっているようです。
なぜ PHP が落ちたのかをより深く調べるために、どのような環境で再現する現象かを確認します。
そのために、いくつかの環境で同じテストを行います。
- OS
- 最初に起きたのは AlmaLinux OS 8 の環境
- CentOS Stream 8 でも確認、同様の現象が発生
OS は関係なく発生することを確認。
- PHP
- 最初に起きたのは PHP 8.1 の環境
- PHP 8.2 にしても現象が発生
- PHP 8.0 にすると現象は発生しない
PHP ではバージョンによって発生する/しないがありました。
- WordPress
- 最初に起きたのは WordPres 6.3.2 (当時の最新) の環境
- WordPres 6.3.1 など 6.3 系列のバージョンで発生
- WordPress 6.2 系列以前では発生しない
WordPress でもバージョンによって発生する/しないがありました。
この時点で考えられる問題は PHP か WordPress のどちらかです。
PHP の問題
PHP がなぜバージョンによって問題が発生したりしなかったりしたのでしょうか。
ここで、一つ思い当たるのは以下の対応です。
KUSANAGI 9モジュール更新情報 – KUSANAGI
PHP 8.0 だけ opcache.jit の値を 1205(function) にする変更が入っていました。
つまり、PHP 8.1 / 8.2 の opcache.jit の値は PHP のデフォルトである 1254(tracing) を使用していたのです。
ここで opcache.jit の値を変えてテストすることにしてみました。
- PHP
- PHP 8.1
- opcache.jit = 1254 では現象が発生
- opcache.jit = 1205 では現象が発生しない
- PHP 8.2
- opcache.jit = 1254 では現象が発生
- opcache.jit = 1205 では現象が発生しない
- PHP 8.1
この時点で問題は opcache.jit にあると特定できました。
PHP の opcache.jit 機能とは
それでは PHP の opcache.jit 機能とはなんでしょうか?
これは PHP 8 から追加された機能です。
PHP は他のインタプリタ言語の例にもれず、PHP のコードをバイトコード(Opcode)に変換し、それを VM(Zend VM) がマシンコードに変換して実際に実行されます。
その際に変換されたバイトコードを共有メモリに保存して、再利用するのが opcache の仕組みです。
opcache は PHP7 でも利用できます。
更に一歩進んで、マシンコードを共有メモリに保存して、再利用するのが opcache.jit の仕組みです。
opcache.jit には種類がいくつかあり、代表的なものに以下の2つがあります。
- Function JIT
関数単位で JIT コンパイルが行われれる。
opcache.jit の値は function もしくは 1205 となる。
- Tracing JIT
ホットコードを識別して最適化された JIT コンパイルが行われる。
opcache.jit の値は tracing もしくは 1254 となる。
こちらが、 PHP のデフォルトの値となる。
参考:
PHP: 実行時設定 – Manual
PHP8で追加されたJITと、PHP OPcacheについて徹底解説!「悪りぃが、こっから先は一方通行だ」 #PHP – Qiita
WordPress の問題
しかし、疑問なのは WordPress 6.3 系で発生して 6.2 系以前では現象が発生しないという点です。
なぜ、バージョンの違いで現象が発生したりしなかったりしたのか、そこも確認していきました。
現象が発生するときのコードを追っていき、具体的に落ちるポイントを以下に特定しました。
return wp_tempnam( $filename, $dir );
実際のコード:
WordPress/wp-admin/includes/file.php at 6.3-branch · WordPress/WordPress · GitHub
WordPress インストール時に言語選択を行った場合、その言語ファイルがまだ存在しない場合は言語ファイルのダウンロードを行います。
言語ファイルダウンロード後に、圧縮された言語ファイルを展開する時の wp_tempnam で落ちていることを確認しました。
この部分が以前のバージョンと何が変わったのか確認したところ、以下のように変更されていました。
$temp_filename = $dir . wp_unique_filename( $dir, $temp_filename );
↓
$temp_filename = wp_unique_filename( $dir, $temp_filename );
/*
* Filesystems typically have a limit of 255 characters for a filename.
*
* If the generated unique filename exceeds this, truncate the initial
* filename and try again.
*
* As it's possible that the truncated filename may exist, producing a
* suffix of "-1" or "-10" which could exceed the limit again, truncate
* it to 252 instead.
*/
$characters_over_limit = strlen( $temp_filename ) - 252;
if ( $characters_over_limit > 0 ) {
$filename = substr( $filename, 0, -$characters_over_limit );
return wp_tempnam( $filename, $dir );
}
$temp_filename = $dir . $temp_filename;
変更内容としては、ファイル名が 255 文字を超えないようする対応で、特別問題のある対応ではありませんでした。
ただ、WordPress 6.3 系のこの対応を 6.2 に戻して実行すると、現象が発生しないことも確認できました。
問題のまとめ
現象が発生する条件をまとめると、以下のようになりました。
- PHP の opcache.jit = 1254
- WordPress 6.3 系
ある程度問題が特定できたので、この内容を PHP もしくは WordPress に報告しようと思いました。
そのためには KUSANAGI の環境ではなく、まっさらから立ち上げた環境で WordPress を立ち上げ、より問題の特定をしようと作業を進めていました。
問題の解消
しかし、10月末になったところで、突然現象が発生しなくなりました。
慌てて再度調査したところ、PHP のマイナーバージョンが上がっており、マイナーバージョンを1つ落とすと再現することが確認できました。
つまり、マイナーバージョンアップによって、 opcache.jit の何かが解消されたということになります。
そのため、 PHP 公式のバージョンアップの情報を確認しました。
すると、思った通り 8.1.25(10月26日更新) の対応で以下のような記載がありました。
- Opcache:
- Fixed opcache_invalidate() on deleted file.
Opcache に関連する部分で削除されたファイルに関する部分が修正されたようです。
おそらく今回の現象は、元々 opcache.jit が抱えていた問題で、削除されたファイルに対する扱いが不正だったと思われます。
実際 WordPress の言語設定の部分で言語ファイルをダウロードして、圧縮ファイルを展開する際に起きた現象でした。
圧縮ファイルを展開する際には一時ファイル名で展開するため、展開後にその一時ファイルを削除することが発生します。
他にも、以前から弊社でも確認できていた、プラグインやテーマのアップデート後にセグメンテーションフォルトが起きていた現象も同様だと思われます。
最後に
結果として PHP 側が対応したことにより問題は解消しましたが、問題を特定し報告を行うことで OSS(PHP or WordPress) へ貢献しようと思いました。
それは弊社の掲げる理念の1つである「すべてはエンタープライズOSSエコシステム発展のために」を実現するためです。
これからもその理念の実現を進めていきます。