PR

Go言語(Golang)1.27で追加されたpprofの「goroutineleak」とは?ゴルーチンリークを調べる方法を解説

2. 基礎

こんにちは。Tomoyuki(@tomoyuki65)です。

Go言語(Golang)のバージョン1.27では、ゴルーチンのリークを調べるためのプロファイル「goroutineleak」が追加されました。

これにより、処理が終了しているにもかかわらず、終了できずに取り残されているゴルーチンがないかを簡単に調べられるようになっています。

この記事では、そんなpprofの「goroutineleak」についてご紹介します。

 

Go言語(Golang)1.27で追加されたpprofの「goroutineleak」とは?ゴルーチンリークを調べる方法を解説

Go言語の「pprof」とは?

Go言語の「pprof」というのは、GoにおけるプログラムのCPU使用率やメモリ使用量、そしてゴルーチン数などを調査するためのプロファイリング機能です。

アプリケーションのパフォーマンス問題が発生した際に、どこで負荷が発生しているのかを調べるために利用できます。

 

「goroutineleak」とは?

goroutineleak(ゴルーチンリーク)というのは、本来終了するはずのゴルーチンが終了せず、残り続けてしまう状態のことを指します。

ゴルーチンが想定通りに終了せずに残り続けると、メモリなどのリソースを消費したり、最終的にはアプリケーションのパフォーマンス低下や障害にも繋がる可能性があります。

そのため、ゴルーチン数が時間の経過とともに増え続けてしまっている場合については、ゴルーチンリークが発生していないかを調査する必要性がでてきます。

 

ゴルーチンリークを調べる方法

そんなゴルーチンリークを調べたい場合、Go1.27からpprofにプロファイル「goroutineleak」が追加されたため、これを使うと調査が可能です。

例えば以下のようなサンプルコードを作ると、pprofのプロファイル「goroutineleak」を試せます。

まずは以下のコマンドを実行し、各種ファイルを作成します。

$ mkdir go-goroutineleak && cd go-goroutineleak
$ mkdir src && touch src/main.go
$ touch Dockerfile compose.yml

※Dockerを利用してローカル環境を作ります。

 

次に作成したファイルをそれぞれ以下のように記述します。

・「src/main.go」

package main

import (
    "net/http"

    // pprof用のエンドポイント登録
    _ "net/http/pprof"

    "github.com/labstack/echo/v5"
)

// ゴルーチンリークが発生する関数
func leak() {
    // チャネル作成
    ch := make(chan int)

    // ゴルーチンによるチャネルの受信処理
    go func() {
        <-ch
    }()

    // チャネルへの送信処理がないため、起動したゴルーチンが終了せず残る
}

func main() {

    // pprof用のサーバー起動
    go func() {
        if err := http.ListenAndServe(":6060", nil); err != nil {
            panic(err)
        }
    }()

    // echoのエンドポイント設定
    e := echo.New()

    e.GET("/", func(c *echo.Context) error {
        return c.JSON(http.StatusOK, map[string]string{
            "message": "Hello World !!",
        })
    })

    // ゴルーチンリークを発生させるAPI
    e.GET("/leak", func(c *echo.Context) error {
        // ゴルーチンリークを発生させる関数を呼び出す
        leak()

        return c.JSON(http.StatusOK, map[string]string{
            "message": "goroutine leak triggered !",
        })
    })

    // echoのサーバー起動
    if err := e.Start(":8080"); err != nil {
        panic(err)
    }
}

※通常のAPIについては、フレームワーク「echo」を利用しています。

 

・「Dockerfile」

FROM golang:1.27.1-alpine3.24

WORKDIR /go/src

COPY ./src .

# go.modがあれば依存関係をインストール
RUN if [ -f ./go.mod ]; then \
      go install; \
    fi

# 開発用のライブラリをインストール
RUN go install github.com/air-verse/air@v1.67.4

EXPOSE 8080

 

・「compose.yml」

services:
  go-app:
    container_name: go-app
    build:
      context: .
      dockerfile: ./Dockerfile
    command: air -c .air.toml
    volumes:
      - ./src:/go/src
    ports:
      - "8080:8080"
      # pprof用のポート設定
      - "6060:6060"
    tty: true
    stdin_open: true

 

次に以下のコマンドを実行し、Dockerコンテナのビルドをします。

$ docker compose build --no-cache

 

次に以下のコマンドを実行し、goの初期化をします。

$ docker compose run --rm go-app go mod init go-goroutineleak
$ docker compose run --rm go-app go mod tidy
$ docker compose run --rm go-app air init

 

次に以下のコマンドを実行し、Dockerコンテナの再ビルドおよび起動をします。

$ docker compose build --no-cache
$ docker compose up -d

 

次に以下のコマンドを実行し、Dockerコンテナのステータスを確認します。

$ docker compose ps

 

実行後、以下のようにDockerコンテナが起動していればOKです。

 

次にcurlコマンドで以下を実行し、ルートのエンドポイント「/」のAPIを試します。

$ curl -v http://localhost:8080

 

実行後、以下のように想定通りのレスポンス結果が返ってこればOKです。

 

次にブラウザで「http://localhost:6060/debug/pprof/」にアクセスし、以下のような画面が表示されればOKです。

Count Profileに「goroutineleak」がありますが、まだゴルーチンのリークは発生していないため、「0」件です。

 

次にcurlコマンドで以下を実行し、ゴルーチンリークを発生させるエンドポイント「/leak」のAPIを試します。

$ curl -v http://localhost:8080/leak

 

実行後、以下のように想定通りのレスポンス結果が返ってこればOKです。

 

次に先ほどブラウザで開いた画面にある「goroutineleak」をクリックします。

※キャッシュの影響により、ブラウザを更新しても、件数がすぐに反映されない可能性があります。

 

次にブラウザで「http://localhost:6060/debug/pprof/goroutineleak?debug=1」が開かれ、ゴルーチンリークが1件あることが確認できます。

 

次に以下のコマンドを実行し、profファイル「src/tmp/goroutineleak.prof」として取得します。

$ curl http://localhost:6060/debug/pprof/goroutineleak > src/tmp/goroutineleak.prof

 

次にgo toolコマンドを使って以下を実行し、取得したprofファイルを調査します。

$ docker compose exec go-app go tool pprof ./tmp/goroutineleak.prof

 

実行後、以下のようにpprofが起動します。

 

次にpprofで「top」コマンドを実行すると以下のように表示され、「runtime.gopark」の「flat」が「1」のため、runtime.goparkのところで1つのゴルーチンが待機状態になっているのがわかります。

※flat:その関数自身で消費・保持している量、cum:その関数+その関数から呼び出している処理まで含めた量、をそれぞれ指します。

 

次にpprofで「list leak」コマンドを実行すると以下のように表示され、リークしているゴルーチンの数(Total)と、そのゴルーチンがどのコード箇所で待機しているのかを確認できます。

※pprofを終了したい場合「exit」コマンドを実行して下さい。

 

このように、Go1.27で追加された「goroutineleak」プロファイルを利用することで、ゴルーチンのリーク調査が簡単にできるようになっています。

 

echoの方にprof用のエンドポイントを追加したい場合は?

例えばフレームワーク「echo」の方にprof用のエンドポイントを追加したい場合は、「”net/http/pprof”」をimportし、以下のようにエンドポイントを追加すると可能です。

・「src/main.go」

package main

import (
    "net/http"

    // pprof用のエンドポイント登録
    _ "net/http/pprof"

    "net/http/pprof"

    "github.com/labstack/echo/v5"
)

// ゴルーチンリークが発生する関数
func leak() {
    // チャネル作成
    ch := make(chan int)

    // ゴルーチンによるチャネルの受信処理
    go func() {
        <-ch
    }()

    // チャネルへの送信処理がないため、起動したゴルーチンが終了せず残る
}

func main() {

    // pprof用のサーバー起動
    go func() {
        if err := http.ListenAndServe(":6060", nil); err != nil {
            panic(err)
        }
    }()

    // echoのエンドポイント設定
    e := echo.New()

    e.GET("/", func(c *echo.Context) error {
        return c.JSON(http.StatusOK, map[string]string{
            "message": "Hello World !!",
        })
    })

    // ゴルーチンリークを発生させるAPI
    e.GET("/leak", func(c *echo.Context) error {
        // ゴルーチンリークを発生させる関数を呼び出す
        leak()

        return c.JSON(http.StatusOK, map[string]string{
            "message": "goroutine leak triggered !",
        })
    })

    // pprof用のエンドポイントをechoに登録
    e.GET("/debug/pprof/", echo.WrapHandler(http.HandlerFunc(pprof.Index)))
    e.GET("/debug/pprof/cmdline", echo.WrapHandler(http.HandlerFunc(pprof.Cmdline)))
    e.GET("/debug/pprof/profile", echo.WrapHandler(http.HandlerFunc(pprof.Profile)))
    e.GET("/debug/pprof/symbol", echo.WrapHandler(http.HandlerFunc(pprof.Symbol)))
    e.GET("/debug/pprof/trace", echo.WrapHandler(http.HandlerFunc(pprof.Trace)))
    e.GET("/debug/pprof/allocs", echo.WrapHandler(pprof.Handler("allocs")))
    e.GET("/debug/pprof/block", echo.WrapHandler(pprof.Handler("block")))
    e.GET("/debug/pprof/goroutine", echo.WrapHandler(pprof.Handler("goroutine")))
    e.GET("/debug/pprof/goroutineleak", echo.WrapHandler(pprof.Handler("goroutineleak")))
    e.GET("/debug/pprof/heap", echo.WrapHandler(pprof.Handler("heap")))
    e.GET("/debug/pprof/mutex", echo.WrapHandler(pprof.Handler("mutex")))
    e.GET("/debug/pprof/threadcreate", echo.WrapHandler(pprof.Handler("threadcreate")))

    // echoのサーバー起動
    if err := e.Start(":8080"); err != nil {
        panic(err)
    }
}

 

尚、ブラウザで「http://localhost:8080/debug/pprof/」にアクセスすると、以下のように表示されます。

 

prof利用時の注意点

上記で試したように、pprof用のエンドポイントを用意することで、ゴルーチンリークのような問題の調査が可能になります。

ただし、本番環境ではpprofのエンドポイントをそのまま一般公開するのは、セキュリティ上避ける必要があり、もし本番環境で利用するなら認証を付けたり、アクセス制限を設けたりするなど、何らかのアクセス制御が必要です。

また、例えばGoogle CloudのCloud Runのように、複数のインスタンスが動的に増減する環境では注意が必要です。

pprofで取得できるのは、あくまでアクセスしたインスタンスのプロファイルであり、ゴルーチンリークが発生しているインスタンスとは別のインスタンスにアクセスしてしまうと、リークを確認できないことがあります。

そのため、pprofを使う場面としては、ローカル環境での調査や、調査対象のインスタンスを特定できる環境などが考えられます。

 

スポンサーリンク

最後に

今回はGo1.27で追加されたpprofの「goroutineleak」についてご紹介しました。

pprofを使ってゴルーチンのリーク調査を簡単にできるようになったため、その点は非常に便利なのがわかります。

ただし、本番環境でpprofを使うのは何らかのアクセス制御が必要になったり、簡単ではない可能性があるため、その点は注意が必要です。

Go言語によるゴルーチンリークの調査方法を知りたい方は、ぜひ参考にしてみて下さい。

 

SubstackでITエンジニア向けニュースレター(メルマガ)を始めました!
こんにちは。Tomoyuki(@tomoyuki65)です。最近はどのSNSもアルゴリズム(どの投稿を誰に表示するかを決める仕組み)中心のタイムライン(投稿が流れてくる画面)になっていて、フォローしている人の投稿が見つけにくく、逆にフォロー...

 

この記事を書いた人
Tomoyuki

SE→ブロガーを経て、現在はSoftware Engineer(Web/Gopher)をしています!

Tomoyukiをフォローする
2. 基礎
スポンサーリンク
Tomoyukiをフォローする

コメント

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