Linuxでメモリリークを調査する方法

Linuxでメモリリークを調査する方法

Linuxでメモリリークを疑う場面では、単に「メモリ使用量が高い」という結果だけを見ても原因は特定しにくい。実際には、正常なキャッシュ増加、ワーキングセットの拡大、共有メモリ、断片化、プロセスの一時的なピーク、そして本当のリークが混在する。つまり、最初にやるべきことは「本当にリークなのか」を切り分けること。そのうえで、OSレベルでは /procpspmaptop などで増え方を確認し、アプリケーションレベルでは Valgrind、LeakSanitizer、Massif などでどこから確保され、どこで解放されていないかを追う流れが基本になる。長時間運用で少しずつ増えるケースほど、単発確認では見逃しやすいため、時系列での観察が重要になる。

最初に整理するべきこと

メモリリーク調査では、まず次の4つを分けて考えると整理しやすい。
・本当にリークか
・どのプロセスが増えているか
・増えているのはRSSか、仮想メモリか、共有分を含むか
・アプリの実装問題か、ライブラリ・設定・キャッシュ戦略の問題か
たとえば、Linuxはファイルキャッシュも積極的に使うため、単に「used メモリが増えた」だけでリークとは言えない。まずはOS全体と特定プロセスの両方を分けて観察する必要がある。

メモリリークの典型的な症状

実際にリークが疑わしいときは、次のような症状が出やすい。
・プロセスを再起動するまでRSSが右肩上がりで増え続ける
・アクセス数や処理件数に比例してメモリが減らない
・時間経過とともにswap使用量が増える
・OOM Killer が発動する
・初期は問題ないが、数時間から数日で急に落ちる
特に「負荷が下がっても使用量が戻らない」状態はリークを疑いやすい。ただし、メモリアロケータの挙動やキャッシュ保持も似た見え方をするため、すぐ断定しない方が安全。

まずはOS全体のメモリ状況を確認する

最初に見るべきはサーバー全体のメモリ状態。
最も手早いのは free -h

free -h

ここで見るポイントは、
・total
・used
・available
・swap
特に available を重視すると、Linuxがキャッシュ込みでどの程度余裕を持っているか判断しやすい。
used だけ見て「全部使われている」と決めつけると誤判断しやすい。

どのプロセスがメモリを使っているかを絞る

OS全体が重いことが分かったら、次は犯人候補のプロセスを探す。
まずは ps でメモリ使用率順に並べる。

ps aux –sort=-%mem | head

これで上位プロセスが見える。
たとえば、
・java
・python
・php-fpm
・node
・mysqld
などが上位に来ていれば、そのプロセスの中を掘る方向へ進む。
重要なのは、システム全体が苦しいのか、特定プロセスだけが増え続けているのかをここで分けること。

top で増加中のプロセスをリアルタイムに観察する

瞬間的な一覧だけでなく、リアルタイムで増え方を見るなら top が便利。
起動して、メモリ使用量の高いプロセスが固定的に上位へ居座るかを観察する。

top

必要ならメモリ順で見やすくする。
環境によっては Shift + M でメモリ順ソートが使いやすい。
ここでの観察ポイントは、
・同じPIDが継続的に増えるか
・ワーカーが複数いて一斉に増えているか
・一定時間で解放されるか
という点になる。

RSSとVSZを混同しない

メモリ調査で非常に多い誤解が、RSSとVSZの混同。
pstop で見える数値のうち、実際に物理メモリ上に載っている量を重視したいならRSS寄りで見る方が分かりやすい。
一方、仮想メモリサイズだけ大きくても、直ちにリークとは限らない。
一覧で確認するなら次のようにRSSとVSZを並べて見ると判断しやすい。

ps -o pid,comm,rss,vsz,%mem –sort=-rss -p 1234

増えているのがどちらかを見誤ると、実害の小さい確保をリークだと思い込むことがある。

/proc/PID/status でメモリの内訳を確認する

特定プロセスをさらに詳しく見るなら /proc/<PID>/status が使いやすい。
ここでは VmSize、VmRSS、RssAnon、RssFile、RssShmem などを確認できる。

cat /proc/1234/status

見るポイントは、
・VmRSS
・RssAnon
・RssFile
・RssShmem
匿名メモリが増えているのか、ファイルマッピング由来なのか、共有メモリが多いのかが分かると、リーク候補の方向がかなり絞れる。
特にアプリのヒープ由来なら RssAnon 側が増えやすい。

/proc/PID/smaps でPSSやマッピングごとの詳細を掘る

さらに細かく見るなら /proc/<PID>/smaps が強い。
ここでは各マッピングごとの RSS、PSS、匿名領域、共有領域などを追える。

cat /proc/1234/smaps | less

非常に長いため、必要なら絞り込む。

grep -E “^(Size|Rss|Pss|Private|Shared|Anonymous)” /proc/1234/smaps

これにより、
・共有ライブラリなのか
・匿名ヒープなのか
・スタックなのか
といった粒度で増加場所を見やすくなる。
本当にリークっぽいなら、匿名メモリの増え方が目立つことが多い。

pmap でマップ単位のメモリ使用を確認する

pmap もプロセスのメモリマップを見るうえで扱いやすい。
簡易的に総量を見たいなら次のように使える。

pmap -x 1234 | tail -n 1

これで合計の仮想サイズ、RSS、dirty などが見やすい。
全体一覧は次。

pmap -x 1234

smaps より少し読みやすく、ざっくりした傾向確認に向いている。
特に、ヒープ領域やマップ数がどう増えているかをざっと掴みたいときに便利。

時間経過で増えるかを watch で監視する

メモリリークは「今多い」より「時間とともに増え続ける」ことが本質なので、継続監視が重要。
簡単には watch を使うと確認しやすい。

watch -n 2 “ps -o pid,comm,rss,vsz,%mem -p 1234”

または pmap の合計行だけを見る方法もある。

watch -n 5 “pmap -x 1234 | tail -n 1”

これで一定間隔ごとの増え方が分かる。
アクセスや処理件数と一緒にメモリが増え、処理終了後も戻らないなら、リークらしさが強くなる。

swapの増加も一緒に確認する

リークが進行すると、最終的にはswapが増えたり、システム全体が重くなったりする。
そのため、プロセス単体だけでなく、OS全体のswapも見た方が良い。

free -h
vmstat 1

vmstat 1 では、swap in/out の動きや待ちも見やすい。
swapが増えている場合、メモリリークの被害がOS全体へ波及している可能性が高い。
単にアプリ1つが肥大化しているだけでなく、他プロセスの性能にも影響が出やすくなる。

Valgrind Memcheck で未解放メモリを検出する

C/C++系アプリやネイティブバイナリでリークを追うなら、Valgrind の Memcheck は定番。
コンパイル時にデバッグ情報を入れておくと読みやすい。

gcc -g -O0 main.c -o app

実行は次のような形。

valgrind –leak-check=full –show-leak-kinds=all ./app

これで、
・definitely lost
・indirectly lost
・possibly lost
・still reachable
といった分類でレポートが出る。
definitely lost は特に優先して疑うべき項目になりやすい。
ただし、実行速度はかなり遅くなるため、本番常時監視向きではなく、再現環境での精査向きになる。

Valgrind Massif でヒープ使用量の増え方を見る

「どこで解放されていないか」だけでなく、「どの時点でどのコード経路がヒープを増やしているか」を見たいなら Massif も有効。

valgrind –tool=massif ./app
ms_print massif.out.*

これにより、ヒープ使用量のピークや増加元のスタックを追いやすくなる。
長時間でじわじわ増えるケースより、ある処理をきっかけに急増するケースで特に役立つ。

Clang / GCC 系なら LeakSanitizer も有力

C/C++系の開発では、LeakSanitizer を使ったリーク検出も有効。
ビルド時に sanitizer を有効にして実行する形になる。

clang -g -O1 -fsanitize=address -fno-omit-frame-pointer main.c -o app
./app

LeakSanitizer を有効にすると、終了時にリーク検出フェーズが走り、未解放メモリを報告しやすい。
Valgrindより軽い場面もあるが、ビルドし直し前提になるため、使い分けが必要になる。

本当にリークではなくキャッシュやプールのこともある

実務では、メモリが増えているように見えて、実際にはライブラリやランタイムのキャッシュ、オブジェクトプール、メモリアロケータの保持戦略であることも多い。
たとえば、
・JVMヒープの拡大
・Node.jsのヒープ成長
・PHP-FPMワーカーごとのメモリ保持
・DBクライアントやHTTPクライアントのバッファ
などは、すぐに「リーク」とは言い切れない。
そのため、「何もしていないのに戻らない」ことに加えて、「処理量と比例して増え続けるか」「再現性があるか」を見ることが大切になる。

ログと負荷条件を合わせて再現させる

リーク調査では、再現条件を明確にした方が早い。
たとえば、
・特定APIを1000回叩く
・同じバッチを繰り返す
・一定サイズのファイルを何度も読み込む
・画像変換を連続実行する
といった条件を揃え、その前後でRSSやsmapsを比較する。
単に放置して増加を待つより、「この操作で増える」を明確にした方が、コードレベルの修正へつなげやすい。

よくある失敗と勘違い

メモリリーク調査でよくある失敗は次の通り。
・usedメモリだけ見て即リークと決める
・RSSとVSZを混同する
・Linuxのファイルキャッシュ増加をリークだと思う
・一回だけ見て「多いからリーク」と判断する
・Valgrindを本番で常時使おうとする
・still reachable を全部重大リークだと扱う
・アプリ以外のプロセスやcron、監視ツールを見ていない
つまり、「増えている」という事実だけでは足りず、「何が」「どう増え」「戻らないか」まで追う必要がある。

調査の進め方の基本順序

Linuxでメモリリークを調査するとき、進めやすい順序は次の流れ。

  1. free -h でOS全体の余裕を見る
  2. ps aux --sort=-%mem | head で犯人候補を探す
  3. top でリアルタイムに挙動を見る
  4. /proc/PID/statuspmap -x で内訳を確認する
  5. watch で増加傾向を追う
  6. 再現条件を作る
  7. Valgrind / LeakSanitizer / Massif でコードレベルに掘る
    この順で進めると、「OS側の見立て」と「コード側の証拠」をつなげやすい。

まとめ

Linuxでメモリリークを調査するには、
・まず本当にリークかを切り分ける
freepstop/procpmap でOS視点から観察する
・時間経過で増え続けるかを確認する
・swapやシステム全体への影響も見る
・必要に応じて Valgrind、Massif、LeakSanitizer でコードレベルへ掘る
という流れがかなり有効になる。
大事なのは、「メモリが多い」ではなく、「解放されるべきものが解放されず、負荷や時間に応じて増え続けているか」を証拠付きで押さえること。
この順序で進めると、ただのキャッシュ挙動と本物のリークを見分けやすくなる。