本番環境でのトラブルシューティング
本番環境で稼働する 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...)
}
}
本番環境での注意点
セキュリティ考慮事項
- pprof エンドポイント: 本番環境では認証付きで公開するか、内部ネットワークのみからアクセス可能にする
- デバッグ情報: 機密情報がログに含まれないよう注意
- ヘルスチェック: 詳細すぎる情報を外部に公開しない
パフォーマンス影響
- プロファイリング: 常時有効にするとパフォーマンスに影響するため、必要時のみ有効化
- ログレベル: 本番環境では適切なログレベルを設定
- メトリクス収集: 過度なメトリクス収集はオーバーヘッドとなる
これらのツールと技法を使用することで、本番環境での Go アプリケーションの問題を効率的に診断・解決できます。