用語の定義#
用語として プロファイル 、プロファイラ、 プロファイリング という言葉が用いられます。これらは様々な使われ方をしますが、本ワークショップでは以下のような意味合いで用います。
| 用語 | 意味 |
|---|---|
| プロファイル | プログラムの実行時の振る舞いを計測した記録データ |
| プロファイラ | プロファイルを計測・収集・可視化するためのツール |
| プロファイリング | プロファイラを用いてプログラムの性能を計測・解析する作業全般 |
例えば、今回利用するツールのpprofは プロファイラです。そして、pprofを用いて得られるサンプリングデータは、 プロファイル です。また、pprofを用いて解析を行う行為は プロファイリング です。
プロファイリングとオブザーバビリティ#
プロファイリングを実施する上で、参考になる概念である オブザーバビリティ と オブザーバビリティ駆動開発 について触れておきます。
オブザーバビリティとは#
オブザーバビリティ(Observability, 可観測性) とは、もともとエンジニアのRudolf E. Kálmánによって作られた造語で、システムの外部出力からその内部状態を理解する能力のことです。もともとは制御理論から来た概念です。
『オブザーバビリティ・エンジニアリング』によると、現代のソフトウェアシステムに適用したオブザーバビリティは以下のように述べられています:
簡単に言うと、私たちが考えるソフトウェアシステムの「オブザーバビリティ」とは、 システムがどのような状態になったとしても、それがどんなに斬新で奇妙なものであっても、どれだけ理解し説明できるかを示す尺度です。
出典: Charity Majors, Liz Fong-Jones, George Miranda 著, 大谷和紀, 山口能迪 訳『オブザーバビリティ・エンジニアリング』オーム社, 2023年, 1章.
言い換えるならば、システムを観測して得られる情報から、どれだけ内部状態を理解して説明できるかの尺度 と捉えることができるでしょう。
コラム: Three Pillars of Observability(オブザーバビリティの3本柱)
オブザーバビリティを実現するための代表的なシグナルとして、以下の3つが知られています。
| シグナル | 説明 | 例 |
|---|---|---|
| ログ(Logs) | 離散的なイベントの記録 | エラーメッセージ、アクセスログ |
| メトリクス(Metrics) | 時系列で集計された数値データ | CPU使用率、リクエスト数、レイテンシ |
| トレース(Traces) | リクエストの処理経路を追跡 | 分散システムでのリクエストフロー |
これらに加え、プロファイル(Profiles) もオブザーバビリティの重要なシグナルとして位置づけられています。プロファイルは、コードレベルでのリソース消費を可視化し、「なぜ遅いのか」「どこでメモリを使っているのか」という問いに答えます。
Observability-Driven Development(オブザーバビリティ駆動開発)#
同書では、Observability-Driven Development(ODD/オブザーバビリティ駆動開発) という開発アプローチが提唱されています。これは、開発サイクルの初期段階からオブザーバビリティツールを活用し、本番環境に近い形でコードの振る舞いを理解しながら開発を進める手法 です。
Observability-Driven Developmentについて、同書では以下のように説明されています。
オブザーバビリティドリブン開発とは、可観測なシステムから取得したテレメトリーを開発ワークフローの一部として活用するプラクティス、つまりデプロイする前に本番トラフィックに対してコードをテストすることです。
出典: Charity Majors, Liz Fong-Jones, George Miranda 著, 大谷和紀, 山口能迪 訳『オブザーバビリティ・エンジニアリング』オーム社, 2023年, 15章.
従来の開発では、パフォーマンス問題は本番環境で発覚してから対処することが多く、問題の再現や原因特定に多大な時間を要していました。ODD では、開発・テスト段階からプロファイリングを含むオブザーバビリティツールを積極的に活用することで、問題を早期に発見し、修正コストを削減します。
オブザーバビリティ駆動開発のフィードバックループ#
同書の11章では、オブザーバビリティ駆動開発においてフィードバックループを小さくすることの重要性が強調されています。
計装を開発する際に役立つ目標は、強化メカニズムを作り、フィードバックのループを小さくすることです。言い換えれば、コードをリリースしてからエラーの結果を実感するまでのループを緊密なものにすることです。
出典: Charity Majors, Liz Fong-Jones, George Miranda 著, 大谷和紀, 山口能迪 訳『オブザーバビリティ・エンジニアリング』オーム社, 2023年, 11章.
graph LR
A[1.計装] --> B[2.デプロイ]
B --> C[3.観察]
C --> D[4.問題の特定]
D --> E[5.改善]
E --> A
style A fill:#e1f5fe
style B fill:#fff3e0
style C fill:#fce4ec
style D fill:#e8f5e9
style E fill:#f3e5f5| フェーズ | 説明 | プロファイリングでの実践 |
|---|---|---|
| 計装 | テレメトリーを出力するコードを埋め込む | プロファイラの導入 |
| デプロイ | 計装済みコードを本番環境(またはステージング)にリリース | FeatureFlags の活用 |
| 観察 | 本番環境での挙動を観察し、以下を確認する: ・コードは意図通り動くか ・前のバージョンと比較してどうか ・異常事態は発生していないか | プロファイルの収集・可視化 |
| 問題の特定 | デバッグが必要なコードがシステムのどこにあるかを見つけ出す | プロファイラを活用したホットスポットの特定 |
| 改善 | 特定した問題に対してコードを修正する | コード改善 |
各フェーズの根拠(11章からの引用)
1. 計装(11.5節):
良い計装はオブザーバビリティを駆動します。計装がどのように役立つかを考えるひとつの方法は、プルリクエストの状況で考慮することです。プルリクエストをサブミットしたりアクセプトするまえに、「この変更が意図した通りに動いているかどうか、私はどうやって確認できるだろうか?」と自分自身に問いかけましょう。
2. デプロイ(11.5節):
十分な計装があれば、提案された変更が本番環境でどのように機能するかを理解する最善の方法として、その変更を本番環境にデプロイして、どのように機能するかを測定できるようになります。
3. 観察(11.5節):
すべてのエンジニアは、デプロイ後すぐにこれらの質問に答えられるように、自分のコードに計装することを期待されるべきです。
- コードは意図通り動くか
- 前のバージョンと比較してどうか
- ユーザーはコードを使えているか
- 異常事態は発生していないか
4. 問題の特定(11.3節):
オブザーバビリティはコードのロジックをデバッグするためのものではありません。オブザーバビリティは、デバッグが必要なコードがシステムのどこにあるかを見つけ出すためのものです。オブザーバビリティツールは、問題が発生している箇所を迅速に絞り込みます。
5. 改善(11.2節):
バグを迅速に解決するには、原作者の頭の中に新鮮な意思が残っているうちに、問題を検証できるかどうかに強く依存しています。コードを書いてリリースした直後のタイミングのように、簡単に問題をデバッグできることは二度とないでしょう。
出典: Charity Majors, Liz Fong-Jones, George Miranda 著, 大谷和紀, 山口能迪 訳『オブザーバビリティ・エンジニアリング』オーム社, 2023年, 11章.
このサイクルを素早く回すことで、問題を早期に発見し、修正コストを削減できます。
Go におけるプロファイリング#
Go におけるプロファイリングでは主に pprof が用いられます。pprof は、プロファイルのサンプリング機能と可視化機能の2つの機能を持ち合わせています。
| ツール | 説明 | サンプリング | 可視化 |
|---|---|---|---|
| runtime/pprof | プロファイルを収集するための標準ライブラリ | ✓ | - |
| net/http/pprof | HTTPエンドポイント経由でプロファイルを収集するライブラリruntime/pprofを内部で利用している | ✓ | - |
| cmd/pprof | プロファイルを分析・可視化するためのCLIツール github.com/google/pprof のコピー | - | ✓ |
収集できる6つのプロファイルタイプ#
Goのpprofでは、以下の6種類のプロファイルを収集できます。各プロファイルの詳細は個別ページを参照してください。
| プロファイル | 測定対象 | 主なユースケース | 詳細 |
|---|---|---|---|
| CPU | CPUサイクルの消費箇所 | CPU使用率が高い処理の特定、ホットパスの最適化 | Part 1-1 |
| Heap | メモリ割り当て | メモリリークの検出、過剰なアロケーションの特定 | Part 1-2 |
| Goroutine | 全goroutineのスタックトレース | goroutineリークの検出、デッドロックの調査 | Part 1-3 |
| Block | 同期プリミティブでのブロック時間 | チャネルやmutexでの待ち時間の分析 | Part 1-4 |
| Mutex | ロック競合の状況 | mutex競合によるCPU未活用の特定、ロック設計の見直し | Part 1-4 |
| Thread Create | OSスレッドの生成 | OSスレッドの過剰生成の調査 | - |
参考: Go Wiki - Custom Pprof Profiles
プロファイルの収集方法#
runtime/pprof#
テストやバッチ処理など、開始と終了が明確なプログラムで使用します。
import (
"os"
"runtime/pprof"
)
// CPU profiling
f, _ := os.Create("cpu.prof")
pprof.StartCPUProfile(f)
defer pprof.StopCPUProfile()
// Heap profiling
f, _ := os.Create("mem.prof")
pprof.WriteHeapProfile(f)
f.Close()net/http/pprof#
Webサーバなど、長時間稼働するプログラムで使用します。
import (
_ "net/http/pprof"
"net/http"
)
func main() {
go func() {
http.ListenAndServe("localhost:6060", nil)
}()
// your application code
}エンドポイント:
/debug/pprof/profile?seconds=30- 30秒間のCPUプロファイル/debug/pprof/heap- ヒープメモリスナップショット/debug/pprof/goroutine- goroutineスタックトレース/debug/pprof/block- blockプロファイル/debug/pprof/mutex- mutexプロファイル
go test でのprofiling#
ベンチマークと組み合わせて簡単にプロファイルを取得できます。
go test -cpuprofile cpu.prof -memprofile mem.prof -bench .go tool pprofのTips#
プロファイルを取得したら、go tool pprofで分析します。
インタラクティブモード(CLI)#
go tool pprof cpu.prof主要コマンド:
top- 上位N個の関数を表示list <関数名>- 関数のソースコードと行ごとの統計web- グラフをブラウザで表示pdf- PDFとして出力
使う場合は、 help してみると良いです。
Webビューア#
go tool pprof -http=:8080 cpu.profブラウザで以下のビューが利用可能:
- Graph: コールグラフ
- Flame Graph: フレームグラフ
- Top: 関数ごとの統計
- Source: ソースコードビュー
- Peek: 特定関数の呼び出し関係を詳細表示
Webビューアの主要な概念#
pprofのWebビューアでは、以下の重要な指標が表示されます:
| 指標 | 意味 | 見方 |
|---|---|---|
| flat | その関数「自身」で消費したリソース(自己コスト) | 大きい → その関数の中身が重い |
| flat% | flatの全体に対する割合 | 大きい → 最適化の候補 |
| sum% | 上位からの累積割合 | 80%以上 → 主要なボトルネック |
| cum | その関数が呼ばれている間に消費された合計リソース(累積コスト) | 大きい → その関数以下のどこかが重い |
| cum% | cumの全体に対する割合 | 大きい → 広範囲に影響 |
flat と cum の違い#
pprofにおけるflatとcum(cumulative)は、「その関数自身で使ったコスト」と「その関数配下も含めた累積コスト」を区別するための指標です。
具体例#
func A() {
// A 自身の処理: 20ms
B()
C()
}
func B() {
// B 自身の処理: 30ms
}
func C() {
// C 自身の処理: 50ms
}この場合のプロファイル結果:
| 関数 | flat | cum |
|---|---|---|
| A | 20ms | 100ms |
| B | 30ms | 30ms |
| C | 50ms | 50ms |
Aのflatは20ms(A自身の処理)Aのcumは20 + 30 + 50 = 100ms(A自身 + B + C)Bのflatとcumは同じ30ms(子関数を呼んでいないため)
使い分けの考え方#
flatが大きい関数:
- 関数の中身そのものが重い
- 👉 アルゴリズム改善・ループ削減・ロック見直しなどが効く
- 即効性のある改善点
cumが大きい関数:
- 配下の呼び出しを含めて重い
- 👉 呼び出し回数削減・キャッシュ・責務分割の見直しが効く
- 広範囲な改善が必要
よくある誤解#
- ❌ 「cumが大きい = その関数が悪い」
- ✅ 「cumが大きい = その関数以下のどこかがボトルネック」
cumが大きくてもflatが小さければ、その関数自体は軽いということになります。 重いのは呼び出し先なので、call graphをたどって真のボトルネックを探す必要があります。
実務的な見方#
- topでflatが大きいものを見る(即効性のある改善点)
- 次にcumが大きいものを見てcall graphをたどる
- 真に重いleaf(flatが大きい末端関数)を特定
各ビューの詳しい使い方#
1. Graph(コールグラフ)#
表示内容:
- ノード(四角): 関数
- エッジ(矢印): 関数呼び出し
- ノードのサイズと色: flat値に比例(大きく・赤いほど重い)
- エッジの太さ: 呼び出し回数やコストに比例
ノードの表記:
関数名
flat値 (flat%)
of cum値 (cum%)色の意味:
- 赤系: ホットスポット(最適化すべき箇所)
- オレンジ: やや重い処理
- グレー: 軽い処理
使い方:
- 赤いノードを探す(ボトルネック)
- エッジをたどって呼び出し元を確認
- クリックで詳細表示、ダブルクリックでその関数を起点に再表示
2. Flame Graph(フレームグラフ)#
表示内容:
- 横軸: リソース消費量(幅が広いほど重い)
- 縦軸: コールスタックの深さ(下が呼び出し元、上が呼び出し先)
- 色: ランダム(同じ関数は同じ色)

読み方:
- 幅が広い関数: CPU時間やメモリを多く消費
- 平らな台地: その関数自体が重い(flat値が大きい)
- 尖った山: 呼び出し先が重い(cum値が大きいがflat値は小さい)
操作:
- クリック: その関数を起点にズームイン
Reset zoom: 全体表示に戻るSearch: 関数名で検索(ハイライト表示)
3. Top(関数一覧)#
表示内容:
Showing nodes accounting for XXXms, YY% of ZZZms total
flat flat% sum% cum cum% Name
100.5ms 45.2% 45.2% 150.3ms 67.5% main.heavyFunc
50.2ms 22.6% 67.8% 50.2ms 22.6% runtime.mallocgc
30.1ms 13.5% 81.3% 35.7ms 16.0% main.parseData各列の意味:
- flat: その関数自体の実行時間/メモリ
- flat%: 全体に対するflatの割合
- sum%: 上から累積した割合(80-90%に注目)
- cum: その関数と呼び出し先を含む合計
- cum%: 全体に対するcumの割合
- Name: 関数名
4. Source(ソースコード表示)#
表示内容:
- ソースコードの各行にflat値とcum値が表示される
- 行ごとにどれだけリソースを消費しているかが分かる
表記例:
flat cum
100ms 150ms 10: for i := 0; i < n; i++ {
50ms 50ms 11: result += process(data[i])
12: }見方:
- flat値が大きい行: その行自体が重い(ループ、計算処理など)
- cum値が大きい行: その行で呼び出している関数が重い
- 値が表示されない行: ほとんどリソースを消費していない
5. Peek(関数の呼び出し関係)#
表示内容:
- 選択した関数の呼び出し元(Callers)と呼び出し先(Callees)
- それぞれのflat/cum値
使い方:
VIEWメニューからPeekを選択- 関数名を入力(例:
main.processData) - 呼び出し関係を確認
表示例:
Callers of main.processData:
cum: 150ms main.handleRequest
main.processData:
flat: 50ms
cum: 150ms
Callees of main.processData:
cum: 80ms json.Unmarshal
cum: 20ms strings.SplitSAMPLEドロップダウン(プロファイルタイプ別)#
Webビューア上部のSAMPLEドロップダウンでプロファイルの種類を切り替えられます:
CPU Profile:
samples: サンプル数(CPU時間に比例)cpu: CPU時間(ナノ秒)
Heap Profile:
alloc_objects: 累積アロケーション回数alloc_space: 累積アロケーション量inuse_objects: 現在使用中のオブジェクト数inuse_space: 現在使用中のメモリ量
Block Profile:
contentions: ブロック回数delay: ブロック時間
Mutex Profile:
contentions: 競合回数delay: 競合待ち時間
REFINEメニュー(フィルタリング)#
主要オプション:
| オプション | 説明 | 使い方 |
|---|---|---|
| Focus | 特定の関数のみに絞り込み | Focus: main\.process でmain.processを含むパスのみ表示 |
| Ignore | 特定の関数を除外 | Ignore: runtime\. でランタイム関数を除外 |
| Hide | ノードを非表示(エッジは維持) | Hide: wrapper でwrapper関数を非表示 |
| Show from | 特定の関数から下だけ表示 | Show from: main でmain以下のみ表示 |
正規表現が使える: Focus: main\.(process|parse) のように複数パターンを指定可能
CONFIGメニュー(表示設定)#
主要オプション:
| 設定 | 説明 |
|---|---|
| Unit | 単位の表示形式(自動/最小) |
| Divide by | 値を割る(平均化) |
| Sample index | SAMPLEと同じ(プロファイルタイプ選択) |
効果的な分析フロー#
Flame Graphで全体像を把握
- 幅の広い部分を探す
Graphで呼び出し関係を確認
- 赤いノードをクリックして詳細確認
- REFINEでフォーカス
Topで数値を確認
- flat%が大きい関数をリストアップ
- sum%が80%までの関数群に注目
Sourceで実装を確認
- 重い関数のソースコードを行単位で分析
- どの行が重いかを特定
Peekで影響範囲を確認
- 呼び出し元を確認(どこから呼ばれているか)
- 呼び出し先を確認(どこが重いか)
値の大小の判断基準#
プロファイルデータを見る際の目安となる基準値です。
flat値(関数自体のコスト):
- 10%以上: 重大なボトルネック → 優先的に最適化
- 5-10%: 中程度のボトルネック → 検討の価値あり
- 1-5%: 軽微な影響 → 他に問題がなければ最適化対象
- 1%未満: 最適化の優先度は低い
cum値(関数ツリー全体のコスト):
- 50%以上: その関数ツリー全体が非常に重い
- 30-50%: 重要な処理パス
- 10-30%: 一部の処理に影響
- 10%未満: 全体への影響は小さい
最適化の優先順位:
flat%が大きい → その関数自体を最適化(効果が直接的)cum%が大きくflat%が小さい → 呼び出し先を調査(間接的な影響)sum%が80%に達するまでの関数群 → パレートの法則を適用(20%の関数が80%のリソースを消費)
次のステップ#
まずはCPU Profilingから始めましょう。