ps aux の出力を1列ずつ解剖
障害対応でpsを打っても数字の意味が曖昧なままでは切り分けが進まない。ps auxの11列を1列ずつ精読し、%CPUが平均値である理由、VSZとRSSの差、D状態の意味まで現場で効く読み方を身につける。
- ps auxは11列でプロセスを示す。%CPUは現在値ではなく、起動後の累積CPU時間を経過時間で割った平均だ。今この瞬間の負荷を調べるならtopを使う。
- VSZは未使用領域や共有ライブラリも含む仮想アドレス空間の総量。RSSは物理メモリ上の量だが、共有部分を各プロセスへ重複計上するため、RSSの全件合計は実使用量にならない。
- STATの先頭は状態で、Rは実行可能、Sは割り込み可能待ち、DはI/Oなどの割り込み不可待ち、Zはゾンビ、Tは停止。D状態はkill -9でも消えず、ディスクやネットワークの応答側を調べる。
このコマンドは何を見せているか
psが見せているのは、コマンドを打った瞬間のプロセステーブルの写真だ。topのような更新され続ける計器ではない。実体はカーネルが/proc配下に置く各PIDのディレクトリで、psはその擬似ファイルを一巡して読んで並べるだけだ。この「1回読んで終わり」が%CPUの解釈を左右する。
auxにハイフンが付かないのも誤記ではない。psにはハイフンなしのBSD形式と、ハイフン付きのUNIX(System V)形式という2つの流儀がある。auxはBSD形式のオプション3つの組で、aは自分以外のユーザーのプロセスも含める、uは詳細な列構成にする、xは制御端末を持たないプロセスも含める。aとxで初めて「全プロセス」になり、uが以下の11列を決める。
ps aux と ps -aux は本来別物だ。後者はUNIX形式と解釈され、-uがユーザー指定のオプションになるため、xという名前のユーザーのプロセスを表示せよという意味になる。procps-ngはxが存在しないときだけ警告付きでauxと解釈する。
プロセスやメモリ管理の仕組み自体はOSの話題であり、本稿は読解に集中する。
横にスクロール
1列ずつ解剖
題材は、アプリケーションサーバー1台で ps aux を打った出力の抜粋だ。現在時刻は7月16日14時33分とする。
USER PID %CPU %MEM VSZ RSS TTY STAT START TIME COMMAND
root 1 0.0 0.3 168404 12996 ? Ss Jul02 3:41 /sbin/init
root 2 0.0 0.0 0 0 ? S Jul02 0:00 [kthreadd]
root 14 0.0 0.0 0 0 ? I< Jul02 0:00 [kworker/0:0H-events_highpri]
root 412 0.0 0.0 0 0 ? S Jul02 0:53 [jbd2/nvme0n1p2-8]
root 688 0.0 0.3 96820 15612 ? Ss Jul02 0:22 /usr/sbin/sshd -D
www-data 902 0.4 0.9 214880 38112 ? S Jul02 84:12 nginx: worker process
deploy 2148 78.6 23.6 4198520 992304 ? Sl 09:41 229:31 java -Xmx2g -jar app.jar
deploy 2210 0.1 0.0 0 0 ? Z 14:02 0:02 [convert] <defunct>
deploy 2884 0.4 0.2 28560 9884 ? D 14:28 0:01 rsync -a /mnt/nfs/backup/ .
deploy 3055 0.0 0.1 17092 5240 pts/0 Ss 14:02 0:00 -bash
deploy 3120 0.0 0.0 21536 3204 pts/0 T 14:05 0:00 vim deploy.log
deploy 3390 12.0 0.6 246220 25640 ? RN 14:31 0:14 python3 /opt/batch/aggregate.py
deploy 3402 0.0 0.0 15008 2100 pts/0 R+ 14:33 0:00 ps aux
USER — どの権限で動いているか
先頭は実効ユーザーIDを名前に解決した値だ。実効ユーザーとは、そのプロセスが今どの権限でファイルやシステムコールへアクセスできるかを決める主体で、起動した本人(実ユーザー)と一致するとは限らない。setuidやsudo経由では両者がずれる。
PID — 一意だが永久ではない
プロセスID。1はinit(今日ではsystemdが多い)、2はkthreaddで、カーネルスレッドはすべて2の子孫だ。PIDは/proc/sys/kernel/pid_maxまで昇順に払い出され、上限で小さい値へ折り返して再利用される。「今は一意」でも「永久に一意」ではない。数分前にメモしたPIDへkillを撃つのは、同じ番号が別物へ再配布されている危険を伴う。
%CPU — 現在値ではなく起動からの平均
ここが最大の落とし穴だ。%CPUは、そのプロセスが起動してから今までに消費したCPU時間を、同じ区間の経過時間で割った値にすぎない。javaの78.6は目を引くが、これは「今78.6パーセント使っている」ではなく「09:41の起動から約4時間52分、ならして78.6パーセント使ってきた」という意味だ。逆に、たった今100パーセントへ張り付いたプロセスでも、起動が2週間前なら%CPUは0.0のままである。なおSMP環境では複数スレッドが並列に回れば100を超える。
%MEM — RSSを総物理メモリで割っただけ
%MEMは後述のRSSを総物理メモリ(/proc/meminfoのMemTotal)で割った百分率だ。javaはRSSが992304、%MEMが23.6なので、この環境の物理メモリはおよそ4GiBと逆算できる。RSS由来なので、RSSが抱える問題をそのまま引き継ぐ。スワップへ追い出された分は含まれない。
VSZ と RSS — 宣言と実体
この2列は単位がどちらもKBで隣り合うが、意味はほとんど正反対だ。
| 列 | 何を表すか | 含まれるもの | 落とし穴 |
|---|---|---|---|
| VSZ | 仮想アドレス空間の総量(KB) | 未使用の予約領域、mmapしたファイル、共有ライブラリの全マップ | 物理メモリ消費と直結しない。大きくても異常とは限らない |
| RSS | 物理メモリに載っているページの合計(KB) | 実際に触ったページ、常駐している共有ライブラリ | 共有ページを全プロセスへ重複計上。合計しても実使用量にならない |
VSZ(Virtual Size)は仮想アドレス空間の総量だ。確保しただけで一度も書き込んでいない領域、共有ライブラリの全マップ、JVMが予約したヒープ上限まで、「使うかもしれないと宣言した空間」がすべて入る。javaがVSZ約4.2GBに対しRSS約970MBなのは異常ではなく、-Xmx2gの予約が仮想空間だけを膨らませている姿だ。VSZを根拠にメモリリークと騒ぐのは、ほぼ誤診である。
RSS(Resident Set Size)は、そのうち現に物理メモリへ載っているページの合計だ。実体を映すが、致命的な癖については後述する。
TTY — 端末にぶら下がっているか
制御端末の名前だ。pts/0はSSHや端末エミュレータ越しの擬似端末、tty1は物理コンソール、疑問符はデーモンやカーネルスレッドのように制御端末を持たないものを示す。端末が閉じたときSIGHUPで巻き添えを食うのは端末を持つ側だ。
STAT — 状態1文字と付加文字
1文字目がプロセスの状態を、2文字目以降が付加情報を表す。障害対応で最初に見るべき列だ。
| 文字 | 状態 | 現場での意味 |
|---|---|---|
| R | 実行中または実行可能 | CPUを使っているか、CPU待ちの列に並んでいる |
| S | 割り込み可能スリープ | イベント待ちの正常な待機。大半のプロセスはこれ |
| D | 割り込み不可スリープ | I/O完了待ち。シグナルが届かない。続くならストレージを疑う |
| Z | ゾンビ(defunct) | 終了済みで親のwait待ち。資源は解放済み |
| T | 停止中 | SIGSTOPやCtrl+Zによる停止。デバッガ由来の停止は小文字のt |
| I | アイドル | 仕事のないカーネルスレッド。ロードアベレージには数えない |
付加文字は状態の右に続く。
| 付加文字 | 意味 | サンプルでの例 |
|---|---|---|
| s | セッションリーダー | Ss の init・sshd・bash |
| l | マルチスレッド | Sl の java |
| + | フォアグラウンドプロセスグループに属する | R+ の ps aux 自身 |
| N | 低優先度(nice値が正) | RN の python3 |
| < | 高優先度(nice値が負) | I< の kworker |
| L | メモリ内にロックされたページを持つ | サンプルには無し |
サンプルを読み直そう。init・sshd・bashは Ss でセッションリーダー、javaは Sl で複数スレッド、python3は RN でnice値を上げて(優先度を下げて)実行中、ps aux 自身は R+ でフォアグラウンドだ。kworkerは待機中のIに高優先度の記号が添えられた形になる。
START と TIME — 起動時刻と累積CPU時間
STARTは起動時刻だ。当日ならHH:MM、それより前ならMmmDD、さらに古ければ年が出る。
TIMEは累積CPU時間(ユーザー時間とシステム時間の和)を分:秒で表す。経過時間ではない。nginxワーカーは%CPUが0.4と地味だが、7月2日から生き続けてTIMEは84分12秒まで積み上がっている。一方rsyncのTIMEは1秒だ。この3列は次の関係で結ばれる。
%CPU ≒ TIME ÷ (現在時刻 − START)
javaで確かめる。TIME 229分31秒は13771秒、経過は17520秒。13771 ÷ 17520 = 0.786、つまり78.6だ。
COMMAND — 角括弧はカーネルスレッド
uオプションのCOMMANDは、プロセス自身が持つコマンドライン(/proc配下のcmdline)だ。だから java -Xmx2g -jar app.jar のように引数まで読める。ただしプロセスが書き換えられる領域でもあり、nginx: worker process は実行ファイル名ではなくnginxが上書きした文字列だ。
角括弧付きの [kthreadd] や [jbd2/nvme0n1p2-8] はカーネルスレッドである。ユーザー空間のアドレス空間を持たずcmdlineが空なので、psは代わりにカーネルが保持する短い名前を角括弧で括る。VSZとRSSが揃って0なのも同じ理由だ。ゾンビもcmdlineを失うため角括弧表示になり、末尾にdefunctが添えられる。
aux と -ef — BSD形式とUNIX形式
もう一つの定番が ps -ef だ。-eは全プロセス、-fは詳細表示を指す。
| 観点 | ps aux(BSD形式) | ps -ef(UNIX形式) |
|---|---|---|
| 列の構成 | USER PID %CPU %MEM VSZ RSS TTY STAT START TIME COMMAND | UID PID PPID C STIME TTY TIME CMD |
| メモリ | %MEM・VSZ・RSSが出る | 出ない |
| CPU | %CPU(起動からの平均)と累積TIME | C(整数の利用率)と累積TIME |
| 親子関係 | PPIDが出ない | PPIDが出る。ゾンビの親を追える |
| 状態 | STATが出る | 出ない |
| 得意な場面 | リソースを食っている犯人の特定 | プロセスの親子関係の追跡 |
決定的な違いはPPID(親プロセスID)の有無で、ゾンビの親を突き止めたいときauxでは足りない。サンプルの2210なら ps -ef か ps -o pid,ppid,stat,comm で親を特定し、その親を直す。結局は ps -eo pid,pcpu,rss,stat,comm --sort=-rss のように-oで組み立てるのが速い。
つまずきやすい点
第一に、%CPUを現在の負荷と読むこと。これは起動からの平均で、数分単位の変動はならされている。「今どれが重いか」なら、一定間隔でCPU時間の差分を取るtopコマンドの読み方へ切り替えるしかない。psで代用するなら、間を空けて2回実行しTIMEの差を取る。
第二に、RSSを足し算すること。RSSはlibcのような共有ライブラリのページを、マップしている全プロセスへ丸ごと重複計上する。fork直後の子なら、まだ1バイトも複製していないページまで親子双方に載る。だから全プロセスのRSSの合計は実使用量を大きく上回る。按分したければ、共有ページを共有プロセス数で割るPSS(Proportional Set Size)を/proc配下のsmaps_rollupから読む。
第三に、ゾンビを恐れすぎること。ゾンビはすでに終了してメモリもファイルも解放済みで、終了ステータスを親へ渡すためだけにプロセステーブルの1エントリとして残る抜け殻だ。CPUもメモリも消費しない。2210でVSZとRSSが0なのはそのためだ。killを撃っても意味はない。すでに死んでいるからだ。悪いのは常に親側で、waitを呼んでいないコードが原因だ。親を終了させればPID 1に引き取られて消える。ただし放置してPIDを食い潰せばforkが失敗する。
第四に、D状態へkillを撃ち続けること。Dは割り込み不可のスリープで、多くはディスクやNFSのI/O完了待ちだ。シグナルを確認するコードパスにいないため、SIGKILL(kill -9)ですら届かない。シグナルは保留のまま積まれ、I/Oが完了して初めて配送される。止められないのではなく順番が来ていないだけだ。やるべきなのはプロセスを殴ることではなく、なぜI/Oが返らないのかを調べること、つまりNFSサーバーの無応答やディスクの故障を疑うことである。D状態が並びロードアベレージだけ跳ね上がるのに%CPUは低い、という症状がその典型である。
ps -eo pid,stat,wchan:20,comm のwchan列には、そのプロセスがカーネル内のどの関数で眠っているかが出る。nfsやioで始まる関数名が並んでいれば、原因はプロセス側ではなくストレージ側だと即断できる。
まとめ
ps aux の11列は、権限・識別・負荷・メモリ・所属・状態・正体のかたまりとして読める。押さえる原理は3つだ。%CPUは起動からの平均であって現在値ではない。VSZは宣言、RSSは実体、ただしRSSは共有分を重複して数える。STATの1文字目は次の一手を決める最短の手掛かりになる。psが写すのは1枚の写真にすぎず、その境界さえ分かればあとは-oでの列指定で補える。他のコマンドの読み方はコマンド出力の読み方に揃えてある。
コマンド出力の読み方の記事ガイド
ps aux の出力を1列ずつ解剖を実務で読む
TL;DRは入口です。実際に選ぶ・使う段階では、何を解決するか、何と比較するか、導入後にどこで詰まるかまで見る必要があります。
解決すること
コマンド出力
比較で見る軸
難易度: advanced / カテゴリ: コマンド出力の読み方 / タグ数: 4
導入後に効く点
VSZは未使用領域や共有ライブラリも含む仮想アドレス空間の総量。RSSは物理メモリ上の量だが、共有部分を各プロセスへ重複計上するため、RSSの全件合計は実使用量にならない。
先に潰すリスク
用語だけ覚えても、設計・実装・運用でどこに効くかを確認しないと判断を誤る。
- 難易度
- advanced
- カテゴリ
- コマンド出力の読み方
- タグ数
- 4
判断チェックリスト
- 自社の用途が「コマンド出力 / Linux」に近いか確認する。
- 強みである「ps auxは11列でプロセスを示す。%CPUは現在値ではなく、起動後の累積CPU時間を経過時間で割った平均だ。今この瞬間の負荷を調べるならtopを使う。」が本当に評価軸になるか確認する。
- 注意点の「用語だけ覚えても、設計・実装・運用でどこに効くかを確認しないと判断を誤る。」を運用で吸収できるか確認する。
- 公開値や仕様値は、対象プラン・対象機種・対象リージョンまで確認する。
- 既存システム、ID、ネットワーク、監視、バックアップとの接続方法を先に洗い出す。
- 小さく試してから、本番移行、権限設計、障害時手順、コスト監視を決める。