継続的プロファイリングとフレームグラフの原理
CPUを食う関数が本番で特定できず勘で最適化していませんか。サンプリングがなぜ統計的に正しいか、フレームグラフの読み方、常時プロファイリングのオーバーヘッド管理までを原理から押さえます。
- 1.サンプリングプロファイラは一定周期でスタックを抜き取り、ある関数の出現比率がそのままCPU占有時間の比率に一致するという統計的推定。誤差は標本数で決まる。
- 2.フレームグラフは横幅=CPU総時間、縦=呼び出し深さ。各バーの幅で「どこが重いか」を一目で読み、色やy座標には意味がない。
- 3.本番常時プロファイリングは周期を粗くして実行頻度を下げ(例99Hz)、固定オーバーヘッドに抑える。pprofはタイマー割り込み、eBPFはカーネル側で低コストにスタックを採取する。
本番で「CPUが張り付いているが、どの関数が原因か分からない」という状況は珍しくありません。ログやメトリクスは「遅い」ことは教えても「どのコード行が時間を食ったか」は答えられません(/devops/observability-pillars-internals/)。これを埋めるのがプロファイリングで、本稿はその統計的原理とフレームグラフの読み方、本番常時運用のコスト管理を扱います。
サンプリングの統計的原理:なぜ「抜き取り」で全体が分かるのか
CPUプロファイラには2系統あります。計装(instrumenting) 型は関数の入口と出口に計測コードを埋め込み、全呼び出しを正確に計時します。正確ですが、呼び出し回数が多い小さな関数でオーバーヘッドが膨らみ、計測自体が挙動を歪めます。本番常時運用には向きません。
もう一方が サンプリング(statistical) 型で、これが継続的プロファイリングの主流です。発想は単純で、一定周期(例:毎秒100回)でプロセスを覗き、そのとき実行中のスタックトレースを1枚スナップショットする だけです。各サンプルは「この瞬間、CPUはこの関数を実行していた」という1票です。
なぜ抜き取りで全体が分かるのか。これは標本調査と同じ原理です。ある関数がCPU時間の X% を占めるなら、ランダムな瞬間にその関数を捕まえる確率も X% です。十分な数のサンプルを集めれば、各関数の出現比率は真のCPU占有比率に収束します(大数の法則)。プロファイラは全呼び出しを数えるのではなく、占有時間の比率を統計推定 しているのです。
ある関数の真の占有率を p とすると、各サンプルは確率 p で当たるベルヌーイ試行です。N サンプルでの観測比率の標準誤差は sqrt(p*(1-p)/N) に比例し、N の平方根で縮みます。つまり精度はサンプリング周波数そのものではなく「集めた総サンプル数」で決まる。99Hz で5分回せば約3万サンプルになり、占有率1%程度の関数まで安定して見えます。逆に短時間の計測では稀な関数の数字は信用できません。
重要な帰結として、サンプリングは「短すぎる関数」を取りこぼします。サンプル間隔(99Hz なら約10ms)より短命な関数は、運悪く割り込みの瞬間に走っていなければ票が入りません。これは欠陥ではなく仕様で、合計CPU時間が小さい=最適化の優先度が低い関数を自然に無視できる、と捉えるのが正しい読み方です。
オンCPUとオフCPU:何を測っているかを取り違えない
サンプリングプロファイラが標準で数えるのは オンCPU時間、すなわち実際にCPUを使って計算していた時間です。ところが本番の遅延の多くは、ロック待ち・I/O待ち・ネットワーク待ちといった オフCPU時間(スレッドがCPUを手放してブロックしている時間)に潜みます。
ここを取り違えると判断を誤ります。CPUフレームグラフが平坦なのにレイテンシが悪い場合、原因はCPUではなく待ちにあります。オフCPUプロファイリング はスケジューラがスレッドをブロック状態にした瞬間と復帰した瞬間を捉え、待ち時間をスタック別に集計します。レイテンシ分析(/devops/percentile-latency-statistics/)では、まず「CPU律速か待ち律速か」をこの2種で切り分けるのが定石です。
| 観点 | オンCPUプロファイル | オフCPUプロファイル |
|---|---|---|
| 測る対象 | 実際に計算した時間 | ブロックして待った時間 |
| 主な原因 | ホットループ・重い計算・GC | ロック競合・I/O・syscall待ち |
| 採取契機 | タイマー割り込み(定周期) | スレッドのブロック/復帰イベント |
| 見落とすもの | 待ち時間は写らない | CPU計算は写らない |
フレームグラフの読み方
集めた大量のスタックトレースをそのまま並べても読めません。フレームグラフ は、同じ呼び出し経路を持つサンプルを集約して可視化する手法です。読み方の原則は3つだけです。
- 横幅 = そのフレームが出現したサンプル数 = CPU総時間の割合。 幅が広いバーほど多くの票を集めた、すなわちCPUを食っている。これが唯一にして最重要の軸です。
- 縦(y軸)= スタックの深さ。 下が呼び出し元、上が呼び出し先。あるバーの真上に乗るバー群は、その関数が呼び出した子関数です。
- 横の並び順とは無関係、色も無意味。 同じ深さのバーは通常アルファベット順に並ぶだけで、左右はx軸=時間ではありません。色は視認性のための慣習で、意味を持たせない実装が一般的です。
読むときの探し方は「上辺の平らで広い部分(プラトー)を探す」です。フレームグラフの一番上の辺は「割り込みの瞬間に実際にCPU上で走っていた関数」の集まりです。上辺で幅広く平らに伸びている関数こそ、自分自身でCPUを消費している真犯人(self time が大きい)です。
あるバーが広くても、その幅の大半が上に積まれた子関数で占められているなら、重いのは子であって自分ではありません。自分自身の消費(self time)は「そのバーの幅のうち、真上に子バーが乗っていない部分」に現れます。最適化対象は、上辺に露出した幅広のフレームです。一方、バーが広いのに上が深く積層しているなら、最適化すべきは呼び出しグラフの先にあります。
派生形も押さえておくと実務で迷いません。アイシクルグラフ(icicle)は上下を反転し、根を上に置いたもので、意味は同じです。ディファレンシャルフレームグラフ は2時点のプロファイルの差分を色で表し、デプロイ前後でどの関数が重くなったかを直接見られます。リリース戦略(/devops/deployment-strategies/)と組み合わせると回帰検知に効きます。
オーバーヘッド管理:本番で常時回すための設計
継続的プロファイリングの肝は、本番で常時動かしてもサービスの挙動を歪めないこと です。オーバーヘッドを支配するのは「採取の頻度」と「採取1回のコスト」の積です。
第一の手段は 採取頻度を粗くする ことです。多くのプロファイラが既定で 99Hz(毎秒99回)を使うのは理由があります。100Hz のようなキリのよい値はカーネルの周期タスクや他の定周期処理と位相が揃い(エイリアシング)、特定の処理を体系的に過大/過小評価する恐れがあります。99 のように切りの悪い値をあえて選ぶことでこの同期を避けます。99Hz なら採取は10msに1回程度で、固定的な小さなオーバーヘッド(多くの実装で1%前後)に収まります。前述のとおり、精度は周波数より総サンプル数で決まるので、頻度を落としても時間をかければ十分な解像度が得られます。
第二の手段は 採取1回のコストを下げる ことです。ここで実装方式が効きます。
| 方式 | スタック採取の仕組み | オーバーヘッドと特徴 |
|---|---|---|
| pprof(タイマー割り込み) | SIGPROF等の定周期割り込みで実行中スタックを巻き取る | 言語ランタイム統合・移植性高。割り込み処理が各プロセスで走る |
| eBPF(カーネル側採取) | perf_eventをフックしカーネル空間でスタックを採取・集約 | ユーザ空間に介入せず低コスト。言語非依存でホスト全体を一括計測 |
pprof 系(Goランタイム内蔵のものなど)は OS のプロファイリングタイマーで割り込みをかけ、ランタイムが自前でスタックを巻き取ります。言語ランタイムと統合され、シンボル解決やGC情報まで取れるのが強みです。一方 eBPF ベースは、カーネルに安全な小プログラムを差し込み、perf_event のサンプリング割り込みをカーネル空間で受けてスタックを採取します。ユーザ空間のコードに一切手を入れず、しかもカーネル内で集約してからユーザ空間へ渡すため、データ転送量も小さく抑えられます。言語非依存で、コンテナを含むホスト上の全プロセスを横断的に常時計測できるのが決定的な利点です。
生のサンプルはアドレスの列にすぎず、関数名へ変換するシンボル化が必要です。これを計測のホットパスで毎回行うと重いので、実務ではサンプル収集と分離して非同期・オフボックスで行います。またスタックの巻き取りはフレームポインタ方式(高速だが、フレームポインタ省略でビルドされたバイナリでは壊れる)か DWARF 方式(正確だが重い)かで精度とコストが変わります。eBPF で正しいスタックを得るには、対象バイナリをフレームポインタ付きでビルドするのが安全策です。
負荷テスト(/devops/load-testing/)で取るプロファイルも、本番常時プロファイルも、統計的原理は同一です。違いは「どんな母集団から標本を取るか」だけ。合成負荷は理想化された分布、本番は実トラフィックの分布を反映します。容量計画(/devops/capacity-planning/)でCPU予算を見積もるなら、合成ではなく本番プロファイルの占有比率を根拠にするのが正確です。
まとめ
- サンプリングプロファイラは全呼び出しを数えず、定周期でスタックを抜き取り、関数の出現比率からCPU占有比率を統計推定 する。精度はサンプリング周波数ではなく総サンプル数(の平方根)で決まる。
- 標準のプロファイルは オンCPU時間 のみを測る。ロック待ちやI/O待ちは オフCPUプロファイル で別途捉え、まず「CPU律速か待ち律速か」を切り分ける。
- フレームグラフは 横幅=CPU総時間、縦=呼び出し深さ。色とx軸の並び順に意味はない。上辺の幅広で平らなプラトー(self time が大きい関数) が最適化の第一候補。
- 本番常時運用は 頻度を粗く(例99Hz、エイリアシング回避) して固定オーバーヘッドに抑え、eBPF でカーネル側採取 すれば言語非依存・低コストでホスト全体を横断計測できる。
- シンボル化とスタック巻き取りは隠れたコスト。シンボル化は非同期・オフボックス、バイナリはフレームポインタ付き が安全な既定値。
DevOps/インフラ Article
継続的プロファイリングとフレームグラフの原理を実務で読む
TL;DRは入口です。実際に選ぶ・使う段階では、何を解決するか、何と比較するか、導入後にどこで詰まるかまで見る必要があります。
解決すること
プロファイリング
比較で見る軸
難易度: advanced / カテゴリ: DevOps/インフラ / タグ数: 6
導入後に効く点
フレームグラフは横幅=CPU総時間、縦=呼び出し深さ。各バーの幅で「どこが重いか」を一目で読み、色やy座標には意味がない。
先に潰すリスク
用語だけ覚えても、設計・実装・運用でどこに効くかを確認しないと判断を誤る。
- 難易度
- advanced
- カテゴリ
- DevOps/インフラ
- タグ数
- 6
判断チェックリスト
- 自社の用途が「プロファイリング / フレームグラフ」に近いか確認する。
- 強みである「サンプリングプロファイラは一定周期でスタックを抜き取り、ある関数の出現比率がそのままCPU占有時間の比率に一致するという統計的推定。誤差は標本数で決まる。」が本当に評価軸になるか確認する。
- 注意点の「用語だけ覚えても、設計・実装・運用でどこに効くかを確認しないと判断を誤る。」を運用で吸収できるか確認する。
- 公開値や仕様値は、対象プラン・対象機種・対象リージョンまで確認する。
- 既存システム、ID、ネットワーク、監視、バックアップとの接続方法を先に洗い出す。
- 小さく試してから、本番移行、権限設計、障害時手順、コスト監視を決める。