RSS・VSZ・PSSがそれぞれ測るもの
一言でいうと
VSZは約束、RSSは実際に確保されている物理メモリ、PSSは共有分を分け合った取り分です。Pod複数のRSSを足すと、実際より多く出ます。そのとき見るべきものがPSSです。
なぜ必要なのか
「メモリリークのようだ」という報告の半分は、リークではありません。プロセスが大きくなる理由はいくつもあります。
- 本物のリーク: 解放していない割り当てが積み上がり続ける
- キャッシュの増加: アプリケーションが意図的にキャッシュを満たす(到達すれば止まる)
- ヒープの断片化: 解放はしたが、OSに返せなかった
- アロケーターの特性: glibcのmallocは、小さなブロックをOSにあまり返さない
4つは対処が違います。区別せずに「メモリを増やそう」へ進むと、数日後に同じことが繰り返されます。
どう動くのか
1つのプロセスの実際の使用量は、smaps_rollupの1行が最も正確です。
grep -E '^(Rss|Pss|Private_Dirty|Swap):' /proc/<PID>/smaps_rollup
| 項目 | 意味 |
|---|---|
Rss |
物理メモリに載っている全体(共有ライブラリを含む) |
Pss |
共有分を、共有している側の数で割った取り分。合計を出すときに使う唯一の値 |
Private_Dirty |
このプロセスだけが使う、ファイルに戻せない部分。リークを見るときはこれ |
リークの判別は、ある時点の値ではなく傾きで行います。負荷を一定にかけた状態でPrivate_Dirtyを数分間隔で記録し、単調に増加していればリーク側、ある線で平らになればキャッシュ側です。
よくある勘違い
VSZを見て驚くこと。64ビットのJVMやGoのランタイムは、VSZで数十GBを確保します。アドレス空間を予約しただけで、物理メモリではありません。VSZは、ほとんどいつも無視してかまいません。
RSSを足すこと。コンテナ10個が同じlibcを使うと、そのページは物理的には1つなのに、RSSには10回計上されます。合計はPSSで出します。
free -hのfreeが小さいと心配すること。Linuxは、余ったメモリをすべてページキャッシュに使います。見るべき値はavailableです。
コンテナのメモリ上限はどこで見るか
ホストのfreeやtopは、コンテナに割り当てられた分を知りません。cgroupを直接見ます。
cat /sys/fs/cgroup/memory.max # 한도 (max 면 제한 없음)
cat /sys/fs/cgroup/memory.current # 지금 쓰는 양
cat /sys/fs/cgroup/memory.stat # 항목별 내역
memory.currentにはページキャッシュが含まれます。ファイルを多く読むプロセスは、キャッシュのせいで上限に近く見えますが、圧迫が来るとカーネルがキャッシュを先に捨てるので、OOMにはなりません。そのため「メモリを90%使っている」が、そのまま危険というわけではありません。
本当に危険かどうかは、memory.statの2行で見ます。
anon 1234567890 ← 익명 메모리. 이건 버릴 수 없다
file 987654321 ← 페이지 캐시. 압박이 오면 버려진다
anonが上限に近ければ、まもなくOOMです。
OOMが起きたかを確認する方法
Podが突然再起動したのにログに何もなければ、たいていOOMです。アプリケーションは、自分が死ぬことを知る方法がなく、最後の言葉を残せません。
kubectl describe pod <파드> | grep -A3 "Last State"
Last State: Terminated
Reason: OOMKilled
Exit Code: 137
# cgroup 쪽 기록
cat /sys/fs/cgroup/memory.events
oom 3
oom_kill 1
oomは、上限に達して回収を試みた回数で、oom_killが実際に殺した回数です。oomだけが増えてoom_killが0なら、まだ死んではいないが、圧迫を受け続けているという意味で、そのときから遅延が悪化します。このシグナルを見逃すと、「ときどき遅い」としか見えません。
言語ごとに何を調整するか
コンテナの上限を絞っても、ランタイムがそれを知らなければ意味がありません。
| ランタイム | 調整するもの | しないと |
|---|---|---|
| JVM | -XX:MaxRAMPercentage=75 |
古いJVMは、ホストのメモリを基準にヒープを確保します |
| Node.js | --max-old-space-size=<MB> |
デフォルトのヒープが、コンテナの上限より大きいことがあります |
| Go | GOMEMLIMIT |
GCが遅く動いて上限を超えます |
| Python | 別途の設定なし | ワーカー数(gunicornの-w)で調整します |
GOMEMLIMITはGo 1.19からあり、上限の90%ほどにしておくと、GCがその線でより頻繁に動きます。これなしでGoのサービスを狭い上限に入れると、GCがのんびり構えているうちにOOMを食らいます。
実務で本当に大切なこと
コンテナはホストではなく、cgroupの上限で死にます。
cat /sys/fs/cgroup/memory.max # 한도
cat /sys/fs/cgroup/memory.current # 지금
cat /sys/fs/cgroup/memory.events # oom_kill 횟수
memory.currentには、ページキャッシュも入ります。そのため、ファイルを多く読むプロセスは、実際のヒープが小さくても上限に達します。memory.statのanon(匿名メモリ)とfile(キャッシュ)を分けて見ると、本当の原因が見えてきます。OOM Killはカーネルがすることなので、アプリケーションのログには何も残りません。memory.eventsのoom_killが増えたかどうかが、唯一の証拠であることが多いのです。