WordPress管理画面の「重大なエラー」を追う ── PHP-FPMメモリ枯渇問題と自動復旧の追加

発生した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、正常範囲)
特定プラグインの設定ページで発生する整数オーバーフロー的な異常 ほぼ確定(原因コード側の特定は継続調査中)

「重大なエラー」は原因がバラバラなだけに、思い込みで一つの仮説に飛びつくと時間を無駄にする。今回は、

  1. リソース状況を機械的に確認して「メモリ不足」説を消し
  2. ログの実際の中身(ファイル・行・referer・数値そのもの)を読み込んで犯人を絞り込み
  3. 根本原因の完全究明とは別に、まず実害を止める自動復旧の仕組みを先に入れる

という順番で進めた。原因調査と応急処置は必ずしも同時に終わらせる必要はなく、「止血」と「原因究明」を分けて考えるのが結果的に早かったように思う。


※この記事は調査ログを基に構成しています。実際のホスト名・PHP詳細バージョン・IPアドレス・データベース認証情報などは伏せています。

sponsored links
sponsored links
sponsored links