本番環境でのトラブルシューティング

本番環境でのトラブルシューティング

本番環境で稼働する Go アプリケーションの問題を診断・解決する方法について説明します。

パフォーマンス分析

pprof パッケージ

pprof パッケージを使用して Go アプリのパフォーマンスを理解する

Web アプリケーションでの pprof 有効化

import (
    _ "net/http/pprof"
    "net/http"
)

func main() {
    // あなたのアプリケーションコード
    
    // pprof エンドポイントを公開
    go func() {
        log.Println(http.ListenAndServe("localhost:6060", nil))
    }()
    
    // メインアプリケーション
    // ...
}

プロファイリングデータの取得

# CPU プロファイルを取得
go tool pprof http://localhost:6060/debug/pprof/profile?seconds=30

# メモリプロファイルを取得
go tool pprof http://localhost:6060/debug/pprof/heap

# ゴルーチンプロファイルを取得
go tool pprof http://localhost:6060/debug/pprof/goroutine

# ミューテックス競合プロファイルを取得
go tool pprof http://localhost:6060/debug/pprof/mutex

プロファイルの分析

# 対話的なモードで分析
go tool pprof profile.prof

# Web UI で分析
go tool pprof -http=:8080 profile.prof

# 主要なコマンド
# (pprof) top10      - トップ10の関数を表示
# (pprof) list main  - main関数の詳細を表示
# (pprof) web        - グラフ表示(要Graphviz)

ベンチマークとプロファイリング

// ベンチマークテストでプロファイリング
func BenchmarkMyFunction(b *testing.B) {
    for i := 0; i < b.N; i++ {
        MyFunction()
    }
}
# CPU プロファイル付きベンチマーク実行
go test -bench=. -cpuprofile=cpu.prof

# メモリプロファイル付きベンチマーク実行
go test -bench=. -memprofile=mem.prof

# プロファイル結果を分析
go tool pprof cpu.prof

ヒープダンプ

Go バージョン別のヒープダンプ

Go 1.3

heapdump13 - Go 1.3 でのヒープダンプ取得方法。

Go 1.4

heapdump14 - Go 1.4 でのヒープダンプ取得方法。

Go 1.5 以降

heapdump15-through-heapdump17 - Go 1.5 から 1.7 でのヒープダンプ取得方法。

現代的なメモリ分析

// メモリプロファイルを手動で取得
import (
    "os"
    "runtime/pprof"
)

func captureHeapProfile() {
    f, err := os.Create("heap.prof")
    if err != nil {
        log.Fatal(err)
    }
    defer f.Close()
    
    runtime.GC() // ガベージコレクションを実行
    if err := pprof.WriteHeapProfile(f); err != nil {
        log.Fatal(err)
    }
}

メモリリーク検出

# 定期的にヒーププロファイルを取得
curl http://localhost:6060/debug/pprof/heap > heap1.prof
# 時間を置いて
curl http://localhost:6060/debug/pprof/heap > heap2.prof

# 差分を比較
go tool pprof -base heap1.prof heap2.prof

ログとモニタリング

構造化ログ

import (
    "log/slog"
    "os"
)

func main() {
    logger := slog.New(slog.NewJSONHandler(os.Stdout, nil))
    
    logger.Info("Application started",
        "version", "1.0.0",
        "port", 8080)
    
    logger.Error("Database connection failed",
        "error", err,
        "host", "localhost",
        "database", "myapp")
}

メトリクス収集

// Prometheus メトリクスの例
import (
    "github.com/prometheus/client_golang/prometheus"
    "github.com/prometheus/client_golang/prometheus/promhttp"
)

var (
    requestsTotal = prometheus.NewCounterVec(
        prometheus.CounterOpts{
            Name: "http_requests_total",
            Help: "Total number of HTTP requests",
        },
        []string{"method", "endpoint", "status_code"},
    )
    
    requestDuration = prometheus.NewHistogramVec(
        prometheus.HistogramOpts{
            Name: "http_request_duration_seconds",
            Help: "HTTP request duration in seconds",
        },
        []string{"method", "endpoint"},
    )
)

func init() {
    prometheus.MustRegister(requestsTotal)
    prometheus.MustRegister(requestDuration)
}

// メトリクスエンドポイントを公開
http.Handle("/metrics", promhttp.Handler())

トレーシング

分散トレーシング

// OpenTelemetry を使用した例
import (
    "go.opentelemetry.io/otel"
    "go.opentelemetry.io/otel/trace"
)

func processRequest(ctx context.Context) {
    tracer := otel.Tracer("myapp")
    ctx, span := tracer.Start(ctx, "process_request")
    defer span.End()
    
    // ビジネスロジック
    result, err := doSomething(ctx)
    if err != nil {
        span.SetStatus(codes.Error, err.Error())
        return
    }
    
    span.SetAttributes(attribute.String("result", result))
}

ヘルスチェック

アプリケーションヘルスチェック

type HealthChecker struct {
    db     *sql.DB
    redis  *redis.Client
    // 他の依存関係
}

func (h *HealthChecker) CheckHealth() map[string]interface{} {
    health := map[string]interface{}{
        "status":    "healthy",
        "timestamp": time.Now(),
        "checks":    map[string]interface{}{},
    }
    
    // データベース接続チェック
    if err := h.db.Ping(); err != nil {
        health["status"] = "unhealthy"
        health["checks"].(map[string]interface{})["database"] = map[string]interface{}{
            "status": "down",
            "error":  err.Error(),
        }
    } else {
        health["checks"].(map[string]interface{})["database"] = map[string]interface{}{
            "status": "up",
        }
    }
    
    return health
}

// ヘルスチェックエンドポイント
func healthHandler(w http.ResponseWriter, r *http.Request) {
    health := healthChecker.CheckHealth()
    
    w.Header().Set("Content-Type", "application/json")
    if health["status"] == "unhealthy" {
        w.WriteHeader(http.StatusServiceUnavailable)
    }
    
    json.NewEncoder(w).Encode(health)
}

ランタイム情報

ランタイムメトリクス

import "runtime"

func runtimeStats() {
    var m runtime.MemStats
    runtime.ReadMemStats(&m)
    
    fmt.Printf("Allocated memory: %d KB\n", m.Alloc/1024)
    fmt.Printf("Total allocations: %d\n", m.TotalAlloc/1024)
    fmt.Printf("System memory: %d KB\n", m.Sys/1024)
    fmt.Printf("Number of GC cycles: %d\n", m.NumGC)
    fmt.Printf("Number of goroutines: %d\n", runtime.NumGoroutine())
}

環境情報の収集

func systemInfo() {
    fmt.Printf("Go version: %s\n", runtime.Version())
    fmt.Printf("OS: %s\n", runtime.GOOS)
    fmt.Printf("Architecture: %s\n", runtime.GOARCH)
    fmt.Printf("CPU cores: %d\n", runtime.NumCPU())
}

デバッグ技法

デバッグビルド

# デバッグ情報付きでビルド
go build -gcflags="-N -l" myapp.go

# Delve デバッガーでデバッグ
dlv exec ./myapp

# 実行中のプロセスにアタッチ
dlv attach <pid>

ログベースデバッグ

// デバッグレベルに応じたログ出力
const (
    LogLevelError = iota
    LogLevelWarn
    LogLevelInfo
    LogLevelDebug
)

func debugLog(level int, msg string, args ...interface{}) {
    if level <= currentLogLevel {
        log.Printf("[DEBUG] "+msg, args...)
    }
}

本番環境での注意点

セキュリティ考慮事項

  1. pprof エンドポイント: 本番環境では認証付きで公開するか、内部ネットワークのみからアクセス可能にする
  2. デバッグ情報: 機密情報がログに含まれないよう注意
  3. ヘルスチェック: 詳細すぎる情報を外部に公開しない

パフォーマンス影響

  1. プロファイリング: 常時有効にするとパフォーマンスに影響するため、必要時のみ有効化
  2. ログレベル: 本番環境では適切なログレベルを設定
  3. メトリクス収集: 過度なメトリクス収集はオーバーヘッドとなる

これらのツールと技法を使用することで、本番環境での Go アプリケーションの問題を効率的に診断・解決できます。