昨日、チームに「サイトの読み込みが遅い、クレームが来ている」というチケットが届いた。サーバーにSSH接続してps aux | grep php-fpmを実行し、プロセスを一つひとつ手で数えた——PHP-FPMのワーカーが枯渇しリクエストがキューに詰まっていると判明するまで10分近くかかった。監視ダッシュボードがあれば、ユーザーが気づく前に検知できたはずだ。
監視環境を導入する前は、サーバーに一台ずつSSHして確認する必要があった——今はダッシュボードを開くだけですべてが見渡せる。この記事では、Prometheusを使ったPHP-FPM監視の構築方法を紹介する。特に問題を引き起こしやすい3つの指標:active processes、idle workers、slow requestsに絞って解説する。
なぜPHP-FPMを個別に監視する必要があるのか?
PrometheusとGrafanaはCPU、RAM、ディスクを監視できるが、それはサーバーの状態であり、PHPアプリケーションの状態ではない。CPUがまだ20%アイドルのままでも、PHP-FPMのワーカーが詰まっている場合がある。この2つは完全に独立している。
PHP-FPMはプロセスプールモデルで動作する:各PHPリクエストはワーカープロセスが処理する。プールにはワーカーの最大数(pm.max_children)が設定されており、すべてのワーカーがビジー状態になるとリクエストはキューで待機する。キューが満杯になるとサーバーは502または504エラーを返す。
監視すべき3つのコア指標:
- Active Processes:現在リクエストを処理中のワーカー数
- Idle Workers:アイドル状態で新しいリクエストを受け付けられるワーカー数
- Slow Requests:処理に時間がかかりすぎたリクエスト(
request_slowlog_timeoutの閾値に基づく)
active processesがpm.max_childrenに近づきidle workersが0に近づいたとき、PHP-FPMが詰まり始めているサインだ。slow requestsが急増する場合は、データベースクエリの遅延や外部APIのタイムアウトが原因である可能性が高い。
PHP-FPMステータスページを有効にする
PHP-FPMにはメトリクスを返すエンドポイントが標準で用意されており、有効化するだけでよい。プール設定ファイル(通常は/etc/php/8.x/fpm/pool.d/www.conf)を開く:
sudo nano /etc/php/8.2/fpm/pool.d/www.conf
以下の行を探してアンコメント(または追記)する:
; ステータスページを有効化
pm.status_path = /status
; スローリクエストログを有効化
request_slowlog_timeout = 5s
slowlog = /var/log/php-fpm/slow.log
PHP-FPMを再起動する:
sudo systemctl restart php8.2-fpm
ステータスページが動作しているか確認する(Unix socket経由):
sudo -u www-data php-fpm8.2 -d "error_log=/dev/null" 2>/dev/null
curl --unix-socket /run/php/php8.2-fpm.sock http://localhost/status
# TCPポートを使用している場合:
curl http://127.0.0.1:9000/status
出力はactive processes、idle processes、slow requestsなどの指標をテキスト形式で返す…
Nginxを使用している場合、内部エンドポイントを公開するためにlocationブロックを追加する:
server {
listen 127.0.0.1:9001; # 内部アクセスのみ許可
location /status {
fastcgi_pass unix:/run/php/php8.2-fpm.sock;
fastcgi_param SCRIPT_FILENAME $document_root$fastcgi_script_name;
include fastcgi_params;
}
}
sudo nginx -t && sudo systemctl reload nginx
curl http://127.0.0.1:9001/status
php-fpm_exporterをインストールする
PrometheusはPHP-FPMのテキスト形式を直接読み取れないため、変換用のexporterが必要だ。ここではhipages製のphp-fpm_exporterを使用する(Goバイナリで軽量、ランタイム不要)。
# 最新バイナリをダウンロード
wget https://github.com/hipages/php-fpm_exporter/releases/download/v2.2.0/php-fpm_exporter_2.2.0_linux_amd64.tar.gz
tar xzf php-fpm_exporter_2.2.0_linux_amd64.tar.gz
sudo mv php-fpm_exporter /usr/local/bin/
sudo chmod +x /usr/local/bin/php-fpm_exporter
自動起動のためにsystemdサービスを作成する:
sudo nano /etc/systemd/system/php-fpm-exporter.service
[Unit]
Description=PHP-FPM Exporter for Prometheus
After=network.target php8.2-fpm.service
[Service]
Type=simple
User=www-data
ExecStart=/usr/local/bin/php-fpm_exporter \
--phpfpm.scrape-uri="tcp://127.0.0.1:9000/status" \
--web.listen-address=":9253"
Restart=on-failure
[Install]
WantedBy=multi-user.target
PHP-FPMがTCPではなくUnix socketを使用している場合:
ExecStart=/usr/local/bin/php-fpm_exporter \
--phpfpm.scrape-uri="unix:///run/php/php8.2-fpm.sock;/status" \
--web.listen-address=":9253"
sudo systemctl daemon-reload
sudo systemctl enable --now php-fpm-exporter
# メトリクスが正しくexportされているか確認
curl http://localhost:9253/metrics | grep phpfpm
出力には以下のようなメトリクスが表示される:
phpfpm_active_processes 3
phpfpm_idle_processes 7
phpfpm_max_children_reached_total 0
phpfpm_slow_requests_total 12
phpfpm_listen_queue 0
phpfpm_max_listen_queue 5
PrometheusにPHP-FPMのスクレイプを設定する
prometheus.ymlに新しいジョブを追加する:
scrape_configs:
# ... 既存のジョブ ...
- job_name: 'php-fpm'
static_configs:
- targets: ['localhost:9253']
labels:
server: 'web01'
pool: 'www'
scrape_interval: 15s
Prometheusをリロードする:
sudo systemctl reload prometheus
# ターゲットがupになっているか確認
curl http://localhost:9090/api/v1/targets | python3 -m json.tool | grep -A5 php-fpm
GrafanaでダッシュボードとAlertを作成する
新しいダッシュボードを作成し、PromQLクエリでパネルを追加する:
Panel 1 — Active vs Idle Workers (Graph):
# アクティブプロセス
phpfpm_active_processes{job="php-fpm"}
# アイドルプロセス
phpfpm_idle_processes{job="php-fpm"}
# Max children(最大上限)
phpfpm_max_active_processes{job="php-fpm"}
Panel 2 — Slow Requests (Rate per minute):
rate(phpfpm_slow_requests_total{job="php-fpm"}[5m]) * 60
Panel 3 — Listen Queue(待機中のリクエスト数):
phpfpm_listen_queue{job="php-fpm"}
Panel 4 — Worker Utilization %(最も重要):
phpfpm_active_processes / (phpfpm_active_processes + phpfpm_idle_processes) * 100
この値が80%を超えたら注意が必要で、95%を超えるとPHP-FPMが詰まりかけている状態だ。
キューに積み上がりが始まった場合のAlert RuleをGrafana(またはAlertmanager)に設定する:
# alerting_rules.yml
groups:
- name: php-fpm
rules:
- alert: PHPFPMHighWorkerUtilization
expr: phpfpm_active_processes / (phpfpm_active_processes + phpfpm_idle_processes) * 100 > 85
for: 2m
labels:
severity: warning
annotations:
summary: "{{ $labels.server }} でPHP-FPMワーカーが不足しています"
description: "Worker utilizationが{{ $value | printf \"%.1f\" }}%に達しており、リクエストがキューに入るリスクがあります"
- alert: PHPFPMListenQueueFull
expr: phpfpm_listen_queue > 0
for: 30s
labels:
severity: critical
annotations:
summary: "PHP-FPMキューに待機リクエストが積み上がっています"
description: "{{ $labels.server }} で{{ $value }}件のリクエストがワーカーの空きを待っています"
実際の数値をどう読むか
ダッシュボードを見ていてよく見かけるパターンをいくつか紹介する:
- Activeが急増してから正常に戻る:一時的なトラフィックスパイクで、プールが十分大きければ処理できている——心配不要。
- Activeが高いまま維持され、Idleがほぼ0の状態が続く:ワーカーが不足している。
pm.max_childrenを増やすべきだが、RAM消費も考慮すること(PHP-FPMワーカー1つあたり約30〜50MB)。 - 特定の時間帯にSlow requestsが急増する:重いcronジョブや定期的なレポートクエリが原因の可能性がある。クエリを最適化するか、別のキューに分離する必要がある。
- Listen queue > 0:ユーザーがサイトの遅延を感じている状態だ。即座に対処が必要。
目安としてpm.max_children = 空きRAM(MB)÷ 50で設定している。例えばPHP-FPM用に2GBのRAMが確保できる場合、最大40ワーカーとなる。
まとめ
PrometheusによるPHP-FPM監視は難しくない——ステータスページを有効にして小さなexporterを起動するだけで、必要なメトリクスがすべて揃う。重要なのは数値の読み方だ:worker utilizationが継続的に高い状態こそ問題であり、短いスパイクは正常の範囲内だ。
ダッシュボードがあれば、「PHP-FPMのワーカー不足によるサイト遅延」と「データベースクエリの遅延によるサイト遅延」を区別できる——この2つは解決策がまったく異なる。監視がなければ、ワーカーを増やしても原因が別のところにあって依然として遅いままということが起きてしまう。

