
こんにちは。よっしーです(^^)
本日は、Go言語のパフォーマンス分析ついて解説しています。
背景
Go言語でアプリケーションを開発していると、必ずと言っていいほど直面するのがパフォーマンスの問題や予期しないバグです。「なぜこんなにメモリを消費しているのか?」「どこで処理が遅くなっているのか?」「このゴルーチンはなぜデッドロックしているのか?」—— こうした疑問に答えるために、Goのエコシステムには強力な診断ツール群が用意されています。
しかし、これらのツールは種類が多く、それぞれ異なる目的と特性を持っているため、どのような場面でどのツールを使うべきか迷ってしまうことも少なくありません。プロファイリング、トレーシング、デバッギング、ランタイム統計…それぞれのツールが解決できる問題は異なり、時には相互に干渉し合うこともあります。
本記事では、Go公式ドキュメントの診断ツールに関する解説を日本語で紹介しながら、実際の開発現場でどのように活用できるかを探っていきます。適切なツールを選択し、効果的に問題を診断することで、より高品質で高性能なGoアプリケーションの開発が可能になるはずです。特に、パフォーマンスのボトルネックを特定し改善することは、ユーザー体験の向上やインフラコストの削減に直結する重要なスキルと言えるでしょう。
ランタイム統計とイベント
ランタイムは、ユーザーがランタイムレベルでのパフォーマンスと使用率の問題を診断するために、内部イベントの統計とレポートを提供します。
ユーザーはこれらの統計を監視することで、Goプログラムの全体的な健全性とパフォーマンスをより良く理解できます。頻繁に監視される統計と状態:
runtime.ReadMemStatsは、ヒープ割り当てとガベージコレクションに関連するメトリクスを報告します。メモリ統計は、プロセスがどれだけのメモリリソースを消費しているか、プロセスがメモリを適切に利用できているか、メモリリークを検出するために有用です。debug.ReadGCStatsは、ガベージコレクションに関する統計を読み取ります。GCの一時停止にどれだけのリソースが費やされているかを確認するのに有用です。また、ガベージコレクタの一時停止のタイムラインと一時停止時間のパーセンタイルも報告します。debug.Stackは、現在のスタックトレースを返します。スタックトレースは、現在実行中のゴルーチンの数、それらが何をしているか、ブロックされているかどうかを確認するのに有用です。debug.WriteHeapDumpは、すべてのゴルーチンの実行を一時停止し、ヒープをファイルにダンプできるようにします。ヒープダンプは、特定の時点でのGoプロセスのメモリのスナップショットです。割り当てられたすべてのオブジェクト、ゴルーチン、ファイナライザーなどが含まれます。runtime.NumGoroutineは、現在のゴルーチン数を返します。この値を監視することで、十分なゴルーチンが利用されているか、またはゴルーチンリークを検出できます。
ランタイム統計 – プログラムの「健康診断データ」
ランタイム統計とは?
例え: 人間の健康診断で血圧、体温、血糖値を測るように、プログラムの「健康状態」を数値で把握する仕組み
主要な診断ツール一覧
| ツール | 診断対象 | 例え |
|---|---|---|
ReadMemStats | メモリ使用状況 | 体重・体脂肪率 |
ReadGCStats | GCパフォーマンス | 血圧・脈拍 |
Stack | ゴルーチン状態 | レントゲン写真 |
WriteHeapDump | メモリ詳細 | MRI検査 |
NumGoroutine | 並行処理数 | 白血球数 |
runtime.ReadMemStats – メモリ診断
基本的な使用方法
package main
import (
"fmt"
"runtime"
"time"
)
func monitorMemory() {
var m runtime.MemStats
// メモリ統計を読み取る
runtime.ReadMemStats(&m)
fmt.Printf("=== メモリ統計 ===\n")
fmt.Printf("現在のメモリ使用量: %v MB\n", m.Alloc/1024/1024)
fmt.Printf("累計メモリ割り当て: %v MB\n", m.TotalAlloc/1024/1024)
fmt.Printf("システムメモリ: %v MB\n", m.Sys/1024/1024)
fmt.Printf("GC実行回数: %v\n", m.NumGC)
fmt.Printf("ゴルーチン数: %v\n", runtime.NumGoroutine())
}
// メモリリーク検出の例
func detectMemoryLeak() {
ticker := time.NewTicker(1 * time.Second)
defer ticker.Stop()
var prevAlloc uint64
threshold := uint64(10 * 1024 * 1024) // 10MB
for range ticker.C {
var m runtime.MemStats
runtime.ReadMemStats(&m)
if prevAlloc > 0 {
growth := m.Alloc - prevAlloc
if growth > threshold {
fmt.Printf("⚠️ メモリ急増検出: +%v MB\n",
growth/1024/1024)
}
}
prevAlloc = m.Alloc
}
}
重要なMemStatsフィールド
type MemoryMonitor struct {
stats runtime.MemStats
}
func (mm *MemoryMonitor) CollectAndAnalyze() {
runtime.ReadMemStats(&mm.stats)
// 重要な指標
metrics := map[string]uint64{
"割り当て中メモリ": mm.stats.Alloc,
"累計割り当て": mm.stats.TotalAlloc,
"ヒープ割り当て": mm.stats.HeapAlloc,
"ヒープシステム": mm.stats.HeapSys,
"ヒープ使用中": mm.stats.HeapInuse,
"ヒープ解放済み": mm.stats.HeapReleased,
"オブジェクト数": mm.stats.HeapObjects,
"スタック使用量": mm.stats.StackInuse,
"GC回数": mm.stats.NumGC,
"最後のGC時刻": mm.stats.LastGC,
"次のGC目標": mm.stats.NextGC,
}
for name, value := range metrics {
if name == "最後のGC時刻" {
fmt.Printf("%s: %v\n", name,
time.Unix(0, int64(value)).Format(time.RFC3339))
} else if strings.Contains(name, "メモリ") ||
strings.Contains(name, "ヒープ") {
fmt.Printf("%s: %v MB\n", name, value/1024/1024)
} else {
fmt.Printf("%s: %v\n", name, value)
}
}
}
debug.ReadGCStats – GC診断
package main
import (
"fmt"
"runtime/debug"
"time"
)
func analyzeGC() {
var stats debug.GCStats
// GC統計を読み取る
debug.ReadGCStats(&stats)
fmt.Println("=== GC統計分析 ===")
fmt.Printf("最後のGC: %v\n", stats.LastGC)
fmt.Printf("GC実行回数: %v\n", stats.NumGC)
fmt.Printf("総停止時間: %v\n", stats.PauseTotal)
// パーセンタイル分析
fmt.Println("\nGC停止時間分布:")
percentiles := []float64{0.0, 0.25, 0.50, 0.75, 0.90, 0.95, 0.99, 1.0}
labels := []string{"最小", "25%", "50%", "75%", "90%", "95%", "99%", "最大"}
for i, p := range percentiles {
// 停止時間の分位数を計算
idx := int(float64(len(stats.Pause)) * p)
if idx >= len(stats.Pause) {
idx = len(stats.Pause) - 1
}
if idx >= 0 && len(stats.Pause) > 0 {
fmt.Printf("%s: %v\n", labels[i], stats.Pause[idx])
}
}
// 最近のGC履歴
fmt.Println("\n最近5回のGC停止時間:")
count := 5
if len(stats.Pause) < count {
count = len(stats.Pause)
}
for i := 0; i < count; i++ {
fmt.Printf(" GC #%d: %v\n", i+1, stats.Pause[i])
}
}
// GCパフォーマンス監視
func monitorGCPerformance() {
ticker := time.NewTicker(10 * time.Second)
defer ticker.Stop()
var prevNumGC int64
for range ticker.C {
var stats debug.GCStats
debug.ReadGCStats(&stats)
if prevNumGC > 0 {
gcCount := stats.NumGC - prevNumGC
avgPause := stats.PauseTotal / time.Duration(stats.NumGC)
fmt.Printf("過去10秒間: GC %d回, 平均停止時間: %v\n",
gcCount, avgPause)
// 警告閾値
if avgPause > 10*time.Millisecond {
fmt.Println("⚠️ GC停止時間が長い!")
}
}
prevNumGC = stats.NumGC
}
}
debug.Stack – スタックトレース分析
package main
import (
"bytes"
"fmt"
"runtime/debug"
"strings"
)
func analyzeStack() {
// 現在のスタックトレース取得
stack := debug.Stack()
// スタック情報を解析
reader := bytes.NewReader(stack)
scanner := bufio.NewScanner(reader)
goroutineCount := 0
blockedCount := 0
for scanner.Scan() {
line := scanner.Text()
if strings.Contains(line, "goroutine") {
goroutineCount++
if strings.Contains(line, "[chan receive]") ||
strings.Contains(line, "[select]") ||
strings.Contains(line, "[sync.Mutex.Lock]") {
blockedCount++
}
}
}
fmt.Printf("=== スタック分析 ===\n")
fmt.Printf("総ゴルーチン数: %d\n", goroutineCount)
fmt.Printf("ブロック中: %d\n", blockedCount)
fmt.Printf("アクティブ: %d\n", goroutineCount-blockedCount)
// 詳細表示(最初の500文字)
if len(stack) > 500 {
fmt.Printf("\nスタックトレース(抜粋):\n%s...\n", stack[:500])
} else {
fmt.Printf("\nスタックトレース:\n%s\n", stack)
}
}
// ゴルーチンリーク検出
func detectGoroutineLeaks() {
baseline := runtime.NumGoroutine()
// 処理実行
doWork()
// GCを強制実行して不要なゴルーチンをクリーンアップ
runtime.GC()
time.Sleep(100 * time.Millisecond)
current := runtime.NumGoroutine()
if current > baseline+10 { // 許容値を超えた場合
fmt.Printf("⚠️ ゴルーチンリークの可能性: %d → %d\n",
baseline, current)
// スタックトレースで詳細確認
fmt.Println(string(debug.Stack()))
}
}
debug.WriteHeapDump – ヒープダンプ
package main
import (
"fmt"
"os"
"runtime/debug"
"time"
)
func createHeapDump() error {
// ヒープダンプファイルを作成
filename := fmt.Sprintf("heapdump_%d.hprof", time.Now().Unix())
file, err := os.Create(filename)
if err != nil {
return fmt.Errorf("ファイル作成エラー: %v", err)
}
defer file.Close()
// ヒープダンプを書き込み(全ゴルーチンが一時停止)
debug.WriteHeapDump(file.Fd())
fileInfo, _ := file.Stat()
fmt.Printf("ヒープダンプ作成: %s (サイズ: %v MB)\n",
filename, fileInfo.Size()/1024/1024)
return nil
}
// 条件付きヒープダンプ
func conditionalHeapDump() {
var m runtime.MemStats
runtime.ReadMemStats(&m)
// メモリ使用量が閾値を超えたらダンプ
threshold := uint64(500 * 1024 * 1024) // 500MB
if m.Alloc > threshold {
fmt.Printf("メモリ使用量が閾値超過: %v MB\n", m.Alloc/1024/1024)
createHeapDump()
}
}
統合監視システムの実装
package monitor
import (
"encoding/json"
"fmt"
"runtime"
"runtime/debug"
"time"
)
type HealthMetrics struct {
Timestamp time.Time `json:"timestamp"`
MemoryAlloc uint64 `json:"memory_alloc_mb"`
MemoryTotalAlloc uint64 `json:"memory_total_alloc_mb"`
GoroutineCount int `json:"goroutine_count"`
GCCount uint32 `json:"gc_count"`
LastGC time.Time `json:"last_gc"`
CPUCount int `json:"cpu_count"`
}
type HealthMonitor struct {
metrics chan HealthMetrics
alerts chan string
}
func NewHealthMonitor() *HealthMonitor {
return &HealthMonitor{
metrics: make(chan HealthMetrics, 100),
alerts: make(chan string, 10),
}
}
func (hm *HealthMonitor) Start(interval time.Duration) {
ticker := time.NewTicker(interval)
go hm.processAlerts()
go func() {
for range ticker.C {
metrics := hm.collect()
hm.analyze(metrics)
hm.metrics <- metrics
}
}()
}
func (hm *HealthMonitor) collect() HealthMetrics {
var m runtime.MemStats
runtime.ReadMemStats(&m)
return HealthMetrics{
Timestamp: time.Now(),
MemoryAlloc: m.Alloc / 1024 / 1024,
MemoryTotalAlloc: m.TotalAlloc / 1024 / 1024,
GoroutineCount: runtime.NumGoroutine(),
GCCount: m.NumGC,
LastGC: time.Unix(0, int64(m.LastGC)),
CPUCount: runtime.NumCPU(),
}
}
func (hm *HealthMonitor) analyze(m HealthMetrics) {
// メモリリークチェック
if m.MemoryAlloc > 1000 { // 1GB以上
hm.alerts <- fmt.Sprintf("高メモリ使用: %d MB", m.MemoryAlloc)
}
// ゴルーチンリークチェック
if m.GoroutineCount > 10000 {
hm.alerts <- fmt.Sprintf("ゴルーチン数異常: %d", m.GoroutineCount)
}
// GC頻度チェック
if time.Since(m.LastGC) > 1*time.Minute {
hm.alerts <- "GCが長時間実行されていません"
}
}
func (hm *HealthMonitor) processAlerts() {
for alert := range hm.alerts {
fmt.Printf("🚨 アラート: %s\n", alert)
// 実際にはSlack、メール等に通知
}
}
// Prometheusエクスポート用
func (hm *HealthMonitor) ExportMetrics() string {
select {
case m := <-hm.metrics:
data, _ := json.MarshalIndent(m, "", " ")
return string(data)
default:
return "{}"
}
}
実践的な活用例
本番環境での定期レポート
func generateHealthReport() {
report := &bytes.Buffer{}
// メモリ統計
var m runtime.MemStats
runtime.ReadMemStats(&m)
fmt.Fprintf(report, "=== システムヘルスレポート ===\n")
fmt.Fprintf(report, "時刻: %s\n\n", time.Now().Format(time.RFC3339))
fmt.Fprintf(report, "【メモリ】\n")
fmt.Fprintf(report, " 使用中: %v MB\n", m.Alloc/1024/1024)
fmt.Fprintf(report, " 累計: %v MB\n", m.TotalAlloc/1024/1024)
fmt.Fprintf(report, " GC回数: %v\n", m.NumGC)
fmt.Fprintf(report, "\n【ゴルーチン】\n")
fmt.Fprintf(report, " 総数: %v\n", runtime.NumGoroutine())
// GC統計
var gcStats debug.GCStats
debug.ReadGCStats(&gcStats)
fmt.Fprintf(report, "\n【GCパフォーマンス】\n")
fmt.Fprintf(report, " 総停止時間: %v\n", gcStats.PauseTotal)
fmt.Fprintf(report, " 平均停止: %v\n",
gcStats.PauseTotal/time.Duration(gcStats.NumGC))
// レポート送信
sendReport(report.String())
}
ベストプラクティス
- 定期的な監視
// 5秒ごとに軽量チェック // 1分ごとに詳細チェック // 1時間ごとにヒープダンプ(必要時) - 閾値の設定
thresholds: memory_alloc_mb: 500 goroutine_count: 1000 gc_pause_ms: 10 gc_frequency_sec: 60 - アラート統合
// Prometheus、Grafana、Datadogなどと連携
ランタイム統計は、プログラムの「バイタルサイン」を常時監視し、問題を早期発見するための重要なツールです!
おわりに
本日は、Go言語のパフォーマンス分析について解説しました。

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

コメント