用語の定義#

用語として プロファイルプロファイラプロファイリング という言葉が用いられます。これらは様々な使われ方をしますが、本ワークショップでは以下のような意味合いで用います。

用語意味
プロファイルプログラムの実行時の振る舞いを計測した記録データ
プロファイラプロファイルを計測・収集・可視化するためのツール
プロファイリングプロファイラを用いてプログラムの性能を計測・解析する作業全般

例えば、今回利用するツールの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/pprofHTTPエンドポイント経由でプロファイルを収集するライブラリ
runtime/pprofを内部で利用している
-
cmd/pprofプロファイルを分析・可視化するためのCLIツール
github.com/google/pprof のコピー
-

収集できる6つのプロファイルタイプ#

Goのpprofでは、以下の6種類のプロファイルを収集できます。各プロファイルの詳細は個別ページを参照してください。

プロファイル測定対象主なユースケース詳細
CPUCPUサイクルの消費箇所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 CreateOSスレッドの生成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におけるflatcum(cumulative)は、「その関数自身で使ったコスト」と「その関数配下も含めた累積コスト」を区別するための指標です。

具体例#
func A() {
    // A 自身の処理: 20ms
    B()
    C()
}

func B() {
    // B 自身の処理: 30ms
}

func C() {
    // C 自身の処理: 50ms
}

この場合のプロファイル結果:

関数flatcum
A20ms100ms
B30ms30ms
C50ms50ms
  • Aflat20ms(A自身の処理)
  • Acum20 + 30 + 50 = 100ms(A自身 + B + C)
  • Bflatcumは同じ30ms(子関数を呼んでいないため)
使い分けの考え方#

flatが大きい関数:

  • 関数の中身そのものが重い
  • 👉 アルゴリズム改善・ループ削減・ロック見直しなどが効く
  • 即効性のある改善点

cumが大きい関数:

  • 配下の呼び出しを含めて重い
  • 👉 呼び出し回数削減・キャッシュ・責務分割の見直しが効く
  • 広範囲な改善が必要
よくある誤解#
  • 「cumが大きい = その関数が悪い」
  • 「cumが大きい = その関数以下のどこかがボトルネック」

cumが大きくてもflatが小さければ、その関数自体は軽いということになります。 重いのは呼び出し先なので、call graphをたどって真のボトルネックを探す必要があります。

実務的な見方#
  1. topでflatが大きいものを見る(即効性のある改善点)
  2. 次にcumが大きいものを見てcall graphをたどる
  3. 真に重いleaf(flatが大きい末端関数)を特定

各ビューの詳しい使い方#

1. Graph(コールグラフ)#

表示内容:

  • ノード(四角): 関数
  • エッジ(矢印): 関数呼び出し
  • ノードのサイズと色: flat値に比例(大きく・赤いほど重い)
  • エッジの太さ: 呼び出し回数やコストに比例

ノードの表記:

関数名
flat値 (flat%)
of cum値 (cum%)

色の意味:

  • 赤系: ホットスポット(最適化すべき箇所)
  • オレンジ: やや重い処理
  • グレー: 軽い処理

使い方:

  1. 赤いノードを探す(ボトルネック)
  2. エッジをたどって呼び出し元を確認
  3. クリックで詳細表示、ダブルクリックでその関数を起点に再表示
2. Flame Graph(フレームグラフ)#

表示内容:

  • 横軸: リソース消費量(幅が広いほど重い)
  • 縦軸: コールスタックの深さ(下が呼び出し元、上が呼び出し先)
  • : ランダム(同じ関数は同じ色)

pprof 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値

使い方:

  1. VIEWメニューからPeekを選択
  2. 関数名を入力(例: main.processData
  3. 呼び出し関係を確認

表示例:

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.Split

SAMPLEドロップダウン(プロファイルタイプ別)#

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\.processmain.processを含むパスのみ表示
Ignore特定の関数を除外Ignore: runtime\. でランタイム関数を除外
Hideノードを非表示(エッジは維持)Hide: wrapper でwrapper関数を非表示
Show from特定の関数から下だけ表示Show from: main でmain以下のみ表示

正規表現が使える: Focus: main\.(process|parse) のように複数パターンを指定可能

CONFIGメニュー(表示設定)#

主要オプション:

設定説明
Unit単位の表示形式(自動/最小)
Divide by値を割る(平均化)
Sample indexSAMPLEと同じ(プロファイルタイプ選択)

効果的な分析フロー#

  1. Flame Graphで全体像を把握

    • 幅の広い部分を探す
  2. Graphで呼び出し関係を確認

    • 赤いノードをクリックして詳細確認
    • REFINEでフォーカス
  3. Topで数値を確認

    • flat%が大きい関数をリストアップ
    • sum%が80%までの関数群に注目
  4. Sourceで実装を確認

    • 重い関数のソースコードを行単位で分析
    • どの行が重いかを特定
  5. Peekで影響範囲を確認

    • 呼び出し元を確認(どこから呼ばれているか)
    • 呼び出し先を確認(どこが重いか)

値の大小の判断基準#

プロファイルデータを見る際の目安となる基準値です。

flat値(関数自体のコスト):

  • 10%以上: 重大なボトルネック → 優先的に最適化
  • 5-10%: 中程度のボトルネック → 検討の価値あり
  • 1-5%: 軽微な影響 → 他に問題がなければ最適化対象
  • 1%未満: 最適化の優先度は低い

cum値(関数ツリー全体のコスト):

  • 50%以上: その関数ツリー全体が非常に重い
  • 30-50%: 重要な処理パス
  • 10-30%: 一部の処理に影響
  • 10%未満: 全体への影響は小さい

最適化の優先順位:

  1. flat%が大きい → その関数自体を最適化(効果が直接的)
  2. cum%が大きくflat%が小さい → 呼び出し先を調査(間接的な影響)
  3. sum%が80%に達するまでの関数群 → パレートの法則を適用(20%の関数が80%のリソースを消費)

次のステップ#

まずはCPU Profilingから始めましょう。