【Zabbix】2GBメモリのサーバーで「Disk I/O is overloaded」が発生!PHP-FPMの最適化と安定運用のノウハウ

インフラエンジニアや Web サイトの運用担当者に向けて、2GB メモリの小規模サーバーで発生した Disk I/O 高負荷(ウェイト)障害の根本原因と解決策、そしてサーバーを安定稼働させるための設計思想をまとめました。

1. 突然の「Disk I/O is overloaded」アラート

ある日、Zabbix から Disk I/O is overloaded(ディスク I/O の過負荷)のアラートが頻発。サーバーのレスポンスが著しく低下し、最悪の場合はサービス全体がフリーズする事態が発生しました。

サーバーのスペックは RAM 2GB。Zabbix のメトリクスを調査したところ、直接の原因は Disk 自体の不具合ではなく、「メモリ枯渇によるスワップ(Swap)の大量発生」 でした。

  • アクセス増加に伴い、PHP-FPM のワーカープロセスが大量生成された。
  • 物理メモリ(2GB)を食いつぶし、OS がスワップ領域(ディスク)に大量の読み書きを開始。
  • 結果として Disk I/O が悲鳴を上げ、システム全体が応答不全に陥った。

1.2トラブル調査:スワップ領域の大量発生を発見するコマンドと手順

アラート受信時、どのようにして「Disk I/O 悲鳴の原因がメモリ枯渇によるスワップ発生である」と特定したのか、実際の Linux コマンドと分析手順を解説します。

ステップ 1:`free` コマンドでメモリとスワップの使用量を確認

まずは OS 全体のメモリ空き容量とスワップ領域の使用状況を確認します。

$ free -h
              total        used        free      shared  buff/cache   available
Mem:          1.8Gi       1.6Gi        85Mi        20Mi       120Mi       100Mi
Swap:         2.0Gi       1.4Gi       600Mi

【着眼点】
Memavailable が非常に小さくなっている。
Swapused が 1.4GiB と突出して多く消費されている。
これだけで物理メモリが不足し、ディスク領域(Swap)に退避していることが分かります。

ステップ 2:`vmstat` コマンドでリアルタイムの I/O とスワップ頻度を特定

スワップの発生(読み書き)が現在進行形で Disk I/O を圧迫しているかを確認するため、1秒間隔で vmstat を実行します。

$ vmstat 1 5
procs -----------memory---------- ---swap-- -----io---- -system-- ------cpu-----
 r  b   swpd   free   buff  cache   si   so    bi    bo   in   cs us sy id wa st
 2  3 1468000  87040   2100 122880 4096 8192  5120  9200 3200 6500 15 25  0 60  0

【着眼点】
si(Swap-In: スワップからの読み出し)と so(Swap-Out: スワップへの書き込み)の数値が継続的に発生している。
procsb(I/O 待ちプロセス数)が数個溜まっている。
cpuwa(I/O Wait 時間の割合)が 50〜60% 以上に跳ね上がっている。
これにより、「スワップの連続発生 ➔ ディスク I/O の過負荷(wa 上昇)」という因果関係が確定します。

ステップ 3:`ps` / `top` コマンドでメモリを食いつぶしているプロセスを特定

どのプロセスがメモリを消費しているか、メモリ消費量(%MEM / RSS)順にソートして特定します。

# メモリ消費量の多い順に上位10件のプロセスを表示
$ ps aux --sort=-%mem | head -n 11

実行結果、php-fpm: pool www のワーカープロセスがずらりと並び、各プロセスが数%ずつのメモリを占有しながら合計 30 個以上起動していることが判明しました。これで犯人が「PHP-FPM の過剰なプロセス生成」であると完全に特定できました。

2. 問題解決:原因となった設定の見直し

原因は PHP-FPM(www.conf)の pm.max_children = 36 という過大な設定でした。

1 プロセスあたり約 40MB のメモリを消費すると仮定した場合、40MB × 36 = 約 1.44GB の計算になります。OS や DB(MySQL)が動作している 2GB メモリの環境で 1.44GB を PHP だけに割り当てれば、物理メモリが枯渇するのは当然です。

2GB メモリの制約内で絶対にスワップを発生させないため、pm.max_children = 10 に引き下げました(最大でも 400MB〜500MB 程度に抑制)。

「上限を絞ると 503 エラーが増えるのでは?」という疑問について
プロセス上限に達しても即座にエラーになるわけではなく、Apache 側のキュー(行列)に入り順次処理されます。「全員巻き込んでサーバーごと落ちる」状態から、「一時的な待ち時間で安全にやり過ごす」運用へと移行できました。

3. 監視項目:Zabbix で見るべき 4 つの重要メトリクス

PHP-FPM の状態を可視化するため、Apache 経由で /status および /ping を Zabbix エージェントから取得できるように設定しました。特に注視すべきメトリクスは以下の 4 つです。

  1. PHP-FPM: Processes, active
    リアルタイムで処理中のプロセス数。常に上限に張り付いている場合は処理の停滞を疑う。
  2. PHP-FPM: Memory usage (rss)
    PHP-FPM 全体の実メモリ消費量。サーバーの空きメモリに対して安全圏か確認。
  3. PHP-FPM: Max children reached
    プロセス上限(pm.max_children)に達した回数。この値が頻繁に増える場合は、メモリ増設またはプログラム改修の検討基準となる。
  4. PHP-FPM: Slow requests
    設定時間を超えた「遅いリクエスト」の発生回数。

4. 設定内容:本番環境に適用したパラメータと各種設定

トラブル防止と障害解析のために適用した各種設定ファイルの内容です。

① PHP-FPM(www.conf)の設定

/etc/php-fpm.d/www.conf を編集し、メモリ消費の制限とステータス出力の有効化、スローログを設定します。

; 制御方式(動的生成)
pm = dynamic

; 2GBメモリに合わせて安全な上限値(10)に制限
pm.max_children = 10
pm.start_servers = 3
pm.min_spare_servers = 2
pm.max_spare_servers = 6

; メモリリーク(蓄積)防止:500リクエストごとにプロセスを再生成
pm.max_requests = 500

; ステータス・Ping ページの有効化(Zabbix 連携用)
pm.status_path = /status
ping.path = /ping
ping.response = pong

; スローログの有効化(3秒以上かかった重い処理を記録)
request_slowlog_timeout = 3s
slowlog = /var/log/php-fpm/www-slow.log

※注意: pm.max_children を変更する際は、pm.max_spare_servers などの数値が max_children を超えないようバランスを調整してください(超えると PHP-FPM が起動エラーを起こします)。

② Apache(00-php-fpm-status.conf)の設定

他の VirtualHost や Rewrite 規則、認証処理に邪魔されず、ローカル(127.0.0.1)から直接 PHP-FPM のステータスを取得できるよう /etc/httpd/conf.d/00-php-fpm-status.conf を作成します。ドメイン設定(example.com等)の影響を受けないように個別にルーティングを上書きします。

<LocationMatch "^/(status|ping)">
    # PHP-FPM ソケットへ直接プロキシ伝送
    SetHandler "proxy:unix:/run/php-fpm/www.sock|fcgi://localhost"
    RewriteEngine Off

    # 既存の認証設定を無効化
    AuthType None
    Satisfy Any

    # ローカルアクセスのみ許可(外部非公開)
    <RequireAny>
        Require ip 127.0.0.1
        Require ip ::1
    </RequireAny>
</LocationMatch>

② Apache(00-php-fpm-status.conf)の設定

他の VirtualHost や Rewrite 規則、認証処理に邪魔されず、ローカル(127.0.0.1)から直接 PHP-FPM のステータスを取得できるよう /etc/httpd/conf.d/00-php-fpm-status.conf を作成します。ドメイン設定(example.com等)の影響を受けないように個別にルーティングを上書きします。

<LocationMatch "^/(status|ping)">
    # PHP-FPM ソケットへ直接プロキシ伝送
    SetHandler "proxy:unix:/run/php-fpm/www.sock|fcgi://localhost"
    RewriteEngine Off

    # 既存の認証設定を無効化
    AuthType None
    Satisfy Any

    # ローカルアクセスのみ許可(外部非公開)
    <RequireAny>
        Require ip 127.0.0.1
        Require ip ::1
    </RequireAny>
</LocationMatch>

【設定反映のプロセス】

設定ファイルの更新後、Web サービスを止めることなく安全に設定を反映・確認するための標準的な手順です。

ステップ 1:構文チェック(文法エラーの確認)
設定変更後にいきなり再起動すると、記述ミスがあった場合に Apache が起動できずサービス停止につながります。必ず事前チェックを行います。

$ sudo apachectl configtest
# または (RHEL/CentOS系)
$ sudo httpd -t

Syntax OK と表示されることを確認します。エラーが表示された場合は該当行の記述を見直してください。

ステップ 2:PHP-FPM と Apache の設定反映
構文エラーがないことを確認したら、サービスを再読み込み(または再起動)します。PHP-FPM の www.conf を変更した場合は PHP-FPM のリロードも必須です。

# 既存の接続を切断せずに設定を再読み込み(推奨)
$ sudo systemctl reload php-fpm
$ sudo systemctl reload httpd   # Ubuntu/Debian系の場合は apache2

ステップ 3:ローカルからの導通・動作確認
curl コマンドを使い、ローカルホスト(127.0.0.1)経由でステータスが正しく返ってくるかテストします。

# /ping ページの確認(pong が返ればOK)
$ curl -i http://127.0.0.1/ping
HTTP/1.1 200 OK
...
pong

# /status ページの確認(ステータス情報が返ればOK)
$ curl -i http://127.0.0.1/status
HTTP/1.1 200 OK
...
pool:                 www
process manager:      dynamic
start time:           ...

HTTP/1.1 200 OK が返り、ステータス情報が取得できれば設定反映は完了です。

③ Zabbix 側の設定

Zabbix サーバーから PHP-FPM の状態を自動収集するための設定です。

  • テンプレートの適用: 対象ホストに Zabbix 公式の PHP-FPM by HTTP テンプレートをリンクします。
  • マクロ(変数)の設定: ホストのマクロタブで以下のように設定します。
    {$PHP_FPM.PING.PAGE}status?ping
    {$PHP_FPM.STATUS.PAGE}status
    {$PHP_FPM.SCHEME}http
    {$PHP_FPM.PORT}80

設定後、curl -i http://127.0.0.1/status を実行し、HTTP/1.1 200 OK とステータス情報(または pong)が正しく返ってくることを確認すれば監視の導通は完了です。

5. PHP 切り分けの重要性(モジュール版 vs PHP-FPM)

Apache のモジュール版(mod_php)と FastCGI 版(PHP-FPM)には、運用監視において決定的な違いがあります。

項目 モジュール版 (mod_php) PHP-FPM (fastcgi)
プロセス管理 Apache プロセス内部で動作 Apache と別プロセスで独立
メモリの独立性 Apache 全体でメモリを消費 PHP 専用のメモリ上限を厳密に設定可能
ステータス監視 大まかな Web サーバー状態のみ プロセス数・Ping・スローログ等の詳細取得が可能

モジュール版は「1つの重い PHP リクエストが Web サーバー全体のメモリを侵食する」リスクが高く、制御が困難です。安定運用と細かな監視を行うためには、PHP-FPM 構成への移行が強く推奨されます

6. サイトとバッチの切り分け(OS の設定・リソース制御)

同一サーバー内で重いバッチ処理(Cron等)を実行すると、Web サイト側のレスポンスが巻き込まれて悪化します。これらを OS / プロセスレベルで分離・制限するノウハウです。

① nice と ionice で優先度を下げる(最も手軽)

バッチ処理を実行する際、CPU と Disk I/O の優先度を最低(Idle)に下げて実行します。

# Cron などの記述変更例
nice -n 19 ionice -c 3 php /path/to/batch.php

これにより、OS は Web 側のアクセス処理を優先し、バッチは「CPU・Disk が空いている隙間時間」にだけ実行されます。

② PHP-FPM プール(Pool)の分離

Web サイト用(www.conf)とは別に、バッチ専用のプール設定ファイルを作成してソケットを分けることで、バッチ処理が大量のメモリを消費したり Web 側の同時実行枠(max_children)を食いつぶしたりするのを防げます。

手順 1:バッチ専用プールファイル(/etc/php-fpm.d/batch.conf)を作成

既存の www.conf をベースに、バッチ用プール [batch] を作成します(Ubuntu/Debian系の場合は /etc/php/8.x/fpm/pool.d/batch.conf)。

$ sudo cp /etc/php-fpm.d/www.conf /etc/php-fpm.d/batch.conf
$ sudo vi /etc/php-fpm.d/batch.conf

batch.conf 内の設定を以下のように調整します。

; プール名を [www] から [batch] に変更
[batch]

; ワーカープロセスの実行ユーザー/グループ
user = apache
group = apache

; 専用ソケットファイルのパスを指定(www.sock と分ける)
listen = /run/php-fpm/batch.sock
listen.owner = apache
listen.group = apache
listen.mode = 0660

; 制御方式(オンデマンド生成:実行時のみプロセス起動しメモリ節約)
pm = ondemand
; バッチ処理の最大同時実行数を安全な値に制限
pm.max_children = 2
; 処理終了後にプロセスを自動解放するまでの待機時間
pm.process_idle_timeout = 10s

; メモリリーク防止(バッチ処理ごとにプロセス再生成)
pm.max_requests = 100

; CLI/スクリプト実行用の個別 PHP 設定(メモリ上限やタイムアウトを柔軟に拡大)
php_admin_value[memory_limit] = 256M
php_admin_value[max_execution_time] = 300

手順 2:設定ファイルの構文チェックと反映

PHP-FPM の構文チェックを実行し、サービスを再読み込みして batch.sock が生成されたことを確認します。

# 構文チェック
$ sudo php-fpm -t

# 設定の再読み込み
$ sudo systemctl reload php-fpm

# 専用ソケットファイルが生成されたことを確認
$ ls -l /run/php-fpm/batch.sock
srw-rw----+ 1 apache apache 0  9月 12 10:00 /run/php-fpm/batch.sock

手順 3:Cron や CLI コマンドから専用プール経由で実行

バッチ処理を CLI や Cron で呼び出す際、cgi-fcgi コマンドを使って専用ソケット(batch.sock)を経由して実行します。

# cgi-fcgi パッケージが未インストールの場合は導入(CentOS/RHEL系: fcgi / Ubuntu: libfcgi-bin)
$ sudo dnf install fcgi

# Cron 設定例(nice/ionice と組み合わせて指定のソケット経由で実行)
# /etc/cron.d/my-batch
0 3 * * * root SCRIPT_FILENAME=/var/www/html/batch.php REQUEST_METHOD=GET nice -n 19 ionice -c 3 cgi-fcgi -bind -connect /run/php-fpm/batch.sock

これにより、バッチ処理は batch.sock(上限 2 プロセス)の枠組み内でのみ実行され、Web サイト側(www.sock)のリソースを阻害することが完全になくなります。

7. まとめ:サーバーを安定させるには

今回の障害対応を通じて得られた、サーバーを長期間安定して稼働させるための極意は以下の 3 点です。

  1. 「制限」は防波堤
    サーバーの物理スペック(特に RAM)を超えないパラメータ設計を徹底する。リクエストを制限することは「落ちる」ことではなく「守る」こと。
  2. 可視化(監視)なくして改善なし
    単に CPU / メモリ使用量を見るだけでなく、ミドルウェア内部のステータス(PHP-FPM Status / Slowlog など)を取得してボトルネックを早期発見する。
  3. リソースのアイソレーション(分離)
    Web リクエストとバッチ処理、あるいは複数サイト間でリソースを共有させず、優先度制御やプール分離によって「共連れ障害」を防ぐ構造を作る。
自由な働き方をしてみませんか
フリーランスといえばクラウドソーシングが連想されると思います。 しかし「クラウドワークス」や「ランサーズ」調べてみると仕事の単価が非常に安い。 せっかく身につけた技術力がクラウドソーシングでは全く価値がなくなってしまいます。
そこでオススメなのがリモートワークの案件が多く翌月15日報酬として振り込まれる
クラウドテック」です。 掲載案件では平均月60万週3~4日勤務の案件が豊富生活スタイルに合わせた働き方を設計できます。 また福利厚生が充実し旅行、レジャー、家事代行、ヘビーシッター、健康診断など100種類以上を無料で使えます。
  • 97%がリモートのお仕事
  • 週4日、週3日OKのお仕事多数
  • 登録社数74万件業界トップクラス
  • 2〜4週間程度でお仕事決定
まだ間に合います。まずは無料登録をして自分に合った案件がないか確認してみてください。きっと魅力的な案件が見つかることでしょう。

シェアする

  • このエントリーをはてなブックマークに追加

フォローする