every Tech Blog

株式会社エブリーのTech Blogです。

Pyroscope の Continuous Profiling により Go サーバーのメモリリークを調査・改善した話

Eye-catching image

はじめに

子育てメディア「トモニテ」でバックエンドやフロントエンドの設計・開発を担当している桝村です。

2023年8月1日、MAMADAYSはトモニテに生まれかわりました。

tomonite.com

アプリのメイン機能である「育児記録」「妊娠週数管理」「食材リスト」を軸として、家族やパートナー、家族以外の人や社会との接点を作るためのシェア機能やコミュニティ機能などの拡充をめざしていきます。

今回は、Continuous Profiling を実施することができる Pyroscope を使用して、トモニテで運用している Go サーバーのメモリリークを調査・改善した話をしたいと思います。

以前、トモニテでEKSからECSに移行した話という記事において、EKS on EC2 から ECS Fargate への移行に伴い、厳密なリソース管理の必要性が生じ、メモリリークを検出した件の対応になります。

tech.every.tv

Pyroscope について

Pyroscope とは

Continuous Profiling を実施することができるオープンソースのプラットフォームです。

Continuous Profiling とは、ソフトウェアやアプリケーションが実行されている間、リアルタイムでパフォーマンスデータや実行情報を収集し、CPUやメモリ等のリソースをどこで多く消費しているか分析・チューニングする手法です。

2023年3月にデータ可視化ツール Grafana などを開発している Grafana Labs が Pyroscope を買収し、 オープンソース Grafana Pyroscope として統合されました。

本記事では Grafana Pyroscope の情報にも触れつつ、Pyroscope で Go サーバーのメモリリークを調査・改善した話となります。

Pyroscope でできること

  • コード内のパフォーマンスに関連する問題やボトルネックを見つけることができます。例えば、CPU使用率の上昇やメモリリークの発生等です。

pyroscope single view
pyroscope の Single View 画面

  • タグ機能 Tags や時間指定により、UI上のプロファイリングデータを絞り込みできます。

pyroscope tagged view
pyroscope の Tags でのフィルタリング画面

  • ビュー機能 Comparison View により、2つの時間区間を並べて比較分析できます。

pyroscope comparison view
pyroscope の Comparison View 画面

  • ビュー機能 Diff View により、2つの時間区間の差分を取得してヒートマップのように色付けして比較分析できます。

pyroscope diff view
pyroscope の Diff View 画面

参考: demo.pyroscope.io

Pyroscope の仕組み

Pyroscope Agent という言語ごとに用意されているプロセスが、アプリケーションの動作を定期的に記録・集計し、そのデータを Pyroscope Server に送信します。

Pyroscope Server がそのデータを処理・集約することで、ユーザーがプロファイリングの結果を WEB UI から Flame Graph (フレームグラフ)を閲覧することが可能になります。

なので、Pyroscope で Continuous Profiling を実施するには、Pyroscope AgentPyroscope Server の設定が必要になります。

how does pyroscope work
pyroscope の全体像

参考: pyroscope.io

Grafana Pyroscope について

2023年3月にデータ可視化ツール Grafana などを開発している Grafana Labs が Pyroscope を買収し、 オープンソース Grafana Phlare と統合され、 Grafana Pyroscope になりました。

grafana pyroscope log

参考: grafana.com

また、2023年8月末に Grafana Pyroscope として version 1.0 が公開されました。

このバージョンでは、以下をはじめとした改善が行われました。

  • Grafana と完全に統合され、Grafana ダッシュボードでメトリクスやログ、トレースなど他の可観測性の指標と一緒に Profiling のデータを表示可能になりました。

  • 様々なオブジェクトストレージサービス (ex. AWS S3, Google Cloud Storage) との統合がサポートされ、プロファイルデータをストレージサービスに保存可能になりました。

  • 水平方向のスケールアウトがサポートされ、あらゆる規模のプロジェクトで最適なパフォーマンスを出すことが可能になりました。

参考: github.com

Pyroscope を活用したトラブルシューティング

問題だったこと

トモニテで利用している Go サーバーの基盤である ECS のメモリ使用率が時間の経過とともに上昇し続けるという問題がありました。

もしサーバーのメモリ解放せず長時間経過した場合、サーバーの一時停止といったリスクがあったため、調査・改善が求められていました。

memory usage before
ECS メモリ使用率 改善前

Pyroscope の導入

Pyroscope Server の起動

Pyroscope Server をサーバー上で Docker 経由で起動しました。

Pyroscope によるリソース消費の最適化のため、データの保持期間 retentionexemplars-retention を調整し、それ以外の設定値は、デフォルトの値を利用しました。

Pyroscope Server と Go サーバーのネットワーク設定

それぞれでAWSアカウントが異なる構成だったのもあり、ネットワークの疎通のため、VPCピアリング接続やルーティングテーブルへのルートの設定をしました。

Pyroscope Agent の起動

Profiling したいサーバーは Go 製なので、同様に Go の Pyroscope Agent を利用しました。

Pyroscope Server 側のリソース消費の最適化のため、SampleRate を調整しました。

また、アプリケーションサーバー側の基盤が ECS なのもあり、バージョン単位でより詳細に分析できるように、Tags にタスク定義のバージョンを設定しました。

// Init initialize
func Init() {
    address := conf.PyroscopeAddress()
    if address == "" {
        return
    }

    pyroscopeConfig := pyroscope.Config{
        ApplicationName: "tomonite-server",
    ServerAddress: address,
    SampleRate:    conf.PyroscopeSampleRate(),
    
        Tags: map[string]string{
            "taskArn":               ecs.TaskARN(),
            "taskDefinitionVersion": ecs.TaskDefinitionVersion(),
        },
    }

    if _, err := pyroscope.Start(pyroscopeConfig); err != nil {
        log.Alert(fmt.Errorf("failed to start profiling for pyroscope. err: %w", err))
    }
}

Profiling の結果

時間の経過とともに Go サーバー全体に対してメモリの使用率が著しく上昇している処理を特定することができ、その処理は、grpc-go というモジュールであることが分かりました。

  • ある時点A

pyroscope before view
grpc-go のメモリ使用率はサーバー全体の15%ほど

  • ある時点Aから12時間後

grpc-go のメモリ使用率はサーバー全体の30%ほど

さらなる調査・改修対応

grpc-go は Go サーバー側で明示的に利用されている実装箇所を発見できなかったため、メインのモジュールとの依存関係を調査しました。

すると、Cloud Firestore データベースの読み取りと書き込みのためのクライアントを提供する cloud.google.com/go/firestore が依存していたことが判明しました。

$ go mod why -m google.golang.org/grpc

github.com/everytv/tomonite-server/db
cloud.google.com/go/firestore
google.golang.org/grpc

実際に firestore 周りのコネクションが保持されてそのままになっている処理があったので、コネクションを解放するため、(*firestore.Client).Close() を追記してリリースしました。

pr connection release
firestore のコネクションを解放する修正

その結果、Go サーバー全体に対するメモリの使用率の増加が顕著に緩やかになり、無事にメモリリークの問題を解決することができました。

memory usage after
ECS メモリ使用率 改善後

おわりに

今回は Pyroscope と Continuous Profiling 、Pyroscopeを活用したトラブルシューティングの事例について紹介しました。

Pyroscope を利用するメリットとして、WEB UIで直感的に操作できたり、ビュー機能である Comparison ViewDiff View でより効率的な分析が可能なところだと考えてます。

Continuous Profilingをしておくことで、問題発生後に Profiling 結果を得て、即座に調査が可能になります。今後も Pyroscope をうまく活用してサービスの安定稼働を実現していきます。

また、Grafana Labs による買収・統合に伴い、さらなる機能開発が見込まれるため、今後の開発の動向を注視しつつ最新バージョンへのアップグレードも検討できればと考えています。