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についての詳細は下記を参照ください。
サンプルの準備
今回は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言語でのメモリリークのサンプルケースを検出してみました。
本番環境でプロファイリングできるため、ローカルで分析が難しい場合でも簡単に導入できるのが便利でした。
参考
Discussion