メインコンテンツへスキップ
#79179accepted

runtime/pprof: add a (rss) memory profile type

ステータス変更: likely_accept accepted

要約

AIによる要約であり、誤りを含む場合があります。

概要

runtime/pprof に新しいプロファイル種別 "memory" を追加する提案です。従来のヒーププロファイルが示すのは「生存しているヒープ」だけでした。新プロファイルはこれに加えて、死んだヒープ、スタック、ランタイム自身のメモリ、さらに RSS(OS から見た常駐物理メモリ量)をもとに推定した Go 以外のメモリまで、プログラムのメモリ使用量全体を1つのプロファイルで内訳表示します。

ステータス変更

likely_accept → accepted
2026年10月14日に likely accept となり、最終コメント期間中に合意が変わらなかったため、10月29日に @aclements が accepted を宣言しました。レビューグループは早い段階からこの提案に好意的で、「ヒーププロファイルについて長年多くの人が抱いてきた混乱を解消する」と評価していました。ただし試作コードは AI で生成したもので、レビューグループは人がきちんと書き直した実装でも問題が出ないことを確認したいと考えていました。そのため「実装を見るまでは暫定承認」という扱いになっていました。その後 @nsrip-dd が書き直した実装(CL 841489)を出し、想定外の問題はなかったと報告しました。これで条件が満たされ、承認に至りました。

技術的背景

現状の問題点

メモリの調査は、たいてい RSS などの OS 指標を見るところから始まり、次に pprof.Lookup("heap") の inuse_space(生存ヒープ)に移ります。しかしデフォルト設定(GOGC=100)では、生存ヒープはメモリ使用量全体の50%未満にしかなりません。生存ヒープに含まれないものは次のとおりです。

  • 死んだヒープ: GC のマーク終了時点で到達不能と判定され、その後の sweep(掃除)で解放される領域
  • goroutine と OS スレッドのスタック
  • ランタイムのメタデータ(mspan、mcache、GC 用の情報など)
  • Go 以外のメモリ: 実行ファイル、共有ライブラリ、mmap した領域、cgo の malloc など
    そのため利用者は、RSS、runtime/metrics、ヒーププロファイルの3つを自分で同じ時点にそろえて取得し、数字を突き合わせる必要がありました。

提案された解決策

直近の GC マーク終了時点のメモリ使用量を、次の階層で内訳表示します。

  • [RSS](OS から取得できる場合のみ)
    • Go Memory: /memory/classes/total:bytes - /memory/classes/heap/released:bytes と厳密に一致する
      • Heap: Live(生存ヒープ、スタックトレース付き)と Dead(マーク終了から sweep 終了までに解放された割り当て、スタックトレース付き)
      • Stack: Goroutines、OS Threads
      • Runtime: MSpan、MCache、GC Metadata、Profiling、Other
    • Non-Go Memory: RSS - Go Memory
      RSS が取得できない場合や、Go Memory が RSS を上回る場合は、Non-Go Memory を省きます。その理由は警告フレームではなく、pprof 形式のコメント欄に [WARNING: Go Memory (X MiB) > RSS (Y MiB)] のように記録します。Go Memory が RSS を上回る例としては、ballast のように確保しただけで触れていないメモリがあります。ランタイムの各指標はマーク終了時の STW(全 goroutine の一時停止)中に一貫したスナップショットとして取得します。RSS の読み取りはシステムコールが必要なので STW 中には行わず、マーク終了の直後に読みます。

これによって何ができるようになるか

  • OOM や障害の調査: RSS が増えたとき、原因がヒープなのか、goroutine スタックなのか、cgo などの Go 以外のメモリなのかを、1つのプロファイルで切り分けられます。
  • コスト最適化: 死んだヒープの割り当て元をスタックトレースで特定できます。これにより、割り当ての多い箇所を減らして GC の頻度を下げ、GOGC や GOMEMLIMIT をより厳しく設定する判断材料が得られます。
  • 継続的プロファイリング: 3種類のデータを自前で突き合わせる必要がなくなり、ダッシュボードの値と内訳が一致するようになります。

コード例

// Before: 3つの情報源を別々に集め、手作業で突き合わせる
pprof.Lookup("heap").WriteTo(f, 0) // 生存ヒープのみ
samples := []metrics.Sample{
    {Name: "/memory/classes/total:bytes"},
    {Name: "/memory/classes/heap/released:bytes"},
    {Name: "/memory/classes/heap/stacks:bytes"},
}
metrics.Read(samples)
statm, _ := os.ReadFile("/proc/self/statm") // RSS は OS ごとの方法で自分で取得
// After: 1つのプロファイルで全体を内訳表示
pprof.Lookup("memory").WriteTo(f, 0)
// go tool pprof で開くと [RSS] → [Go Memory] → [Heap] → [Live]/[Dead] ... と表示される

議論のハイライト

  • 名前は「rss」から「memory」に変更: @rednafi が、cgroup や Kubernetes などのコンテナ周辺のツールは "memory" を総称として使っていると指摘しました。これを受けて改名し、RSS は取得できる場合だけ付く最上位フレームという位置付けになりました。
  • 既存の heap プロファイルにまとめる案は不採用: 一度は memory_space というサンプル種別を既存の heap プロファイルに追加する案に改訂されました。しかしレビューグループは次の2点を懸念しました。
    • 新プロファイルは独自の仮想ルートフレームを持つため、既存プロファイルとスタックを共有できず、サイズが増える(試算では gzip 後で1.3〜1.9倍)。
    • 既存のスタック構造を変えると、ヒーププロファイルを解析している自動化ツールが壊れる恐れがある。
      結果として、独立した "memory" プロファイルとして追加し、利用者に移行を促す方針に戻りました。
  • 仮想メモリと物理メモリ: ランタイムが数えているのは主に仮想メモリで、RSS は物理メモリです。@aclements は、Go のメモリを RSS の一部として表示すると不正確になりうると懸念しました。これに対し、Go Memory が RSS を超える場合は RSS 側の内訳を省き、その旨をコメントで明示することで対処しました。データセグメントは RSS に含まれます。
  • 警告をフレームではなくコメントにした理由: Go Memory が RSS を超える状態は一時的に起こりうるため、警告をフレームにすると、プロファイルごとにフレームがあったりなかったりして集計や比較を妨げます。そこで pprof のコメント欄を使うことになりました。
  • サンプリングレートの影響(@cherrymui の質問): Live と Dead の内訳は MemProfileRate でサンプリングされたデータから推定し、その合計を /gc/heap/live:bytes に合わせて補正します。そのため、割り当て元ごとの配分の精度はサンプリングレートに左右されます。
  • 新プロファイルが出力するのは、生存中の割り当てと直近の GC で解放された割り当てだけです。このため、過去のすべての記録を処理する既存の heap/alloc プロファイルより収集コストが低い可能性がある、と @nsrip-dd は見込んでいます。

関連リンク