導入
Go言語は強力な並行処理モデルを提供していますが、複雑なアプリケーションでは「なぜか処理が遅い」「CPUは空いているのにスループットが上がらない」という事態に直面することがあります。その原因の多くは、Mutexの競合やチャネルの送受信待ちといった「ブロッキング」にあります。本記事では、標準ライブラリの runtime.SetBlockProfileRate を活用し、並行処理の隠れた待ち時間を可視化してボトルネックを特定する方法を解説します。
基礎知識
Goにおける「ブロックプロファイル」とは、ゴルーチンがリソースの獲得待ちなどで停止したイベントを記録する仕組みです。デフォルトではこのサンプリングは無効(レートが0)になっています。runtime.SetBlockProfileRate を呼び出すことで、指定したナノ秒間隔の待ち時間をサンプリングし、pprofツールを用いて可視化することが可能になります。これにより、コードのどこで排他制御の競合が激しいかを特定し、パフォーマンス改善の指針を得ることができます。
実装/解決策
ブロックプロファイルを取得するためには、主に以下の手順を踏みます。
1. プログラムの初期段階で runtime.SetBlockProfileRate を呼び出し、サンプリングを開始する。
2. 実行中にプロファイルデータを収集する。
3. 収集したデータを go tool pprof で解析する。
実務では、HTTPサーバーに pprof のエンドポイント(net/http/pprof)を組み込んでおき、稼働中のアプリケーションから動的にプロファイルを取得する手法が一般的です。
サンプルプログラム
以下のコードは、意図的にMutexの競合を発生させ、それをブロックプロファイルで特定するための準備を行う例です。
package main
import (
“log”
“net/http”
_ “net/http/pprof” // pprofのエンドポイントを登録
“runtime”
“sync”
“time”
)
func main() {
// ブロックプロファイルのレートを設定(1ミリ秒以上のブロックをサンプリング)
// 数値を小さくするほど精度は上がりますが、オーバーヘッドも増える点に注意してください
runtime.SetBlockProfileRate(1000000)
var mu sync.Mutex
// 競合を発生させるためのダミーゴルーチン
for i := 0; i < 10; i++ {
go func() {
for {
mu.Lock()
time.Sleep(10 time.Millisecond) // クリティカルセクションで待機
mu.Unlock()
}
}()
}
log.Println("サーバー起動: http://localhost:8080/debug/pprof/")
// プロファイル取得用サーバーの起動
log.Fatal(http.ListenAndServe(":8080", nil))
}
このコードを実行後、別のターミナルから以下のコマンドを実行することで解析が可能です。
go tool pprof http://localhost:8080/debug/pprof/block
応用・注意点
実務で runtime.SetBlockProfileRate を使用する際の重要な注意点は、「パフォーマンスへの影響」です。レートを過度に小さく設定すると、頻繁なサンプリングによってアプリケーション自体にオーバーヘッドが生じます。本番環境で運用する場合は、必要な時だけ有効にするか、サンプリング間隔を適切に調整してください。
また、ブロックプロファイルはあくまで「待たされた場所」を示します。Mutexの競合が激しい場合、単にコードを修正するだけでなく、チャネルへの書き換えや、データの局所性を高める設計(ロックの粒度を細かくする、あるいはロックフリーなデータ構造への置き換え)を検討することが、根本的な解決につながります。

コメント