
こんにちは。よっしーです(^^)
本日は、Go言語のプロファイルガイド最適化ついて解説しています。
背景
Go言語でアプリケーションのパフォーマンスを改善したいと思ったことはありませんか? 公式ドキュメントで「Profile-guided optimization (PGO)」という機能を見かけたものの、英語で書かれていて専門用語も多く、「どういう仕組みなんだろう?」「自分のプロジェクトにどう活用すればいいの?」と戸惑った経験があるかもしれません。
この記事では、Go 1.21から正式サポートされたPGO(プロファイルガイド最適化)について、公式ドキュメントの丁寧な日本語訳と、初心者の方にもわかりやすい補足説明をお届けします。PGOは実行時のプロファイル情報をコンパイラにフィードバックすることで、2〜14%のパフォーマンス向上が期待できる強力な機能です。
「コンパイラ最適化」や「プロファイリング」と聞くと難しそうに感じるかもしれませんが、基本的な使い方はとてもシンプルです。この記事では、PGOの仕組みを身近な例えで説明し、実際にどうやって使うのかを具体的なコード例とともに紹介していきます。一緒に学んでいきましょう!
本番環境から代表的なプロファイルを収集する
「プロファイルの収集」セクションで説明したように、本番環境はアプリケーションの代表的なプロファイルを得るための最良の情報源です。
最もシンプルな始め方は、アプリケーションにnet/http/pprofを追加し、サービスの任意のインスタンスから/debug/pprof/profile?seconds=30を取得することです。これは始めるには素晴らしい方法ですが、以下の点で代表的でない可能性があります:
- このインスタンスは、通常は忙しいにもかかわらず、プロファイリングされた瞬間には何もしていない可能性があります
- トラフィックパターンは1日を通じて変化する可能性があり、動作が1日を通じて変わります
- インスタンスは長時間実行される操作を実行する場合があります(例:5分間操作Aを実行し、その後5分間操作Bを実行するなど)。30秒のプロファイルでは、おそらく1つの操作タイプしかカバーできません
- インスタンスは公平なリクエスト分散を受け取らない可能性があります(一部のインスタンスは他のインスタンスよりも特定のタイプのリクエストを多く受け取ります)
より堅牢な戦略は、個々のインスタンスプロファイル間の差異の影響を制限するために、異なるインスタンスから異なる時刻に複数のプロファイルを収集することです。その後、複数のプロファイルを単一のプロファイルにマージして、PGOで使用できます。
多くの組織は、このような種類のフリート全体のサンプリングプロファイリングを自動的に実行する「継続的プロファイリング」サービスを運用しており、これをPGO用のプロファイルのソースとして使用できます。
解説
シンプルな方法の落とし穴
最初に紹介された「1つのサーバーから30秒間プロファイルを取る」方法は簡単ですが、運が悪いと偏ったデータを取得してしまいます。
例:レストランの混雑パターン
想像してください。あなたはレストランの効率化を目指していて、厨房の動きを記録します。
悪い記録方法:
火曜日の午後3時に1つの店舗で30秒間だけ観察
→ この時間は暇な時間帯
→ 「このレストランは暇だから厨房は小さくていい」と誤った結論
良い記録方法:
月曜〜日曜、朝・昼・夜、複数店舗で観察
→ ランチタイムのピーク、ディナーの忙しさ、週末の混雑を把握
→ 「ランチタイムにパスタ調理を効率化すべき」と正確な結論
問題1:タイミングによる偏り
シナリオ:ECサイトのバックエンド
10:00 - プロファイル取得
→ たまたま商品検索リクエストが0件
→ 検索機能が最適化されない
14:00 - プロファイル取得(同じサーバー)
→ 検索リクエストがピーク
→ 検索機能が重点的に最適化される
実際の1日:
朝:在庫チェック (20%)
昼:商品検索 (50%)
夕方:注文処理 (30%)
教訓:30秒のスナップショットでは全体像が見えない
問題2:日内変動
実際のトラフィックは時間帯によって大きく変わります。
例:ニュースサイトのAPIサーバー
時刻 主な処理 CPU使用率
--------------------------------------
08:00 記事取得 API 高 (通勤時間)
12:00 記事取得 + コメント 最高 (昼休み)
15:00 管理画面アクセス 低
20:00 記事取得 + 動画 高 (帰宅時間)
03:00 バックアップ処理 中
1回のプロファイリングの問題:
# 午後3時にプロファイル取得
curl http://server/debug/pprof/profile?seconds=30 > profile.pprof
# 結果:管理画面の処理が最適化される
# 問題:ユーザーが最も使う「記事取得」は最適化されない
問題3:長時間処理の分割
バッチ処理やワークフローが順次実行される場合、30秒では一部しか捉えられません。
例:データ処理パイプライン
処理フロー(合計10分):
Step 1: データ取得 (2分) ← 30秒プロファイルがここを捉えた
Step 2: データ変換 (5分) ← 最も重い処理だが記録されず
Step 3: データ保存 (2分)
Step 4: 通知送信 (1分)
結果:
最も重い「データ変換」が最適化されず、
軽い「データ取得」が最適化される
問題4:不均等なリクエスト分散
ロードバランサーが完全に均等にリクエストを分散しているとは限りません。
例:マイクロサービス構成
インスタンス1: 画像処理リクエスト 70%, テキスト処理 30%
インスタンス2: 画像処理リクエスト 30%, テキスト処理 70%
インスタンス3: 画像処理リクエスト 50%, テキスト処理 50%
インスタンス1からプロファイル取得
→ 画像処理に偏った最適化
→ テキスト処理が多いワークロードで性能が出ない
解決策:複数プロファイルの収集とマージ
より正確なプロファイルを得るには、複数のサンプルを集めて統合します。
戦略1:時間を分散
# 朝のピーク時
curl http://server/debug/pprof/profile?seconds=30 > morning.pprof
# 昼のピーク時
curl http://server/debug/pprof/profile?seconds=30 > noon.pprof
# 夜のピーク時
curl http://server/debug/pprof/profile?seconds=30 > evening.pprof
戦略2:複数インスタンスから収集
# インスタンス1
curl http://server1/debug/pprof/profile?seconds=30 > server1.pprof
# インスタンス2
curl http://server2/debug/pprof/profile?seconds=30 > server2.pprof
# インスタンス3
curl http://server3/debug/pprof/profile?seconds=30 > server3.pprof
戦略3:異なる日に収集
# 平日
curl http://server/debug/pprof/profile?seconds=30 > weekday.pprof
# 週末
curl http://server/debug/pprof/profile?seconds=30 > weekend.pprof
プロファイルのマージ方法
複数のプロファイルを1つに統合するには、pprofツールを使います。
基本的なマージ:
# 複数のプロファイルをマージ
go tool pprof -proto morning.pprof noon.pprof evening.pprof > merged.pprof
# マージしたプロファイルを default.pgo として使用
mv merged.pprof cmd/myapp/default.pgo
重み付きマージ(ピーク時を重視):
# pprof はサンプル数で自動的に重み付けされる
# より長い時間のプロファイル = より多くのサンプル = より重い重み
# ピーク時は60秒、オフピーク時は30秒でプロファイル
curl http://server/debug/pprof/profile?seconds=60 > peak.pprof
curl http://server/debug/pprof/profile?seconds=30 > offpeak.pprof
# マージすると自動的にピーク時が2倍の重みになる
go tool pprof -proto peak.pprof offpeak.pprof > merged.pprof
実践例:1週間のプロファイリング計画
現実的な収集スケジュール:
#!/bin/bash
# weekly_profile_collection.sh
# 1週間かけて代表的なプロファイルを収集
# 月曜日 朝のピーク
curl http://server1/debug/pprof/profile?seconds=60 > mon_morning_s1.pprof
curl http://server2/debug/pprof/profile?seconds=60 > mon_morning_s2.pprof
# 火曜日 昼のピーク
curl http://server1/debug/pprof/profile?seconds=60 > tue_noon_s1.pprof
curl http://server2/debug/pprof/profile?seconds=60 > tue_noon_s2.pprof
# 水曜日 夜のピーク
curl http://server1/debug/pprof/profile?seconds=60 > wed_evening_s1.pprof
curl http://server2/debug/pprof/profile?seconds=60 > wed_evening_s2.pprof
# 金曜日(週末前の特別なトラフィック)
curl http://server1/debug/pprof/profile?seconds=60 > fri_s1.pprof
# すべてマージ
go tool pprof -proto \
mon_morning_s1.pprof mon_morning_s2.pprof \
tue_noon_s1.pprof tue_noon_s2.pprof \
wed_evening_s1.pprof wed_evening_s2.pprof \
fri_s1.pprof \
> weekly_merged.pprof
# default.pgo として配置
cp weekly_merged.pprof ../cmd/myapp/default.pgo
継続的プロファイリング:自動化された解決策
大規模な運用環境では、継続的プロファイリングサービスを使うのが最善です。
継続的プロファイリングとは:
レストランの例で言うと、「監視カメラを設置して24時間365日記録し続ける」ようなものです。
主要なサービス:
- Google Cloud Profiler
import "cloud.google.com/go/profiler"
func main() {
// 自動的に定期的にプロファイルを収集・アップロード
profiler.Start(profiler.Config{
Service: "myapp",
ServiceVersion: "1.0.0",
})
// あとは通常通りアプリケーション起動
startServer()
}
- Datadog Continuous Profiler
import "gopkg.in/DataDog/dd-trace-go.v1/profiler"
func main() {
profiler.Start(
profiler.WithService("myapp"),
profiler.WithEnv("production"),
)
defer profiler.Stop()
startServer()
}
- Pyroscope(オープンソース)
import "github.com/grafana/pyroscope-go"
func main() {
pyroscope.Start(pyroscope.Config{
ApplicationName: "myapp",
ServerAddress: "http://pyroscope:4040",
})
startServer()
}
継続的プロファイリングの利点:
従来の方法:
手動収集 → 特定の時間・サーバーのみ → 偏ったデータ
継続的プロファイリング:
自動収集 → 24/7 全サーバー → 包括的なデータ
→ 時間経過で集計 → 代表的なプロファイル
継続的プロファイリングからPGO用プロファイルを取得
Google Cloud Profilerの場合:
# Cloud Console から過去1週間のプロファイルをダウンロード
# または gcloud コマンドで取得
gcloud profiler query \
--service=myapp \
--profile-type=CPU \
--start-time="2024-01-20T00:00:00Z" \
--end-time="2024-01-27T00:00:00Z" \
> production_weekly.pprof
# PGO用に配置
cp production_weekly.pprof cmd/myapp/default.pgo
Pyroscopeの場合:
# APIで過去1週間の集計プロファイルを取得
curl "http://pyroscope:4040/render?query=myapp.cpu&from=now-7d&until=now&format=pprof" \
> production_weekly.pprof
cp production_weekly.pprof cmd/myapp/default.pgo
実践的なプロファイル収集計画
小規模サービス(サーバー1-3台):
週1回、各時間帯から手動収集
→ 朝・昼・夜 × 3台 = 9個のプロファイル
→ マージして使用
中規模サービス(サーバー4-20台):
cronで自動収集
→ 毎日ピーク時に3台からランダムサンプリング
→ 週末にマージして更新
大規模サービス(サーバー20台以上):
継続的プロファイリングサービス導入
→ 全サーバーから自動収集
→ 月次でPGO用プロファイル更新
チェックリスト:代表的なプロファイルの条件
- ✅ 複数の時間帯(朝・昼・夜)をカバーしている
- ✅ 複数のサーバーインスタンスから収集している
- ✅ ピーク時とオフピーク時の両方を含んでいる
- ✅ 平日と週末の両方のデータがある
- ✅ すべての主要機能が実行されている期間をカバーしている
- ❌ 1つのサーバーから1回だけ収集していない
- ❌ 深夜など特殊な時間帯のみのデータではない
- ❌ テスト環境やステージング環境のデータを使っていない
まとめ
「1台のサーバーから30秒」という最も簡単な方法は、PGOを試すスタート地点としては良いですが、本番運用では複数のプロファイルを収集してマージすることが重要です。理想的には、継続的プロファイリングサービスを導入して、自動的に代表的なプロファイルを収集・更新する仕組みを構築しましょう。これにより、常に最新の本番動作を反映した最適化を維持できます。
おわりに
本日は、Go言語のプロファイルガイド最適化について解説しました。

何か質問や相談があれば、コメントをお願いします。また、エンジニア案件の相談にも随時対応していますので、お気軽にお問い合わせください。
それでは、また明日お会いしましょう(^^)

コメント