TT Lab
はじめる
学ぶ 学習パス コース

CPU・メモリリークの見極め

RSS・VSZ・PSSがそれぞれ測るもの

TT Labで続きを見る

一言でいうと

VSZは約束、RSSは実際に確保されている物理メモリ、PSSは共有分を分け合った取り分です。Pod複数のRSSを足すと、実際より多く出ます。そのとき見るべきものがPSSです。

なぜ必要なのか

「メモリリークのようだ」という報告の半分は、リークではありません。プロセスが大きくなる理由はいくつもあります。

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が増えたかどうかが、唯一の証拠であることが多いのです。