発生したWSOD事象
ある日、wp-admin にアクセスすると次のような画面が表示されるようになった。
このWebサイトに重大なエラーが発生しました。サイト管理者のメール受信箱で手順を確認してください。
いわゆるWordPressのWSOD(White Screen of Death)保護画面だ。フロントは生きているのに管理画面だけ死んでいる、というパターンもあれば、フロント側まで巻き込まれることもあった。
とりあえずの応急処置は分かっている。
sudo systemctl restart php8.x-fpm
これで大抵は復旧する。だが毎回手動で気づいて叩くのも限界があるので、まず「なぜ起きるのか」を調べ、その上で「検知したら自動で直す」仕組みを作ることにした。
調査ステップ1: PHP-FPMのプール設定とメモリ状況
最初に疑ったのは単純なリソース不足だ。php8.x-fpmのプール設定を確認する。
sudo grep -E "^(pm|pm\.max_children|pm\.start_servers)" /etc/php/8.x/fpm/pool.d/*.conf
pm = dynamic
pm.max_children = 10
pm.start_servers = 3
pm.min_spare_servers = 2
pm.max_spare_servers = 5
サーバー全体のメモリも確認する。
free -h
total used free shared buff/cache available
Mem: 30Gi 8.3Gi 14Gi 151Mi 8.7Gi 22Gi
Swap: 2.0Gi 0B 2.0Gi
- 総メモリ30GBに対して使用量は8.3GB、空きは22GBと余裕がある
- スワップ使用量は0B ― メモリが逼迫している形跡はない
- FPMプロセスの平均RSSは1プロセスあたり154MB程度で、特に異常な値ではない
journalctl -kでOOM Killerの発動履歴を確認したが該当なしphp8.x-fpmサービス自体のクラッシュ・異常再起動もログ上には見当たらない
この時点で、「サーバー全体のリソース不足」という線はほぼ消えた。
調査ステップ2: Apacheのエラーログから本丸を発見
サーバー全体は健全なのに管理画面だけ死ぬ ― となれば、特定のリクエストが引き金になっている可能性が高い。Apacheのエラーログを掘る。
sudo tail -100 /var/log/apache2/error.log
決定的な行が見つかった。
PHP Fatal error: Allowed memory size of 805306368 bytes exhausted
(tried to allocate 4295229440 bytes)
in /wp-includes/theme.php on line 189
referer: https://example.com/wp-admin/themes.php?page=xxxxx-settings
805306368 bytes= ちょうど768MB。これはプールに設定していたmemory_limitと一致する4295229440 bytes≈ 4GB強。通常のWordPress処理でこの量を一度に確保することはまずあり得ない
過去ログも遡ったところ、この現象は特定の日の数分間に、同一のURL・同一ファイル・同一行番号で連続発生していたことが分かった。つまり「管理画面全体がじわじわ重くなって死ぬ」のではなく、「特定の管理画面ページにアクセスすると即座にクラッシュする」タイプの問題だった。
寄り道: fail2banを疑ったが違った
途中で「fail2banが変な挙動をしているのでは」という仮説も検証した。
sudo fail2ban-client status
sudo fail2ban-client status apache-auth
Currently banned: 0
Total banned: 0
該当IPのBAN履歴もゼロ。そもそも今回のエラーは、fail2banが遮断するような「接続レベルの拒否」ではなく、リクエストがApache/PHP-FPMまで到達し、PHPが実際にコードを実行した結果としてクラッシュしているものだった。fail2banのログにも該当する痕跡は一切なく、この仮説は棄却した。
調査ステップ3: DBの肥大化を疑ったが違った
もう一つの定番の疑いどころとして、wp_optionsテーブルのautoload=yesデータの肥大化がある。管理画面はほぼ全ページでこれを読み込むため、ここが壊れているとどのページでも同じ症状が出やすい。
SELECT SUM(LENGTH(option_value)) AS total_bytes, COUNT(*) AS count
FROM wp_options WHERE autoload='yes';
total_bytes: 314593 count: 449
約307KB。449件。これはまったく正常な範囲で、DB肥大化説も除外された。
原因の特定: 数字の裏にあった整数オーバーフロー
ここで改めてエラーメッセージの数値に注目した。
tried to allocate 4295229440 bytes
この数字、実は 2の32乗(4,294,967,296)+ 262,144 というほぼキリのいい値になっている。通常のWordPress処理でこんな中途半端に巨大な確保をすることはあり得ない。これは典型的に、負の数や不正な計算結果が符号なし32bit整数として解釈され、「ほぼ4GB」という異常値に化けたときに出るパターンだ。
クラッシュの発生元はtheme.php189行目、get_stylesheet()関数。
function get_stylesheet() {
return apply_filters( 'stylesheet', get_option( 'stylesheet' ) );
}
関数自体はapply_filters('stylesheet', ...)を呼んでいるだけのシンプルな1行だ。つまり犯人はこの関数自体ではなく、stylesheetフィルターに処理を追加している何らかのプラグインが、想定外の値(壊れた設定データなど)を返しているということになる。今回のケースでは、リクエストのreferer(?page=xxxxx-settings)から、あるダークモード切り替え系プラグインの設定ページがトリガーになっていることまでは特定できた。
プラグイン自体を無効化する対応も選択肢としてはあったが、今回はあえてプラグイン構成には手を入れず、症状が出たら自動で復旧する仕組みを先に整えることにした。
対策: 検知したら自動でphp-fpmを再起動する
1. pm.max_requestsを設定する
そもそもpm.max_requestsが未設定(無制限)だった。WordPressはプラグインを多数積むとメモリリークしやすい環境なので、一定リクエストごとにワーカーを再生成させておくのは基本的な予防策になる。
sudo sed -i '/pm.max_spare_servers = 5/a pm.max_requests = 500' /etc/php/8.x/fpm/pool.d/www.conf
sudo systemctl reload php8.x-fpm
2. 監視スクリプト
フロントページを定期的にcurlし、「重大なエラー」の文言が出ていたらphp8.x-fpmを再起動するだけのシンプルなスクリプト。
#!/bin/bash
# /usr/local/bin/check-wp-critical-error.sh
SITE_URL="https://example.com/"
ERROR_STRING="重大なエラーが発生しました"
LOG_FILE="/var/log/wp-critical-error-restart.log"
RESPONSE=$(curl -s --max-time 10 "$SITE_URL")
if echo "$RESPONSE" | grep -q "$ERROR_STRING"; then
TIMESTAMP=$(date '+%Y-%m-%d %H:%M:%S')
echo "[$TIMESTAMP] Critical error detected. Restarting php8.x-fpm." >> "$LOG_FILE"
systemctl restart php8.x-fpm
fi
3. systemdタイマーで2分おきに実行
cronでもいいが、実行ログやステータス確認のしやすさからsystemdタイマーを選んだ。
# /etc/systemd/system/wp-critical-check.service
[Unit]
Description=Check WordPress critical error and restart php-fpm
[Service]
Type=oneshot
ExecStart=/usr/local/bin/check-wp-critical-error.sh
# /etc/systemd/system/wp-critical-check.timer
[Unit]
Description=Run WP critical error check every 2 minutes
[Timer]
OnBootSec=1min
OnUnitActiveSec=2min
[Install]
WantedBy=timers.target
sudo chmod +x /usr/local/bin/check-wp-critical-error.sh
sudo systemctl daemon-reload
sudo systemctl enable --now wp-critical-check.timer
これで「重大なエラー」を検知してから最大2分以内に自動復旧するようになった。再起動が発生した履歴は/var/log/wp-critical-error-restart.logに溜まっていくので、後で頻度を見て「これはやはり根本対応が必要だ」と判断する材料にもなる。
まとめ
| 疑ったもの | 結果 |
|---|---|
| サーバー全体のメモリ不足 | 否定(30GB中22GB空き、スワップ未使用) |
| fail2banによる誤ブロック | 否定(BAN履歴なし、リクエストはPHPまで到達していた) |
| wp_optionsのautoload肥大化 | 否定(約307KB、正常範囲) |
| 特定プラグインの設定ページで発生する整数オーバーフロー的な異常 | ほぼ確定(原因コード側の特定は継続調査中) |
「重大なエラー」は原因がバラバラなだけに、思い込みで一つの仮説に飛びつくと時間を無駄にする。今回は、
- リソース状況を機械的に確認して「メモリ不足」説を消し
- ログの実際の中身(ファイル・行・referer・数値そのもの)を読み込んで犯人を絞り込み
- 根本原因の完全究明とは別に、まず実害を止める自動復旧の仕組みを先に入れる
という順番で進めた。原因調査と応急処置は必ずしも同時に終わらせる必要はなく、「止血」と「原因究明」を分けて考えるのが結果的に早かったように思う。
※この記事は調査ログを基に構成しています。実際のホスト名・PHP詳細バージョン・IPアドレス・データベース認証情報などは伏せています。
※本記事は当サイト管理人の個人的な備忘録です。情報内容には配慮しておりますが、本記事の参照または付随ソースコード利用後にいかなる損害が発生しても、当サイト及び管理人はいっさいの責任を負いません。
