新しいことにはウェルカム

技術 | 電子工作 | ガジェット | ゲーム のメモ書き

GCPを使って、できるだけ楽してGo言語のgoroutineのリークを監視する

ゴルーチン リーク

Go言語はめちゃめちゃ簡単にゴルーチン(スレッドのようなもの)を作れるのが魅力です。

あまりにも簡単なので、あまり深く考えずゴルーチンを生成していたのですが、ゴルーチンはメインフローとは別に切り離されて実行されるので、デッドロックなどでゴルーチンの処理が途中で止まっていても気づかず、「終了したと思っていたが、実は実行中で残っていました」ということがありました。

バッチ処理などの終わりのあるプログラムなら、プログラムの終了時に残っていたゴルーチンも強制終了されるのですが、サーバープログラムなど永続するプログラムの場合、途中で止まっているゴルーチンがあった場合、止まったまま未終了のゴルーチンがどんどん溜まっていってしまいます。

解決方法

じゃあどうすればいいかと言うと、ゴルーチンの起動数をチェックしながら開発して、ゴルーチンのリークが起こらないプログラムを書くようにします。

ローカルでゴルーチンのリークが起こっていないのを確認できていても、いざサーバーで長時間実行した時に、本当にリークが起こっていないかはちょっと心配です。

ですので、サーバーで起動した場合も、ゴルーチンの起動数をログなどに定期的に出力してチェックするようにしていたのですが、正直その作業が面倒くさいなぁと思っていました。

もっと楽したい

Kubernetesのアプリのコンテナから標準エラー出力があったら、外部通知するようにしたくて、GCPのCloud Monitoringを触っていました。

www.kwbtblog.com

Cloud Monitoringは主に、GCPの各種サービスの状態モニタリングを行うサービスなのですが、アプリの状態収集にも使えそうだなと、ドキュメントを見ていると、そのまま簡単に使えそうな機能があったので、それを使ってゴルーチンの起動数を監視することにしました。

方針

Cloud Monitoringに、Go言語のpprofの値を送るようにして、Cloud Monitoring上でゴルーチンの起動数をグラフ表示します。

プログラムからCloud Monitoringに値を送るには、OpenCensusというオープンソースのライブラリを使うのですが、色々お膳立てが必要で、ちょっとした値を送りたい場合でも、結構コードを書く必要があります。

しかし、OpenCensusのGo言語のライブラリには、pprofの値をよしなに取得して送る機能があり、それを使えばOpenCensusのことはよく知らなくても数行のコードでできてしまうので、今回はそれをそのまま使いました。

手順

権限設定

Cloud Monitoringにデータを送るには、「モニタリング管理者」権限が必要なので、アプリに付与しておきます。

サンプルプログラム

package main

import (
    "context"
    "log"
    "os"
    "time"

    "contrib.go.opencensus.io/exporter/stackdriver"
    "go.opencensus.io/metric/metricexport"
    "go.opencensus.io/plugin/runmetrics"
)

func main() {
    //////////////////////////////////////////////////
    // Cloud Monitoringにpprofを送る
    //////////////////////////////////////////////////
    os.Setenv("GOOGLE_APPLICATION_CREDENTIALS", "./service-account-key.json")

    // exporter(Cloud Monitoring)
    exporter, err := stackdriver.NewExporter(stackdriver.Options{
        // Cloud Montoringからの検索用文字列
        MetricPrefix: "test-app-go-pprof",
        // データ送信頻度(60秒以上)
        ReportingInterval: 120 * time.Second,
    })
    if err != nil {
        log.Fatal(err)
    }
    defer exporter.Flush()
    exporter.StartMetricsExporter()
    defer exporter.StopMetricsExporter()

    // metrics(pprof)
    err = runmetrics.Enable(runmetrics.RunMetricOptions{
        EnableCPU:    true,
        EnableMemory: true,
    })
    if err != nil {
        log.Fatal(err)
    }

    // metrics -> exporter
    metricexport.NewReader().ReadAndExport(exporter)

    //////////////////////////////////////////////////
    // ゴルーチン リーク テスト
    //////////////////////////////////////////////////
    ctx := context.Background()
    for {
        go func() {
            var a [1000 * 1000 * 10]byte // メモリも消費
            _ = a
            <-ctx.Done() // 停止
        }()
        time.Sleep(60 * time.Second)
    }
}

補足説明

  • OpenCensusはデータ部分(metrics)と送信部分(exporter)に分かれています。
  • exporterをCloud Monitoring(旧名stackdriver)にして、runmetricsでpprofからmetrictを生成するようにします。
  • exporter(Cloud Monitoring)がmetrics(pprof)を読み込んで出力するよう紐付けします。
  • Cloud Monitoringには色んな種類のデータが送られているので、識別しやすいように「MetricPrefix」を付けます。

Cloud Monitoring

プログラムを実行すると、Cloud Monitoringに

custom.googleapis.com/opencensus/test-app-go-pprof/process/cpu_goroutines

という名前で、ゴルーチン数が記録されるので、[Cloud Monitoring]-[Metrics Explorer]で見ることができます。

GCPを使って、できるだけ楽してGo言語のgoroutineのリークをチェックする

応用

他のpprofの値も送られるので、ダッシュボードでゴルーチン数とメモリの使用量をならべて表示するといったことも、プログラムの修正無しにできます。

GCPを使って、できるだけ楽してGo言語のgoroutineのリークをチェックする

ゴルーチンの数がしきい値を超えたら、Slackに通知などもできます。

GCPを使って、できるだけ楽してGo言語のgoroutineのリークをウォッチする

感想など

これで安心してゴルーチンのリークチェックを放置できます。

こういった監視のサービス・手法は様々なものが乱立しているのですが、互換性に乏しく、しかもアプリにガッツリ組み込む必要があるので、導入の心理的ハードルは高いです。

OpenCensusが、すごいメジャーかと言われるとまだまだな感じで、あまり深入りする気にならないんですよねぇ…。

本当はchannelのバッファの利用状況もチェックしたかったのですが、任意の値を送ろうとすると急に面倒になるので、ここはこれ以上深入りせず、ゴルーチン数(pprof)で満足することにしました。