PR

Go言語(Golang)のAPIでベンチマークからパフォーマンス計測する方法まとめ

応用

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

これまでにGo言語(Golang)によるAPIの作り方については色々とご紹介してきましたが、作ったAPIのパフォーマンスを計測したいこともでてくると思います。

この記事では、そんなGo言語のAPIにおいてベンチマークからパフォーマンス計測する方法についてまとめます。

 

Go言語(Golang)のAPIでベンチマークからパフォーマンス計測する方法まとめ

今回は以前に書いた記事「Go言語(Golang)のGinでDDD(ドメイン駆動設計)構成のバックエンドAPIを開発する方法まとめ」で作成したAPIを用いて、ベンチマークを取ってパフォーマンスを計測する方法をご紹介します。

まずはトレースファイルの確認時にブラウザからグラフ(ノードと矢印)を確認できるようにするためのライブラリ「graphviz」をDockerコンテナにインストールしておきたいので、開発用のDockerfile(docker/local/go/Dockerfile)を以下のように修正します。

FROM golang:1.24-alpine3.21

WORKDIR /go/src

# インストール可能なパッケージ一覧の更新
RUN apk update && \
    apk upgrade && \
    # パッケージのインストール(--no-cacheでキャッシュ削除)
    apk add --no-cache \
            graphviz

COPY ./src .

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

# 開発用のライブラリをインストール
RUN go install github.com/air-verse/air@v1.62.0
RUN go install honnef.co/go/tools/cmd/staticcheck@latest
RUN go install go.uber.org/mock/mockgen@latest
RUN go install github.com/swaggo/swag/cmd/swag@latest

EXPOSE 8080

※Goのバージョンは1.24を使うため、airのバージョンも1.62.0に固定しています。

 

次に出力したトレースファイルをブラウザで確認できるようにするため、compose.ymlにポート番号「8081」を追加しておきます。

services:
  api:
    container_name: go-gin-d-api
    build:
      context: .
      dockerfile: ./docker/local/go/Dockerfile
    command: air -c .air.toml
    volumes:
      - ./src:/go/src
    ports:
      - "8080:8080"
      - "8081:8081"
    tty: true
    stdin_open: true

 

次にベンチマークで出力したファイルをGitHubで管理しないようにするため、.gitignoreに以下を追加しておきます。
・・・

*.test
*.prof
*.out
*.cover

・・・

 

次に以下のコマンドを実行し、インテグレーションテスト(統合テスト)用のファイルを追加します。

touch src/internal/presentation/handler/user/user_handler_integration_test.go

 

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

・「src/internal/presentation/handler/user/user_handler_integration_test.go」

//go:build integration

package user

import (
    "bytes"
    "context"
    "encoding/json"
    "fmt"
    "io"
    "net/http"
    "net/http/httptest"
    "testing"

    usecase_user "go-gin-domain/internal/application/usecase/user"
    "go-gin-domain/internal/infrastructure/database"
    "go-gin-domain/internal/infrastructure/logger"
    persistence_user "go-gin-domain/internal/infrastructure/persistence/user"
    "go-gin-domain/internal/presentation/middleware"

    "github.com/gin-gonic/gin"
    "github.com/stretchr/testify/assert"
)

// テスト用Ginの初期化処理
func initTestGin() *gin.Engine {
    // Ginのテストモードに設定
    gin.SetMode(gin.TestMode)

    // ハンドラーのインスタンス化
    ctx := context.Background()
    logger := logger.NewSlogLogger()
    cfg := database.DummyConfig{
        Dummy: "dummy",
    }
    db_dummy, err := database.NewDummyConnection(cfg, logger)
    if err != nil {
        msg := fmt.Sprintf("エラー: %s", err.Error())
        logger.Error(ctx, msg)
    }
    userRepo := persistence_user.NewUserRepository(logger)
    userUsecase := usecase_user.NewUserUsecase(db_dummy, userRepo, logger)
    h := NewUserHandler(userUsecase)

    // ルーターの初期化
    r := gin.New()

    // ミドルウェアの設定
    m := middleware.NewMiddleware()
    r.Use(m.Request())
    r.Use(m.CustomLogger())
    r.Use(gin.Recovery())

    // ルーティング設定
    apiV1 := r.Group("/api/v1")
    apiV1.POST("/user", h.Create)
    apiV1.GET("/users", m.Auth(), h.FindAll)
    apiV1.GET("/user/:uid", m.Auth(), h.FindByUID)
    apiV1.PUT("/user/:uid", m.Auth(), h.Update)
    apiV1.DELETE("/user/:uid", m.Auth(), h.Delete)

    return r
}

func TestUserHandler_Create_Integration(t *testing.T) {
    // ルーター設定
    r := initTestGin()

    t.Run("Create_Integrationが正常終了すること", func(t *testing.T) {
        // リクエスト設定
        path := "/api/v1/user"
        reqBody := CreateUserRequestBody{
            LastName: "田中",
            FirstName: "太郎",
            Email: "t.tanaka@example.com",
        }
        jsonReqBody, err := json.Marshal(reqBody)
        if err != nil {
            t.Fatal(err)
        }
        req := httptest.NewRequest(http.MethodPost, path, bytes.NewBuffer(jsonReqBody))

        // テストの実行
        w := httptest.NewRecorder()
        r.ServeHTTP(w, req)

        // 検証
        assert.Equal(t, http.StatusCreated, w.Code)

        var data map[string]interface{}
        err = json.Unmarshal(w.Body.Bytes(), &data)
        assert.NoError(t, err)

        assert.NotContains(t, data, "id")
        assert.NotNil(t, data["uid"])
        assert.Equal(t, reqBody.LastName, data["last_name"])
        assert.Equal(t, reqBody.FirstName, data["first_name"])
        assert.Equal(t, reqBody.Email, data["email"])
        assert.NotNil(t, data["created_at"])
        assert.NotNil(t, data["updated_at"])
        assert.Nil(t, data["deleted_at"])
    })
}

/******************************
* ベンチマーク関数を追加
******************************/
func BenchmarkUserHandler_Create_Integration(b *testing.B) {
    // ルーター設定
    r := initTestGin()

    // ログ出力を無効化する
    gin.DefaultWriter = io.Discard

    // リクエストパス設定
    path := "/api/v1/user"

    // タイマーリセット(セットアップの時間を計測から除外)
    b.ResetTimer()

    // --- ベンチマーク実行 (b.N回ループ) ---
    // b.N は go test が自動的に調整するループ回数
    for i := 0; i < b.N; i++ {
        // ループごとに新しいリクエストとレスポンスレコーダーを作成
        // これらは1回のHTTPリクエストに相当するため、ループ内で都度生成する
        lastName := fmt.Sprintf("田中%d", i)
        firstName := fmt.Sprintf("太郎%d", i)
        email := fmt.Sprintf("t.tanaka%d@example.com", i)
        reqBody := CreateUserRequestBody{
            LastName: lastName,
            FirstName: firstName,
            Email: email,
        }
        jsonReqBody, err := json.Marshal(reqBody)
        if err != nil {
            b.Fatal(err)
        }
        w := httptest.NewRecorder()
        req := httptest.NewRequest(http.MethodPost, path, bytes.NewBuffer(jsonReqBody))

        // リクエスト実行
        r.ServeHTTP(w, req)
    }
}

※インテグレーションテストの部分は一部のみ記述しています。今回使うのはベンチマーク関数です。

 

次に以下のコマンドを実行し、ベンチマークを実行してファイルを出力します。

docker compose exec -w /go/src/internal/presentation/handler/user api \
go test -run=^$ -tags=integration -bench=BenchmarkUserHandler_Create -cpuprofile cpu_create.prof -memprofile mem_create.prof -blockprofile block_create.prof -trace trace_create.out -benchmem

※オプション「-w」で対象のディレクトリを指定。オプション「-tags」でインテグレーション用のファイルのみ実行するようにし、オプション「-bench」で対象のベンチマーク関数のみ実行するようにしています。出力するファイルも対象のAPIごとに出力した方がいいのでファイル名を指定しています。

 

実行後、以下のようにベンチマーク結果が出力されるので、必要に応じて記録を残すようにします。

 

ベンチマーク結果を表にまとめると以下のようになります。

項目 説明
ベンチマーク名 BenchmarkUserHandler_Create_Integration-11 ベンチマーク対象の関数名。末尾の「-11」は使用された論理CPUの数(この場合11スレッド)
実行回数 192794 ベンチマーク関数が実行された回数
実行時間(1回あたり) 6479 ns/op 1回の処理にかかった平均時間(ナノ秒)
メモリ使用量(1回あたり) 9245 B/op 1回の実行で確保されたメモリサイズ(バイト)
メモリ割り当て回数(1回あたり) 64 allocs/op 1回の処理で発生したメモリ確保(アロケーション)回数

 

この結果が基準値になるため、必要に応じてファイル「src/internal/presentation/handler/user/BENCHMARK.md」にまとめておいて、GitHubで管理しておくのも有効です。

# User Handler Performance Benchmark
ユーザーハンドラーのベンチマーク結果は以下の通りです。
将来的にコードを変更した際はベンチマークを再実行し、このベースラインから大きく悪化していないことを確認してください。

## Integration Test
インテグレーションテストのベンチマーク結果

### `BenchmarkUserHandler_Create_Integration`

* **測定日:** 2025-09-12
* **Go Version:** 1.24.0
* **備考:** ログ出力を無効化した状態で計測

| メトリクス | 結果 | 備考 |
| :----------------------------- | :------ | :------------------------------------- |
| **時間 (ns/op)** | `5747` | 1リクエストあたりの平均処理時間 (ナノ秒) |
| **メモリ確保量 (B/op)** | `8787` | 1リクエストあたりのメモリ確保量 (バイト) |
| **メモリアロケーション (allocs/op)** | `58` | 1リクエストあたりのメモリ確保回数 |

---

*(ここに他のベンチマーク結果を追加していく...)*

 

VSCodeでプレビューを確認すると以下のように表示されます。

 

CPU使用状況の確認

次に出力されたファイル「src/internal/presentation/handler/user/cpu_create.prof」からCPU使用状況について確認します。

以下のコマンドを実行し、コマンド「go tool pprof」を使ってファイルを開きます。

docker compose exec api go tool pprof /go/src/internal/presentation/handler/user/cpu_create.prof

※Dockerコンテナ内のワークディレクトリは「/go」です。

 

コマンド実行後、pprofでファイルが開かれるので、コマンド「top」を実行し、最もリソースを使用している関数(ノード)を表示させます。

 

結果の見方について、全体としては以下の通りです。

・1490msがCPUプロファイルで観測された総実行時間(≒スレッドの合計CPU時間)
・上位10関数で470ms(全体の約31.5%)を消費している
・289個の関数が観測されたが、ここには上位10だけ表示されている

 

各行の見方については以下の通りです。
列名 意味
flat その関数自身が消費したCPU時間(子関数は含まない)
flat% flatの全体に対する割合
sum% この行までのflat%の累積
cum この関数とその子関数を含めた累積のCPU時間
cum% cumの全体に対する割合
関数名 リソースを消費している関数の名前

 

そしてまず確認すべき点としては、この中に自分が作成した処理に関する関数などが含まれていないことをチェックしましょう。

もし自分が作成した関数などが含まれていたらそこがボトルネックになっている可能性があるため、コマンド「list」で関数名を指定して詳細を確認して下さい。

 

メモリ使用量の確認

次に出力されたファイル「src/internal/presentation/handler/user/mem_create.prof」からメモリ使用量について確認します。

以下のコマンドを実行し、コマンド「go tool pprof」を使ってファイルを開きます。

docker compose exec api go tool pprof /go/src/internal/presentation/handler/user/mem_create.prof

 

コマンド実行後、pprofでファイルが開かれるので、コマンド「top」を実行し、最もリソースを使用している関数(ノード)を表示させます。

 

結果の見方について、全体としては以下の通りです。

・表示している関数群(ノード)が合計で約1517.88MBのメモリを使用しており、全体メモリ使用量1857.25MBのうち約81.73%を占めている
・累積メモリ使用量が9.29MB以下の関数(ノード)96個はレポートから除外されている
・プロファイル全体では77個の関数(ノード)があり、その中でメモリ消費の多い上位10個だけを表示している

 

各行の見方については以下の通りです。
列名 意味
flat その関数(スタックの最上位)で直接使われているメモリ量
flat% 全体のメモリ消費に対する割合
sum% 上位からの累積割合
cum その関数およびそこから呼び出される関数全体の累積メモリ量
cum% 全体の累積割合
関数名 メモリ使用箇所を示す関数名

 

そしてメモリ使用量についてもまずは自分が作成した処理に関する関数などが含まれていないことをチェックしましょう。

また、「sql.(*Rows).Scan 」のようなDBドライバー関連の関数が含まれていた場合、DBを使用している部分でボトルネックが発生している可能性があります。

その場合は、ベンチマークで出力したブロックプロファイル「block_create.prof」から「I/O待ち」の時間をチェックしてみて下さい。

 

goroutine(ゴルーチン)のブロック時間の確認

次にDB関連でボトルネックがありそうだったり、goroutine(ゴルーチン)が同期待ち(チャネルの待機、Mutexのロック待ちなど)に費やした時間を測定したい場合は、以下のコマンドを実行し、コマンド「go tool pprof」を使ってファイルを開きます。

docker compose exec api go tool pprof /go/src/internal/presentation/handler/user/block_create.prof

 

コマンド実行後、pprofでファイルが開かれるので、コマンド「top」を実行し、最もリソースを使用している関数(ノード)を表示させます。

 

結果の見方について、全体としては以下の通りです。

・サンプリングされた合計待機時間(ここでは5.6秒)で、全体の100%を占める
・累積待機時間が28ms以下の6個の関数は省略されている
・23個の待機ノードのうち、上位10個を表示

 

各行の見方については以下の通りです。
列名 意味
flat その関数で直接発生した待機時間(ミリ秒)
flat% 合計待機時間に対する割合
sum% 上位からの累積割合
cum その関数とそこから呼び出される関数すべての累積待機時間
cum% 累積待機時間の割合
関数名 待機が発生した関数

 

もし上位に「net.Conn.Read 」や「syscall.Read」(ネットワーク越しのDBからデータが送られてくるのを待っている時間)、「sync.Mutex.Lock」(DB接続プールからコネクションを取得する際のロック待ち時間)、「database/sql」(DBドライバ関連の関数)などがあれば、DB関連の部分でボトルネックが発生していることになります。

その場合は、以下のような観点で調査を進めるようにして下さい。

・SQLクエリ自体の実行計画を確認し、インデックスが適切に貼られているかなどを調査する
・DBサーバーのリソース(CPU、メモリ、ディスクI/O)を確認する
・ネットワークのレイテンシを確認する
・コネクションプールの上限の設定値は適切か確認する
・トランザクションやsql.RowsをClose()し忘れてないか確認する
・想定以上に多くのゴルーチンが同時にDBアクセスを試みていないか確認する

 

全体的な実行トレースの確認

次にプログラムの実行を非常に詳細な時系列で可視化したい場合、以下のコマンドを実行してトレースファイルの確認を行います。

docker compose exec api go tool trace -http=:8081 /go/src/internal/presentation/handler/user/trace_create.out

※オプション「-http」でcompose.ymlに追加したポート番号「8081」に割り当てます。

 

コマンド実行後、ブラウザで「http://localhost:8081」を開くと、以下のような画面が表示されます。

 

次にリンクの「Goroutine analysis」をクリックし、全体像を把握します。

 

この表ではベンチマーク実行中に、どのような種類のgoroutine(ゴルーチン)が、どれくらいの数、どれくらいの時間実行されたかのサマリーが表示されます。

例えば「testing.(*B).Run」や「main」といったテストフレームワークに関するgoroutine以外に、自分が作成した処理に関するものが多数表示されていないことを確認して下さい。

 

また、goroutineがネットワークI/O(主にDBや外部APIとの通信)で待機(ブロック)していた時間の合計を関数ごとにグラフで表示されるのを確認したい場合は、元のページのリンク「Network blocking profile」をクリックして確認できます。

goroutineが同期待ち(Mutexのロック待ち、チャネルの送受信待ちなど)に費やした時間の合計を関数ごとにグラフで表示されるのを確認したい場合は、元のページのリンク「Synchronization blocking profile」をクリックして確認できます。

 

例えば「Synchronization blocking profile」では、以下のようなグラフが確認できます。

 

まずは以下の観点でチェックするといいです。

・Network blocking profileを確認し、ネットワーク待ちが長ければDBやAPIの応答が遅い。
・Synchronization blocking profileを確認し、ロック待ちが長ければコネクションプールの競合などを疑う。
・上記二つに問題がなくて遅い場合、問題はCPUでの計算にある可能性が高いため、cpu.prof の分析をする。

 

そのほか、元のページのリンク「View trace by proc」や「View trace by thread」からタイムラインビューを確認できます。

例えば「View trace by proc」では以下のように表示され、横軸が時間、縦軸がgoroutineやプロセッサ(P)を表す、非常に詳細なタイムラインが確認できます。

 

タイムラインでは、以下のようなポイントについて確認して下さい。

1. Goroutines行
・この行に大きな隙間(ギャップ)がたくさんあるか
・タイムラインをクリックしてRunnableの数値が高いまま(CPUが飽和)ではないか
2. Network / Syscalls行
・タイムラインを拡大し、個々のネットワークI/Oやシステムコールが発生したタイミングをチェック
3. 個々のgoroutine
・個々のgoroutine(横棒)をクリックすると、そのゴルーチンがどのような処理を行い、どこでブロックされたかの詳細なイベントを確認できる

 

本番環境を想定したリクエストのパフォーマンスを計測したい場合について

上記でご紹介したベンチマークからパフォーマンス計測する方法については、主にユニットレベルや関数レベルでの性能測定に適しており、APIの内部処理のパフォーマンスを計測に便利です。

ただし、本番環境を想定したリクエストのパフォーマンス計測には適していないため、本番環境を想定したパフォーマンス計測については、k6(WebアプリケーションやAPIの負荷テスト・パフォーマンステストを行うためのオープンソースツール)などを利用して計測する必要があります。

尚、k6については以下の記事を参考にしてみて下さい。

Dockerによる負荷テストツール(k6・Prometheus・Grafana)の使い方
こんにちは。Tomoyuki(@tomoyuki65)です。今回はGo言語からは少し離れますが、実務においては作成したAPIの負荷テストをしておくのは重要だったりすると思います。そんな負荷テストをするためのツールとして、「k6・Promet...

 

スポンサーリンク

最後に

今回はGo言語のAPIにおいてベンチマークからパフォーマンス計測する方法についてまとめました。

APIの新規作成時でパフォーマンスについても気にしておきたい場合や、k6などで対象のAPIのパフォーマンスが悪くて調査が必要な際は、ベンチマークを取ってパフォーマンス計測が可能です。

Go言語のAPIにおいてパフォーマンス計測したい方は、ぜひ参考にしてみて下さい。

 

この記事を書いた人
Tomoyuki

SE→ブロガーを経て、現在はWeb系エンジニアをしています!

Tomoyukiをフォローする
応用
スポンサーリンク
Tomoyukiをフォローする

コメント

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