Go言語入門:パフォーマンス診断 -Vol.7-

スポンサーリンク
Go言語入門:パフォーマンス診断 -Vol.7- ノウハウ
Go言語入門:パフォーマンス診断 -Vol.7-
この記事は約15分で読めます。
よっしー
よっしー

こんにちは。よっしーです(^^)

本日は、Go言語のパフォーマンス分析ついて解説しています。

スポンサーリンク

背景

Go言語でアプリケーションを開発していると、必ずと言っていいほど直面するのがパフォーマンスの問題や予期しないバグです。「なぜこんなにメモリを消費しているのか?」「どこで処理が遅くなっているのか?」「このゴルーチンはなぜデッドロックしているのか?」—— こうした疑問に答えるために、Goのエコシステムには強力な診断ツール群が用意されています。

しかし、これらのツールは種類が多く、それぞれ異なる目的と特性を持っているため、どのような場面でどのツールを使うべきか迷ってしまうことも少なくありません。プロファイリング、トレーシング、デバッギング、ランタイム統計…それぞれのツールが解決できる問題は異なり、時には相互に干渉し合うこともあります。

本記事では、Go公式ドキュメントの診断ツールに関する解説を日本語で紹介しながら、実際の開発現場でどのように活用できるかを探っていきます。適切なツールを選択し、効果的に問題を診断することで、より高品質で高性能なGoアプリケーションの開発が可能になるはずです。特に、パフォーマンスのボトルネックを特定し改善することは、ユーザー体験の向上やインフラコストの削減に直結する重要なスキルと言えるでしょう。

プロファイラーハンドラー(/debug/pprof/…)を異なるパスとポートで提供できますか?

はい。net/http/pprofパッケージはデフォルトでハンドラーをデフォルトのmuxに登録しますが、パッケージからエクスポートされたハンドラーを使用して自分で登録することもできます。

例えば、以下の例では、pprof.Profileハンドラーを:7777の/custom_debug_path/profileで提供します:

package main

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

func main() {
	mux := http.NewServeMux()
	mux.HandleFunc("/custom_debug_path/profile", pprof.Profile)
	log.Fatal(http.ListenAndServe(":7777", mux))
}

プロファイラーのカスタム設定 – セキュリティと利便性の両立

なぜカスタマイズが必要?

例え: 家の玄関(メインポート)とは別に、整備士専用の入り口(デバッグポート)を作るようなもの

デフォルト設定の問題点

import _ "net/http/pprof"  // これだけだと...

// 自動的に以下が公開される:
// http://localhost:8080/debug/pprof/
// → メインアプリと同じポート
// → 誰でもアクセス可能
// → パスが予測可能

セキュリティリスク

  • 本番環境で誤って公開してしまう
  • 攻撃者に内部情報を晒す可能性
  • DoS攻撃の標的になる

安全なカスタム実装

1. 別ポートで提供する例

package main

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

func main() {
    // メインアプリケーション(ポート8080)
    go func() {
        mainMux := http.NewServeMux()
        mainMux.HandleFunc("/", handleHome)
        mainMux.HandleFunc("/api/users", handleUsers)
        
        log.Println("Main app listening on :8080")
        log.Fatal(http.ListenAndServe(":8080", mainMux))
    }()
    
    // デバッグ用サーバー(ポート6060)
    debugMux := http.NewServeMux()
    
    // カスタムパスで各プロファイラーを登録
    debugMux.HandleFunc("/debug/pprof/", pprof.Index)
    debugMux.HandleFunc("/debug/pprof/cmdline", pprof.Cmdline)
    debugMux.HandleFunc("/debug/pprof/profile", pprof.Profile)
    debugMux.HandleFunc("/debug/pprof/symbol", pprof.Symbol)
    debugMux.HandleFunc("/debug/pprof/trace", pprof.Trace)
    
    log.Println("Debug server listening on :6060")
    log.Fatal(http.ListenAndServe(":6060", debugMux))
}

2. カスタムパスと認証を追加

package main

import (
    "crypto/subtle"
    "log"
    "net/http"
    "net/http/pprof"
    "os"
)

// Basic認証ミドルウェア
func basicAuth(handler http.HandlerFunc) http.HandlerFunc {
    return func(w http.ResponseWriter, r *http.Request) {
        user, pass, ok := r.BasicAuth()
        
        expectedUser := os.Getenv("PPROF_USER")     // 環境変数から
        expectedPass := os.Getenv("PPROF_PASSWORD")
        
        // セキュアな比較
        userMatch := subtle.ConstantTimeCompare([]byte(user), []byte(expectedUser))
        passMatch := subtle.ConstantTimeCompare([]byte(pass), []byte(expectedPass))
        
        if !ok || userMatch != 1 || passMatch != 1 {
            w.Header().Set("WWW-Authenticate", `Basic realm="Restricted"`)
            http.Error(w, "Unauthorized", http.StatusUnauthorized)
            return
        }
        
        handler(w, r)
    }
}

func main() {
    debugMux := http.NewServeMux()
    
    // 予測困難なパス + 認証
    secretPath := os.Getenv("PPROF_PATH") // 例: "/my-secret-debug-2024/"
    
    debugMux.HandleFunc(secretPath, basicAuth(pprof.Index))
    debugMux.HandleFunc(secretPath+"cmdline", basicAuth(pprof.Cmdline))
    debugMux.HandleFunc(secretPath+"profile", basicAuth(pprof.Profile))
    debugMux.HandleFunc(secretPath+"symbol", basicAuth(pprof.Symbol))
    debugMux.HandleFunc(secretPath+"trace", basicAuth(pprof.Trace))
    
    // 内部ネットワークからのみアクセス可能
    log.Fatal(http.ListenAndServe("127.0.0.1:6060", debugMux))
}

実践的な設定パターン

パターン1: 開発環境と本番環境の切り替え

type Config struct {
    EnableProfiling bool
    ProfilingPort   string
    ProfilingPath   string
}

func setupProfiling(config Config) {
    if !config.EnableProfiling {
        log.Println("Profiling disabled")
        return
    }
    
    mux := http.NewServeMux()
    
    // 全てのプロファイラーハンドラーを登録
    handlers := map[string]http.HandlerFunc{
        "/":          pprof.Index,
        "/cmdline":   pprof.Cmdline,
        "/profile":   pprof.Profile,
        "/symbol":    pprof.Symbol,
        "/trace":     pprof.Trace,
        "/goroutine": pprof.Handler("goroutine").ServeHTTP,
        "/heap":      pprof.Handler("heap").ServeHTTP,
        "/block":     pprof.Handler("block").ServeHTTP,
        "/mutex":     pprof.Handler("mutex").ServeHTTP,
    }
    
    for path, handler := range handlers {
        fullPath := config.ProfilingPath + path
        mux.HandleFunc(fullPath, handler)
    }
    
    go func() {
        log.Printf("Profiling server listening on %s", config.ProfilingPort)
        log.Fatal(http.ListenAndServe(config.ProfilingPort, mux))
    }()
}

func main() {
    // 環境に応じた設定
    config := Config{
        EnableProfiling: os.Getenv("ENV") == "development",
        ProfilingPort:   ":6060",
        ProfilingPath:   "/debug/pprof",
    }
    
    if os.Getenv("ENV") == "production" {
        config = Config{
            EnableProfiling: os.Getenv("ENABLE_PROFILING") == "true",
            ProfilingPort:   "127.0.0.1:9999", // ローカルのみ
            ProfilingPath:   "/" + os.Getenv("SECRET_PATH") + "/",
        }
    }
    
    setupProfiling(config)
    
    // メインアプリケーション起動
    runMainApp()
}

パターン2: 条件付きアクセス制御

func conditionalProfiling() {
    mux := http.NewServeMux()
    
    // IPアドレスベースの制限
    allowedIPs := map[string]bool{
        "127.0.0.1": true,
        "10.0.0.1":  true, // 社内ネットワーク
    }
    
    profileHandler := func(handler http.HandlerFunc) http.HandlerFunc {
        return func(w http.ResponseWriter, r *http.Request) {
            // IPチェック
            clientIP := r.RemoteAddr
            if !allowedIPs[clientIP] {
                http.Error(w, "Forbidden", http.StatusForbidden)
                return
            }
            
            // 時間帯チェック(メンテナンス時間のみ許可)
            hour := time.Now().Hour()
            if hour < 2 || hour > 4 { // 午前2時〜4時のみ
                http.Error(w, "Service available 2-4 AM only", http.StatusServiceUnavailable)
                return
            }
            
            handler(w, r)
        }
    }
    
    // ハンドラー登録
    mux.HandleFunc("/secure-debug/profile", profileHandler(pprof.Profile))
}

pprofハンドラー一覧と設定方法

// 利用可能な全ハンドラー
func registerAllHandlers(mux *http.ServeMux, basePath string) {
    // 関数として提供されるハンドラー
    mux.HandleFunc(basePath+"/", pprof.Index)
    mux.HandleFunc(basePath+"/cmdline", pprof.Cmdline)
    mux.HandleFunc(basePath+"/profile", pprof.Profile)
    mux.HandleFunc(basePath+"/symbol", pprof.Symbol)
    mux.HandleFunc(basePath+"/trace", pprof.Trace)
    
    // http.Handlerとして提供されるハンドラー
    mux.Handle(basePath+"/goroutine", pprof.Handler("goroutine"))
    mux.Handle(basePath+"/heap", pprof.Handler("heap"))
    mux.Handle(basePath+"/threadcreate", pprof.Handler("threadcreate"))
    mux.Handle(basePath+"/block", pprof.Handler("block"))
    mux.Handle(basePath+"/mutex", pprof.Handler("mutex"))
    
    // カスタムプロファイル
    mux.Handle(basePath+"/custom", pprof.Handler("myapp.custom"))
}

Dockerとの組み合わせ

# Dockerfile
FROM golang:1.21-alpine

WORKDIR /app
COPY . .

# デバッグポートを公開
EXPOSE 8080 6060

CMD ["./app"]
# docker-compose.yml
version: '3'
services:
  app:
    build: .
    ports:
      - "8080:8080"  # メインアプリ
      - "127.0.0.1:6060:6060"  # プロファイリング(ローカルのみ)
    environment:
      - PPROF_ENABLED=true
      - PPROF_PATH=/secret-debug-path

アクセス方法

# カスタムパスにアクセス
curl http://localhost:7777/custom_debug_path/profile?seconds=30 > cpu.prof

# 認証付きアクセス
curl -u admin:password http://localhost:6060/secure-debug/heap > heap.prof

# pprofツールで直接接続
go tool pprof http://localhost:7777/custom_debug_path/profile

ベストプラクティス

  1. 本番環境では必ず別ポート使用
  2. 認証を必須にする
  3. 予測困難なパスを使用
  4. アクセスログを記録
  5. ネットワークレベルでも制限
  6. 必要時のみ有効化

監視とアラート設定

// プロファイリングアクセスの監視
func monitorProfilingAccess() http.HandlerFunc {
    return func(w http.ResponseWriter, r *http.Request) {
        log.Printf("Profiling access: IP=%s, Path=%s, Time=%s",
            r.RemoteAddr, r.URL.Path, time.Now())
        
        // アラート送信(必要に応じて)
        if shouldAlert(r) {
            sendAlert("Unexpected profiling access detected")
        }
        
        pprof.Index(w, r)
    }
}

カスタムパスとポートを使用することで、セキュリティと利便性のバランスを取りながら、効果的にプロファイリングを行えます!

おわりに 

本日は、Go言語のパフォーマンス分析について解説しました。

よっしー
よっしー

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

それでは、また明日お会いしましょう(^^)

コメント

タイトルとURLをコピーしました