🔍

Cloud ProfilerでGoのメモリリークを検出する

に公開

はじめに

Cloud ProfilerはGoogle Cloudにデプロイされたシステムのパフォーマンスを分析できるプロファイリングツールです。
最近業務で利用する機会があったので、GoでのメモリリークをサンプルケースにCloud Profilerを利用する手順を紹介します。

Cloud Profilerについて

Cloud Profilerを活用することで、本番環境で実際に発生している問題を直接分析し、原因を特定できます。
本番環境特有の負荷や実データによる挙動など、ローカル環境では再現が難しいケースに有効です。

主な特徴

  • Go、Java、Node.js、Pythonに対応
  • Compute Engine、Google Kubernetes Engine、App Engineなど複数の環境で利用可能
  • リアルタイムでのパフォーマンス可視化
  • 本番環境への影響が最小限

プロファイリングの種類

  • CPU時間
  • ヒープ使用量
  • スレッド情報
  • 経過時間

Cloud Profilerについての詳細は下記を参照ください。
https://docs.cloud.google.com/profiler/docs/about-profiler?hl=ja

サンプルの準備

今回はGoで簡単なアプリケーションを作成し、loggingエージェントのクローズ処理を忘れたことで発生するメモリリークをCloud Profilerで確認してみます。

Profilerの使用にはパッケージをインポートして、プロファイラを初期化するだけで使用できます。

import (
	"cloud.google.com/go/profiler"
)

if err := profiler.Start(profiler.Config{
    Service:        "service-name",
    ServiceVersion: "1.0.0",
    ProjectID:      "project-id"
}); err != nil {
    log.Fatalf("Failed to start profiler: %v", err)
}

サンプルコード

package main

import (
    "context"
    "fmt"
    "log"
    "net/http"
    "os"
    "time"

    "cloud.google.com/go/logging"
    "cloud.google.com/go/profiler"
)

// 環境変数にプロジェクトのIDを設定
var projectId = os.Getenv("PROJECT_ID")

func main() {
    // Cloud Profilerの初期化
    if err := profiler.Start(profiler.Config{
        Service:        "logging-leak-sample",
        ServiceVersion: "1.0.0",
        ProjectID:      projectId,
    }); err != nil {
        log.Fatalf("Failed to start profiler: %v", err)
    }

    // HTTPハンドラの設定
    http.HandleFunc("/leak", handleLeakWithLogging)

    log.Println("Server starting on :8080")
    if err := http.ListenAndServe(":8080", nil); err != nil {
        log.Fatalf("Server failed: %v", err)
    }
}

// メモリリークを発生させるハンドラ
func handleLeakWithLogging(w http.ResponseWriter, r *http.Request) {
    ctx := context.Background()

    // リクエストごとに新しいloggingクライアントを生成
    client, err := logging.NewClient(ctx, projectId)
    if err != nil {
        http.Error(w, "Failed to create logging client", http.StatusInternalServerError)
        return
    }
    // client.Close()を実行しないため、メモリリークが発生する

    // ログを書き込む
    logger := client.Logger("leak-sample-log")
    logger.Log(logging.Entry{
        Payload:  fmt.Sprintf("Request received at %s", time.Now().Format(time.RFC3339)),
        Severity: logging.Info,
    })
}

Cloud Profilerコンソールでの確認

サンプルコードをデプロイしてCloud Profilerを確認すると各種プロファイルが表示されています。

複数回リクエストを送信すると、リクエスト受信毎にメモリが増加しています。

Cloud Profilerでヒープを確認すると、「logging.NewClient」でメモリを大量に使用していることが確認できました。

メモリリーク改善

先ほどのサンプルコードにloggingクライアントをクローズする処理を追記して再度リクエストを送信してみます。

// リクエストごとに新しいloggingクライアントを生成
client, err := logging.NewClient(ctx, projectId)
if err != nil {
    http.Error(w, "Failed to create logging client", http.StatusInternalServerError)
    return
}

defer client.Close() // ここを追記

メモリ消費量を確認すると改善されていることが確認できました!

まとめ

今回はCloud Profilerを使用して、Go言語でのメモリリークのサンプルケースを検出してみました。
本番環境でプロファイリングできるため、ローカルで分析が難しい場合でも簡単に導入できるのが便利でした。

参考

https://cloud.google.com/profiler/docs/about-profiler?hl=ja
https://docs.cloud.google.com/profiler/docs/concepts-profiling?hl=ja

レスキューナウテックブログ

Discussion