runtime/pprof: add a (rss) memory profile type
要約
概要
runtime/pprof パッケージに新しい "memory" プロファイルタイプを追加する提案です。Go プログラムのメモリ使用量をプロセス全体(RSS から始まり、ヒープ・スタック・ランタイムオーバーヘッドまで)の観点で一元的に可視化することを目的としています。
ステータス変更
(新規) → active
2026年5月28日に @aclements がこのプロポーザルを proposals project の active カラムに追加しました。提案の動機・設計・実装プロトタイプが揃っており、週次 proposal review meeting での審議対象として受理されました。
技術的背景
現状の問題点
インシデント対応やコスト最適化の文脈で、Go アプリケーションのメモリ使用量を調査する際、開発者は通常 OS の RSS メトリクスから調査を開始し、次に inuse_space プロファイル(ライブヒープ)を参照します。
// 現在の方法: ライブヒーププロファイルのみ取得
pprof.Lookup("heap").WriteTo(f, 0)
しかし、デフォルト設定(GOGC=100)では、ライブヒープはGoプログラムのメモリ使用量の50%未満しか説明しません。以下の要素が欠落しているためです。
- デッドヒープ: GC のマーク終了後に到達不能と判定されたが、まだスイープされていないメモリ
- ゴルーチンスタック:
/memory/classes/heap/stacks:bytesに相当するメモリ - ランタイムオーバーヘッド: MSpan、MCache、GCメタデータなど
- 非Goメモリ: 実行ファイル、共有ライブラリ、mmap領域、cgo malloc
この結果、RSS・ランタイムメトリクス・ライブヒーププロファイルの間の数値を突き合わせるための長い解説記事(例: Datadog のGoメモリメトリクスガイド)が必要な状況が続いています。
提案された解決策
新しい "memory" プロファイルタイプを追加します。
pprof.Lookup("memory").WriteTo(f, 0)
このプロファイルは、直近の完了した GC サイクル(マーク終了 + スイープ終了)時点のメモリ使用量内訳を提供します。プロファイルの構造は以下の階層になります。
- RSS(OS/カーネルから取得可能な場合)
- Go Memory:
/memory/classes/total:bytes - /memory/classes/heap/released:bytes- Heap
- Live(ライブヒープ): スタックトレース付き内訳
- Dead(デッドヒープ): 直近のマーク終了後にスイープされた割り当て
- Stack
- Goroutines: ゴルーチンスタック
- OS Threads: OSスレッドスタック
- Runtime
- MSpan、MCache、GCメタデータ、プロファイリングバケット、その他
- Heap
- Non-Go Memory:
RSS - Go Memory(Goランタイム管理外のメモリ)
RSS が取得できない場合、またはRSS - Go Memoryが負になる場合は、RSS フレームと Non-Go Memory を省略し、Go Memory を最上位フレームとして表示します。
- Go Memory:
これによって何ができるようになるか
ユースケース1: メモリ使用量の全体把握
コンテナ化された環境でメモリ制限(OOM)に近づいている際、RSS とゴルーチンメモリ、ランタイムオーバーヘッドを一つのプロファイルで確認でき、ボトルネックの特定が大幅に効率化します。
ユースケース2: デッドヒープの可視化
リクエスト指向サービスでは、GC タイミングによって短命なオブジェクトがライブヒープとデッドヒープのどちらに現れるか変わります。デッドヒープの内訳を確認することで、GC 頻度を下げる最適化箇所を特定できます。
ユースケース3: GOMEMLIMIT の設定根拠の確認
Go Memory の内訳(ライブヒープ、デッドヒープ、スタック、ランタイム)が定量的に把握できるため、GOMEMLIMIT や GOGC の適切な設定値を根拠を持って決定できます。
コード例
// Before: 断片的な情報収集(複数の情報源を手動で突き合わせ)
heapProfile := pprof.Lookup("heap")
heapProfile.WriteTo(heapFile, 0)
// 別途 /proc/self/status から VmRSS を読み取り
// runtime.ReadMemStats() でランタイムメトリクスを取得
// これら3つの数値を手動で突き合わせて解釈
// After: 単一の統合メモリプロファイル
memProfile := pprof.Lookup("memory")
memProfile.WriteTo(memFile, 0)
// 1つのプロファイルに RSS・ライブヒープ・デッドヒープ・スタック・
// ランタイムオーバーヘッド・非Goメモリが全て含まれる
議論のハイライト
- 命名の変更: 当初
"rss"という名前が検討されましたが、コンテナ化環境では cgroup v2 がプロセスレベルの RSS をファーストクラスのメトリクスとして提供していないこと、またコンテナツール全般(Kubernetes、docker stats、cgroup v2 のmemory.current等)が「memory」という語を包括的な指標として使用していることから、"memory"に改名されました。 - 仮想メモリ vs 物理メモリ: Goランタイムは主に仮想メモリを追跡する一方、RSS は物理メモリです。提案者は実際には Go Memory が物理メモリと 1:1 でマップされることが多いと指摘しつつ、一致しないケース(大きなメモリバラスト、未アクセスの大型スライスなど)ではベストエフォートとして扱う設計を採用しています。
- デッドヒープの有用性: デッドヒープはプログラムのメモリ使用量を直接説明しない場合がありますが(長寿命な大型オブジェクトと短命なオブジェクトが混在する場合)、GC頻度の削減による CPU/メモリ効率の改善と
GOGC/GOMEMLIMITの最適化に有効であることが論じられています。 - スナップショット整合性: GCのマーク終了時にGoランタイムメトリクスの整合スナップショットを取得することが技術的に可能と確認されています。RSS の読み取りはSTW中に syscall を行うことが望ましくないため、マーク終了後できるだけ早く読み取るベストエフォート方式を採用します。
- 既存プロファイルとの共存:
"memory"という名前がコミュニティでライブヒープの俗称として使われてきた懸念がありますが、既存の"heap"プロファイルがヒープ割り当て詳細の参照先として引き続き機能するため、住み分けができると判断されています。