FreeBSD14 + Nginx + PHP8.3 + WordPress 6.8.2で起きた原因不明のPHPエラー
突然PHPエラーでサイトが表示されなくなって調査しているときの覚書。
Gemini 2.5 Proと対話
環境: FreeBSD 14.3-RELEASE-p2, nginx 1.28.0, PHP 8.3.23, WordPress 6.8.2
エラー内容
2025/09/03 07:20:27 [error] 18603#100288: *143368 FastCGI sent in stderr: "PHP message: PHP Fatal error: Allowed memory size of 268435456 bytes exhausted (tried to allocate 4295229440 bytes) in /home/httpd/hoge/wordpress/wp-includes/theme.php on line 325" while reading response header from upstream, client: 1.2.3.4, server: kaisei-hp.co.jp, request: "GET / HTTP/2.0", upstream: "fastcgi://unix:/var/run/php-fpm.sock:", host: "hoge.jp"
エラーの意味
PHPがスクリプトを実行するために許可されたメモリ上限(memory_limit)を使い果たしてしまった。
- 許可されたメモリ (Allowed memory size): 268,435,456バイト = 256MB
- 確保しようとしたメモリ (tried to allocate): 4,295,229,440バイト = 約4.29GB
wp-includes/theme.phpの325行目。
return apply_filters( 'template', get_option( 'template' ) );
取り急ぎphp-fpmを再起動。
# service php_fpm restart
これでサイトは表示されるようになった。
1. エラー周辺のシステムリソース情報
このサーバーはCollectdをインストールしてあるので、エラー発生周辺時間のリソース情報を取得するシェルスクリプトをGemini先生に作ってもらった。
Gist: https://gist.github.com/DaikiSuganuma/fd49b7f88b9f5d97675d9a822104a0a1
実行してGemini先生に報告。
# cd /home/software/server-utility/
# vim get_collectd_data.sh
# chmod +x get_collectd_data.sh
# ./get_collectd_data.sh
分析結果。
- 問題はリソース不足ではない: サーバーの物理メモリは潤沢であり、継続的なメモリリークやシステム全体の負荷増大が原因ではない。
- 問題のトリガーは単発的なリクエスト: 特定のリクエスト(おそらくWordPressのトップページへのアクセス)が引き金となり、単一プロセス内で異常なメモリ要求が発生した。
- 再起動が問題を解消した: PHP-FPMサービス(またはワーカープロセス)が再起動したことで、問題のワーカーが強制終了され、正常な状態に戻った。
2. PHPの設定変更
今後原因を特定するためにPHPエラーログ設定をする。
この設定はWordPressのWP_DEBUG=falseでも記録されるはず。
# less /usr/local/etc/php.ini
log_errors = Onerror_log = /var/log/php_errors.log
ファイルを作る。
# touch /var/log/php_errors.log
# chown www:www /var/log/php_errors.log
PHP-FPMのスローログ(Slow Log)を有効にする。
# less /usr/local/etc/php-fpm.d/www.conf
slowlog = /var/log/php-fpm-slow.logrequest_slowlog_timeout = 10s
テストして再読み込み。
# php-fpm -t
# service php_fpm reload
Gemini先生がお勧めしてきたOPcacheのvalidate_timestamps(ファイル更新チェック)をオンにする。
# less /usr/local/etc/php/ext-10-opcache.ini
; ファイルのタイムスタンプチェックを無効化。本番環境でのパフォーマンスを最大化。; ※注意: テーマやプラグインの更新後は、必ずphp-fpmのリロードが必要です。; 予期せぬ動作を引き起こす可能性があるため有効にする。opcache.validate_timestamps=1
テストして再読み込み。
# php-fpm -t
# service php_fpm reload
# php -i | grep opcache
php_errors.logの動作確認。
エラーを起こすコマンド。
# php -r '$a = 1/0;'
エラーログ確認。
# less /var/log/php_errors.log
3. Nginxのアクセスログ変更
NginxのアクセスログにPHPの処理時間を記録する。
# cd /usr/local/etc/nginx/conf.d/include/
# less log_format.inc
# --- 元のログフォーマット(比較用) ---# log_format main '$remote_addr - $remote_user [$time_local] "$request" '# '$status $body_bytes_sent "$http_referer" '# '"$http_user_agent" "$http_x_forwarded_for"';## --- [main_v2]アクセスログフォーマット ---# ドメイン名($host)と処理時間($request_time)を追加し、より詳細な分析を可能にする## [main_v2]の出力例# 192.168.1.10 [example.com] - - [15/May/2024:14:30:00 +0900] "GET /about/ HTTP/1.1" 200 5120 "https://www.google.com/" "Mozilla/5.0..." rt=0.035 urt=0.035#log_format main_v2# $remote_addr : [fail2banのために先頭に] リクエスト元のクライアントIPアドレス。 (例: '192.168.1.10')'$remote_addr '# $host : リクエストヘッダのHostフィールド。アクセスされたドメイン名が入る。 (例: 'example.com')# もし$server_nameを使いたい場合は、こちらに置き換えても良い。'[$host] '# $remote_user : Basic認証で認証されたユーザー名。認証がなければ '-' となる。 (例: '-')'- $remote_user '# [$time_local] : サーバーのローカルタイム。 (例: '[15/May/2024:14:30:00 +0900]')'[$time_local] '# "$request" : リクエストのメソッド、URI、プロトコルバージョン。 (例: '"GET /about/ HTTP/1.1"')'"$request" '# $status : レスポンスのHTTPステータスコード。 (例: '200')'$status '# $body_bytes_sent : レスポンスボディのバイト数(コンテンツのサイズ)。 (例: '5120')'$body_bytes_sent '# "$http_referer" : 参照元(どのページから来たか)。直接アクセスなら '-' となる。 (例: '"https://www.google.com/"')'"$http_referer" '# "$http_user_agent" : クライアントのブラウザやツールの情報。 (例: '"Mozilla/5.0 (Windows NT 10.0; ...)"')'"$http_user_agent" '# $request_time : リクエスト全体(Nginxがリクエストを受け取ってからレスポンスを返すまで)の処理時間(秒)。# ネットワークの遅延なども含むため、純粋なPHP処理時間より少し長くなる。 (例: 'rt=0.035')'rt=$request_time '# $upstream_response_time: バックエンド(PHP-FPM)の処理時間(秒)。# 今回の問題調査で最も重要な指標。 (例: 'urt=0.035')'urt=$upstream_response_time';# 新しいフォーマット(main_v2)を使用するように設定するaccess_log /var/log/nginx/access.log main_v2;
テストして再読み込み。
# nginx -t
# service nginx reload
アクセスログの確認。
# tail -f /var/log/nginx/access.log
fail2banを変更する必要はなし。
4. NginxのVTSとPHP OPcacheの情報
NginxのVTSの情報をJSONで取得するコマンド。Basic認証付き。
# curl --user hoge01:pass -s https://tools.hoge.jp/vts_status//format/json
PHP OPcacheの情報をJSONで取得するコマンド。
# php -r 'if (function_exists("opcache_get_status")) { echo json_encode(opcache_get_status(false), JSON_PRETTY_PRINT); }'
この情報をそのままGemini先生に報告すれば解析をしてくれる。
5. Xdebugをインストール
/var/log/php_errors.logにスタックトレースを表示したくてxdebugをインストール。
ただし本番環境でXdebugを有効にすることは推奨されない。
使わない場合は「xdebug.mode=off」に設定する。
参考: Xdebug: Documentation » All settings
pkg経由でインストール。
# pkg search xdebug
# pkg install php83-pecl-xdebug
確認。
# php --version
PHP 8.3.23 (cli) (built: Jul 17 2025 01:12:08) (NTS)Copyright (c) The PHP GroupZend Engine v4.3.23, Copyright (c) Zend Technologieswith Zend OPcache v8.3.23, Copyright (c), by Zend Technologieswith Xdebug v3.3.2, Copyright (c) 2002-2024, by Derick Rethans
iniファイルの場所確認。
# php --ini
Gemini先生に日本語コメント付きの設定ファイルを作ってもらった。
# less /usr/local/etc/php/ext-20-xdebug.ini
; ==============================================================================; Xdebug 3 Configuration for Production Error Analysis (Corrected for v3.3.2); 本番環境でのエラー原因特定のために一時的に使用する設定 (Xdebug 3.3.2対応版); ==============================================================================; Xdebug拡張モジュールを有効にする (これは必須です)zend_extension=xdebug.so; ------------------------------------------------------------------------------; モード設定 (Mode Configuration); ------------------------------------------------------------------------------; [設定] Xdebugの機能を指定します。'develop'はエラー時のスタックトレース表示を有効にします。; 'off'にするとXdebugは完全に無効化され、パフォーマンスへの影響がなくなります。; 原因特定後は 'off' に戻すことを強く推奨します。xdebug.mode = develop; デフォルト値: xdebug.mode = develop; ------------------------------------------------------------------------------; エラー時の動作 (Error Behavior); ------------------------------------------------------------------------------; [設定] エラーや例外が発生した時にだけXdebugの機能をアクティベートします。; これを'yes'にすることが、本番環境でのパフォーマンス影響を最小化する鍵です。; 正常なリクエストではXdebugはほぼ待機状態になります。xdebug.start_upon_error = yes; デフォルト値: xdebug.start_upon_error = default; ------------------------------------------------------------------------------; 変数表示の詳細設定 (Variable Display Details); これらの設定は、スタックトレース内の引数表示にも影響します。; ------------------------------------------------------------------------------; [設定] 配列の要素やオブジェクトのプロパティを最大何個まで表示するか。; -1は無制限。デフォルトでも十分ですが、多すぎるとログが冗長になります。; xdebug.var_display_max_children = 128; デフォルト値: xdebug.var_display_max_children = 128; [設定] 文字列の最大表示長。これを超えると省略されます。; -1は無制限。; xdebug.var_display_max_data = 512; デフォルト値: xdebug.var_display_max_data = 512; [設定] 配列やオブジェクトのネスト(階層)を最大何階層まで表示するか。; メモリ枯渇エラーのような深い呼び出しで詳細を確認するため、少し増やしておくと有効です。xdebug.var_display_max_depth = 5; デフォルト値: xdebug.var_display_max_depth = 3; ------------------------------------------------------------------------------; (オプション) Xdebug自体のログ設定; ------------------------------------------------------------------------------; Xdebugが期待通りに動作しない場合に、その原因を調査するためのログです。; 通常はコメントアウトしたままで問題ありません。; [オプション] Xdebug自体のログを出力するパス。; xdebug.log = /var/log/xdebug.log; デフォルト値: xdebug.log = "" (無効); [オプション] Xdebugのログレベル。0(crit)から10(trace)まで。7はInfoレベル。; xdebug.log_level = 7; デフォルト値: xdebug.log_level = 7
phpinfoで確認。
# php -i | grep xdebug
php-fpmを再読み込み。
# php-fpm -t
# service php_fpm reload
ブラウザでphpinfo()を表示して確認。