[インデックス 13655] ファイルの概要
このコミットは、Go言語の標準ライブラリである net/http
パッケージにおける http.Transport
のミューテックス競合を削減することを目的としています。特に、並列クライアント/サーバーベンチマークにおいて顕著なパフォーマンス改善が見られます。
コミット
commit 20f6a8fdaf07db0cdb817bb4e8f7b8bf07797334
Author: Dmitriy Vyukov <dvyukov@google.com>
Date: Mon Aug 20 13:28:27 2012 +0400
net/http: reduce mutex contention
benchmark old ns/op new ns/op delta
BenchmarkClientServerParallel 155909 154454 -0.93%
BenchmarkClientServerParallel-2 86012 82986 -3.52%
BenchmarkClientServerParallel-4 70211 55168 -21.43%
BenchmarkClientServerParallel-8 80755 47862 -40.73%
BenchmarkClientServerParallel-12 77753 51478 -33.79%
BenchmarkClientServerParallel-16 77920 50278 -35.47%
The benchmark is https://golang.org/cl/6441134
The machine is 2 x 4 HT cores (16 HW threads total).
Fixes #3946.
Now contention moves to net.pollServer.AddFD().
R=bradfitz
CC=bradfitz, dave, dsymonds, gobot, golang-dev, remyoudompheng
https://golang.org/cl/6454142
GitHub上でのコミットページへのリンク
https://github.com/golang/go/commit/20f6a8fdaf07db0cdb817bb4e8f7b8bf07797334
元コミット内容
このコミットは、net/http
パッケージのTransport
構造体におけるミューテックス(排他制御)の競合を減らすことを目的としています。特に、並列処理が多い環境でのパフォーマンス改善に焦点を当てています。ベンチマーク結果が示されており、並列度が高まるにつれてns/op
(操作あたりのナノ秒)が大幅に減少していることがわかります。これは、処理速度が向上したことを意味します。
コミットメッセージには、以下のベンチマーク結果が含まれています。
benchmark | old ns/op | new ns/op | delta |
---|---|---|---|
BenchmarkClientServerParallel | 155909 | 154454 | -0.93% |
BenchmarkClientServerParallel-2 | 86012 | 82986 | -3.52% |
BenchmarkClientServerParallel-4 | 70211 | 55168 | -21.43% |
BenchmarkClientServerParallel-8 | 80755 | 47862 | -40.73% |
BenchmarkClientServerParallel-12 | 77753 | 51478 | -33.79% |
BenchmarkClientServerParallel-16 | 77920 | 50278 | -35.47% |
このベンチマークは、https://golang.org/cl/6441134
で参照されており、テスト環境は「2 x 4 HT cores (16 HW threads total)」と記載されています。
また、このコミットはIssue #3946
を修正するものであり、変更後には競合がnet.pollServer.AddFD()
に移動したことが示唆されています。
変更の背景
Goのnet/http
パッケージは、HTTPクライアントとサーバーの実装を提供します。http.Transport
は、HTTPリクエストの実際の送信、接続の管理(コネクションプーリング、キープアライブなど)を担当する重要なコンポーネントです。
並列処理が増加する高負荷な環境では、共有リソースへのアクセスを制御するために使用されるミューテックスがボトルネックとなることがあります。複数のゴルーチンが同時に同じミューテックスを獲得しようとすると、他のゴルーチンは待機状態に入り、全体のパフォーマンスが低下します。これを「ミューテックス競合(mutex contention)」と呼びます。
このコミットの背景には、http.Transport
内で使用されている単一のミューテックスが、アイドル接続の管理や代替プロトコルの登録など、複数の異なる操作を保護しており、これが並列処理のボトルネックとなっていたという問題がありました。特に、多数の並列リクエストが発生するシナリオでは、この単一ミューテックスが頻繁にロック/アンロックされることで、スループットが低下していました。
コミットメッセージに記載されているベンチマーク結果は、この問題が顕著であったことを裏付けています。並列度が高まるにつれて、旧バージョンではパフォーマンスの伸びが鈍化、あるいは低下していましたが、この変更によって大幅な改善が見られました。
前提知識の解説
1. ミューテックス (Mutex)
ミューテックスは、複数のゴルーチン(またはスレッド)が共有リソースに同時にアクセスするのを防ぐための同期プリミティブです。Go言語ではsync.Mutex
として提供されます。
Lock()
: ミューテックスをロックします。既にロックされている場合、呼び出し元のゴルーチンはロックが解放されるまでブロックされます。Unlock()
: ミューテックスをアンロックします。ロックを保持しているゴルーチンのみがアンロックできます。
ミューテックスは、共有データの一貫性を保つために不可欠ですが、競合が発生するとパフォーマンスのボトルネックになります。
2. リード・ライトミューテックス (RWMutex)
リード・ライトミューテックスは、sync.RWMutex
としてGo言語で提供される、より柔軟なミューテックスです。
RLock()
: 読み取りロックを獲得します。複数のゴルーチンが同時に読み取りロックを獲得できます。RUnlock()
: 読み取りロックを解放します。Lock()
: 書き込みロックを獲得します。書き込みロックが獲得されている間は、他の読み取りロックも書き込みロックも獲得できません。Unlock()
: 書き込みロックを解放します。
リード・ライトミューテックスは、読み取り操作が書き込み操作よりもはるかに頻繁に行われる場合に特に有効です。これにより、読み取り操作の並列性を高め、全体のパフォーマンスを向上させることができます。
3. コネクションプーリング (Connection Pooling)
HTTPクライアントがサーバーと通信する際、毎回新しいTCP接続を確立するのはコストがかかります。コネクションプーリングは、一度確立した接続を再利用することで、このオーバーヘッドを削減する技術です。http.Transport
は、アイドル状態の接続をプールし、後続のリクエストで再利用します。
4. net.pollServer.AddFD()
GoのネットワークI/Oは、内部的にnet.pollServer
というポーリングメカニズムを使用しています。AddFD()
は、ファイルディスクリプタ(ソケットなど)をポーリングシステムに登録する操作です。コミットメッセージで「Now contention moves to net.pollServer.AddFD().」とあるのは、このコミットによってhttp.Transport
内のミューテックス競合が解消された結果、次のボトルネックがネットワークポーリング層に移動したことを示唆しています。これは、パフォーマンス改善の一般的なパターンであり、あるボトルネックを解消すると、別のボトルネックが顕在化するというものです。
技術的詳細
このコミットの主要な技術的アプローチは、以下の3点に集約されます。
-
ミューテックスの粒度を細かくする (Fine-grained Locking):
- 変更前は、
http.Transport
構造体内にlk sync.Mutex
という単一のミューテックスが存在し、アイドル接続のマップ(idleConn
)や代替プロトコルのマップ(altProto
)など、複数の異なるフィールドへのアクセスを保護していました。 - 変更後は、この単一のミューテックスを廃止し、
idleConn
を保護するためのidleLk sync.Mutex
と、altProto
を保護するためのaltLk sync.RWMutex
の2つの専用ミューテックスを導入しました。これにより、異なるデータ構造へのアクセスが互いにブロックし合う可能性が低減されます。
- 変更前は、
-
リード・ライトミューテックスの導入:
altProto
マップは、読み取り(RoundTrip
メソッドでのプロトコル解決)が頻繁に行われる一方で、書き込み(RegisterProtocol
メソッドでのプロトコル登録)は比較的稀です。- この特性を活かし、
altLk
にsync.RWMutex
を採用しました。RoundTrip
ではRLock()
を使用することで、複数のゴルーチンが同時にプロトコルマップを読み取れるようになり、並列性が向上しました。RegisterProtocol
ではLock()
を使用し、書き込み時の排他性を保証します。
-
ロック保持時間の最小化:
CloseIdleConnections
メソッドでは、以前はt.lk.Lock()
を呼び出し、アイドル接続マップのクリアと各接続のクローズ処理全体をロックしていました。- 変更後は、
t.idleLk.Lock()
でマップへのアクセスを保護し、マップの参照をローカル変数にコピーした後、すぐにt.idleLk.Unlock()
を呼び出します。その後、ロックを解放した状態で各接続のクローズ処理を行います。これにより、ミューテックスが保持される時間が大幅に短縮され、他のゴルーチンがidleLk
を待機する時間が減少します。 putIdleConn
やgetIdleConn
メソッドでも同様に、idleConn
マップへのアクセスが必要な最小限の範囲でのみidleLk
をロックするように変更されています。persistConn.isBroken()
メソッドでも、pc.broken
の値をローカル変数にコピーしてからロックを解放するように変更され、ロック保持時間が短縮されています。
これらの変更により、特に並列度の高い環境でのhttp.Transport
のパフォーマンスが大幅に向上しました。
コアとなるコードの変更箇所
このコミットでは、主に以下の2つのファイルが変更されています。
src/pkg/net/http/export_test.go
src/pkg/net/http/transport.go
src/pkg/net/http/export_test.go
の変更
テスト用のエクスポートファイルであり、Transport
構造体の内部状態にアクセスするためのヘルパー関数が含まれています。
IdleConnKeysForTesting()
とIdleConnCountForTesting()
関数内で、t.lk.Lock()
/t.lk.Unlock()
がt.idleLk.Lock()
/t.idleLk.Unlock()
に変更されています。これは、アイドル接続の管理に特化した新しいミューテックスを使用するようにテストコードも更新されたことを示します。
--- a/src/pkg/net/http/export_test.go
+++ b/src/pkg/net/http/export_test.go
@@ -11,8 +11,8 @@ import "time"
func (t *Transport) IdleConnKeysForTesting() (keys []string) {
keys = make([]string, 0)
- t.lk.Lock()
- defer t.lk.Unlock()
+ t.idleLk.Lock()
+ defer t.idleLk.Unlock()
if t.idleConn == nil {
return
}
@@ -23,8 +23,8 @@ func (t *Transport) IdleConnKeysForTesting() (keys []string) {
}
func (t *Transport) IdleConnCountForTesting(cacheKey string) int {
- t.lk.Lock()
- defer t.lk.Unlock()
+ t.idleLk.Lock()
+ defer t.idleLk.Unlock()
if t.idleConn == nil {
return 0
}
src/pkg/net/http/transport.go
の変更
http.Transport
構造体の定義と、その主要なメソッドが変更されています。
Transport
構造体の変更:lk sync.Mutex
が削除されました。idleLk sync.Mutex
が追加されました。altLk sync.RWMutex
が追加されました。
--- a/src/pkg/net/http/transport.go
+++ b/src/pkg/net/http/transport.go
@@ -42,8 +42,9 @@ const DefaultMaxIdleConnsPerHost = 2
// https, and http proxies (for either http or https with CONNECT).
// Transport can also cache connections for future re-use.
type Transport struct {
- lk sync.Mutex
+ idleLk sync.Mutex
idleConn map[string][]*persistConn
+ altLk sync.RWMutex
altProto map[string]RoundTripper // nil or map of URI scheme => RoundTripper
// TODO: tunable on global max cached connections
RoundTrip
メソッドの変更:- 代替プロトコル(
altProto
)の読み取り時に、t.lk.Lock()
/t.lk.Unlock()
がt.altLk.RLock()
/t.altLk.RUnlock()
に変更されました。これにより、読み取り操作の並列性が向上します。
- 代替プロトコル(
@@ -132,12 +133,12 @@ func (t *Transport) RoundTrip(req *Request) (resp *Response, err error) {
return nil, errors.New("http: nil Request.Header")
}
if req.URL.Scheme != "http" && req.URL.Scheme != "https" {
- t.lk.Lock()
+ t.altLk.RLock()
var rt RoundTripper
if t.altProto != nil {
rt = t.altProto[req.URL.Scheme]
}
- t.lk.Unlock()
+ t.altLk.RUnlock()
if rt == nil {
return nil, &badStringError{"unsupported protocol scheme", req.URL.Scheme}
}
RegisterProtocol
メソッドの変更:- 代替プロトコル(
altProto
)の書き込み時に、t.lk.Lock()
/t.lk.Unlock()
がt.altLk.Lock()
/t.altLk.Unlock()
に変更されました。
- 代替プロトコル(
@@ -171,8 +172,8 @@ func (t *Transport) RegisterProtocol(scheme string, rt RoundTripper) {
if scheme == "http" || scheme == "https" {
panic("protocol " + scheme + " already registered")
}
- t.lk.Lock()
- defer t.lk.Unlock()
+ t.altLk.Lock()
+ defer t.altLk.Unlock()
if t.altProto == nil {
t.altProto = make(map[string]RoundTripper)
}
CloseIdleConnections
メソッドの変更:- アイドル接続のクローズ処理において、
t.lk.Lock()
/t.lk.Unlock()
の代わりにt.idleLk.Lock()
を使用し、ロックを保持する時間を最小限に抑えるようにロジックが変更されました。具体的には、idleConn
マップの参照をローカル変数m
にコピーした後、すぐにロックを解放し、その後で接続のクローズ処理を行います。
- アイドル接続のクローズ処理において、
@@ -187,17 +188,18 @@ func (t *Transport) RegisterProtocol(scheme string, rt RoundTripper) {
// a "keep-alive" state. It does not interrupt any connections currently
// in use.
func (t *Transport) CloseIdleConnections() {
- t.lk.Lock()
- defer t.lk.Unlock()
- if t.idleConn == nil {
+ t.idleLk.Lock()
+ m := t.idleConn
+ t.idleConn = nil
+ t.idleLk.Unlock()
+ if m == nil {
return
}
- for _, conns := range t.idleConn {
+ for _, conns := range m {
for _, pconn := range conns {
pconn.close()
}
}
- t.idleConn = make(map[string][]*persistConn)
}
putIdleConn
メソッドの変更:t.lk.Lock()
/t.lk.Unlock()
が削除され、idleConn
マップへのアクセスが必要な部分でのみt.idleLk.Lock()
/t.idleLk.Unlock()
が使用されるようになりました。
@@ -243,8 +245,6 @@ func (cm *connectMethod) proxyAuth() string {
// If pconn is no longer needed or not in a good state, putIdleConn
// returns false.
func (t *Transport) putIdleConn(pconn *persistConn) bool {
- t.lk.Lock()
- defer t.lk.Unlock()
if t.DisableKeepAlives || t.MaxIdleConnsPerHost < 0 {
pconn.close()
return false
@@ -257,7 +257,12 @@ func (t *Transport) putIdleConn(pconn *persistConn) bool {
if max == 0 {
max = DefaultMaxIdleConnsPerHost
}
+ t.idleLk.Lock()
+ if t.idleConn == nil {
+ t.idleConn = make(map[string][]*persistConn)
+ }
if len(t.idleConn[key]) >= max {
+ t.idleLk.Unlock()
pconn.close()
return false
}
@@ -267,16 +272,17 @@ func (t *Transport) putIdleConn(pconn *persistConn) bool {
}
}
t.idleConn[key] = append(t.idleConn[key], pconn)
+ t.idleLk.Unlock()
return true
}
getIdleConn
メソッドの変更:t.lk.Lock()
/t.lk.Unlock()
が削除され、idleConn
マップへのアクセスが必要な部分でのみt.idleLk.Lock()
/t.idleLk.Unlock()
が使用されるようになりました。
@@ -267,16 +272,17 @@ func (t *Transport) putIdleConn(pconn *persistConn) bool {
return true
}
func (t *Transport) getIdleConn(cm *connectMethod) (pconn *persistConn) {
- t.lk.Lock()
- defer t.lk.Unlock()
+ key := cm.String()
+ t.idleLk.Lock()
+ defer t.idleLk.Unlock()
if t.idleConn == nil {
- t.idleConn = make(map[string][]*persistConn)
+ return nil
}
- key := cm.String()
for {
pconns, ok := t.idleConn[key]
if !ok {
persistConn.isBroken()
メソッドの変更:pc.broken
の値をローカル変数にコピーしてからロックを解放するように変更され、ロック保持時間が短縮されました。
@@ -513,8 +519,9 @@ type persistConn struct {
func (pc *persistConn) isBroken() bool {
pc.lk.Lock()
- defer pc.lk.Unlock()
- return pc.broken
+ b := pc.broken
+ pc.lk.Unlock()
+ return b
}
コアとなるコードの解説
このコミットの核心は、http.Transport
構造体におけるミューテックスの再設計と、それに伴うロック戦略の最適化です。
-
Transport
構造体の変更:lk sync.Mutex
(汎用ミューテックス) の削除: 以前は、Transport
内の様々な共有データ(アイドル接続、代替プロトコルなど)をこの単一のミューテックスで保護していました。これが競合の主な原因でした。idleLk sync.Mutex
の導入: これは、アイドル接続プールidleConn
マップへのアクセスを排他的に保護するための専用ミューテックスです。これにより、アイドル接続関連の操作が他のTransport
の操作と独立してロックされるようになります。altLk sync.RWMutex
の導入: これは、代替プロトコルマップaltProto
へのアクセスを保護するためのリード・ライトミューテックスです。altProto
は読み取りが頻繁で書き込みが稀であるため、RWMutex
を使用することで、複数の読み取り操作が同時に進行できるようになり、並列性が大幅に向上します。
-
RoundTrip
メソッドの最適化:RoundTrip
は、HTTPリクエストを送信する際に呼び出される最も頻繁なメソッドの一つです。このメソッド内で代替プロトコルを解決する際にaltProto
マップを読み取ります。- 変更前は
t.lk.Lock()
を使用していましたが、変更後はt.altLk.RLock()
を使用します。これにより、複数のRoundTrip
呼び出しが同時にaltProto
を読み取ることが可能になり、並列リクエスト処理のボトルネックが解消されます。
-
RegisterProtocol
メソッドの最適化:RegisterProtocol
は、新しいプロトコルを登録する際にaltProto
マップを書き換えます。- 変更前は
t.lk.Lock()
を使用していましたが、変更後はt.altLk.Lock()
を使用します。RWMutex
の書き込みロックは排他的であるため、書き込み操作の一貫性が保証されます。
-
CloseIdleConnections
メソッドの最適化:- このメソッドは、アイドル接続をすべてクローズする際に呼び出されます。
- 変更前は、
t.lk
をロックしたまま、idleConn
マップのクリアと、マップ内のすべての接続をループしてクローズする処理を行っていました。接続のクローズ処理は時間がかかる可能性があり、その間ずっとt.lk
が保持されるため、他の操作がブロックされていました。 - 変更後は、
t.idleLk
をロックしてidleConn
マップの参照をローカル変数m
にコピーし、すぐにt.idleLk.Unlock()
を呼び出します。その後、ロックを解放した状態でm
をループして接続をクローズします。これにより、idleLk
が保持される時間が最小限に抑えられ、他のアイドル接続関連の操作がブロックされる時間が短縮されます。
-
putIdleConn
およびgetIdleConn
メソッドの最適化:- これらのメソッドは、アイドル接続プールへの接続の追加と取得を行います。
- 変更前は、
t.lk
をロックしていましたが、変更後はt.idleLk
をロックするように変更され、かつロックのスコープがidleConn
マップへの直接的なアクセス部分に限定されました。これにより、ロックが保持される時間が短縮され、競合が減少します。
-
persistConn.isBroken()
メソッドの最適化:- このメソッドは、接続が壊れているかどうかをチェックします。
- 変更前は、
pc.lk
をロックしたままpc.broken
の値を返していました。 - 変更後は、
pc.lk
をロックしてpc.broken
の値をローカル変数b
にコピーし、すぐにpc.lk.Unlock()
を呼び出してからb
を返します。これにより、ロックが保持される時間がさらに短縮されます。
これらの変更は、Goの並行処理におけるベストプラクティスである「ロックの粒度を細かくする」「リード・ライトミューテックスを適切に利用する」「ロック保持時間を最小限にする」を実践したものであり、高並列環境でのnet/http
パッケージのパフォーマンスとスケーラビリティを大幅に向上させました。
関連リンク
- Go Issue #3946: https://github.com/golang/go/issues/3946 (このコミットが修正したIssue)
- Go Code Review 6441134: https://golang.org/cl/6441134 (ベンチマークのコードレビュー)
- Go Code Review 6454142: https://golang.org/cl/6454142 (このコミットのコードレビュー)
参考にした情報源リンク
- Go言語の
sync
パッケージドキュメント: https://pkg.go.dev/sync - Go言語の
net/http
パッケージドキュメント: https://pkg.go.dev/net/http - Go言語におけるミューテックスと並行処理に関する一般的な情報源 (例: Go Concurrency Patterns, Effective Goなど)
- A Tour of Go - Concurrency: https://go.dev/tour/concurrency/1
- Effective Go - Concurrency: https://go.dev/doc/effective_go#concurrency
- Go Concurrency Patterns: https://go.dev/blog/go-concurrency-patterns-timing-out-and-cancellation (より高度なパターン)
- ミューテックス競合とパフォーマンスに関する一般的な情報 (オペレーティングシステム、並行プログラミングの概念)