Keyboard shortcuts

Press or to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

[インデックス 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(操作あたりのナノ秒)が大幅に減少していることがわかります。これは、処理速度が向上したことを意味します。

コミットメッセージには、以下のベンチマーク結果が含まれています。

benchmarkold ns/opnew ns/opdelta
BenchmarkClientServerParallel155909154454-0.93%
BenchmarkClientServerParallel-28601282986-3.52%
BenchmarkClientServerParallel-47021155168-21.43%
BenchmarkClientServerParallel-88075547862-40.73%
BenchmarkClientServerParallel-127775351478-33.79%
BenchmarkClientServerParallel-167792050278-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点に集約されます。

  1. ミューテックスの粒度を細かくする (Fine-grained Locking):

    • 変更前は、http.Transport構造体内にlk sync.Mutexという単一のミューテックスが存在し、アイドル接続のマップ(idleConn)や代替プロトコルのマップ(altProto)など、複数の異なるフィールドへのアクセスを保護していました。
    • 変更後は、この単一のミューテックスを廃止し、idleConnを保護するためのidleLk sync.Mutexと、altProtoを保護するためのaltLk sync.RWMutexの2つの専用ミューテックスを導入しました。これにより、異なるデータ構造へのアクセスが互いにブロックし合う可能性が低減されます。
  2. リード・ライトミューテックスの導入:

    • altProtoマップは、読み取り(RoundTripメソッドでのプロトコル解決)が頻繁に行われる一方で、書き込み(RegisterProtocolメソッドでのプロトコル登録)は比較的稀です。
    • この特性を活かし、altLksync.RWMutexを採用しました。RoundTripではRLock()を使用することで、複数のゴルーチンが同時にプロトコルマップを読み取れるようになり、並列性が向上しました。RegisterProtocolではLock()を使用し、書き込み時の排他性を保証します。
  3. ロック保持時間の最小化:

    • CloseIdleConnectionsメソッドでは、以前はt.lk.Lock()を呼び出し、アイドル接続マップのクリアと各接続のクローズ処理全体をロックしていました。
    • 変更後は、t.idleLk.Lock()でマップへのアクセスを保護し、マップの参照をローカル変数にコピーした後、すぐにt.idleLk.Unlock()を呼び出します。その後、ロックを解放した状態で各接続のクローズ処理を行います。これにより、ミューテックスが保持される時間が大幅に短縮され、他のゴルーチンがidleLkを待機する時間が減少します。
    • putIdleConngetIdleConnメソッドでも同様に、idleConnマップへのアクセスが必要な最小限の範囲でのみidleLkをロックするように変更されています。
    • persistConn.isBroken()メソッドでも、pc.brokenの値をローカル変数にコピーしてからロックを解放するように変更され、ロック保持時間が短縮されています。

これらの変更により、特に並列度の高い環境でのhttp.Transportのパフォーマンスが大幅に向上しました。

コアとなるコードの変更箇所

このコミットでは、主に以下の2つのファイルが変更されています。

  1. src/pkg/net/http/export_test.go
  2. 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構造体におけるミューテックスの再設計と、それに伴うロック戦略の最適化です。

  1. Transport構造体の変更:

    • lk sync.Mutex (汎用ミューテックス) の削除: 以前は、Transport内の様々な共有データ(アイドル接続、代替プロトコルなど)をこの単一のミューテックスで保護していました。これが競合の主な原因でした。
    • idleLk sync.Mutex の導入: これは、アイドル接続プールidleConnマップへのアクセスを排他的に保護するための専用ミューテックスです。これにより、アイドル接続関連の操作が他のTransportの操作と独立してロックされるようになります。
    • altLk sync.RWMutex の導入: これは、代替プロトコルマップaltProtoへのアクセスを保護するためのリード・ライトミューテックスです。altProtoは読み取りが頻繁で書き込みが稀であるため、RWMutexを使用することで、複数の読み取り操作が同時に進行できるようになり、並列性が大幅に向上します。
  2. RoundTripメソッドの最適化:

    • RoundTripは、HTTPリクエストを送信する際に呼び出される最も頻繁なメソッドの一つです。このメソッド内で代替プロトコルを解決する際にaltProtoマップを読み取ります。
    • 変更前はt.lk.Lock()を使用していましたが、変更後はt.altLk.RLock()を使用します。これにより、複数のRoundTrip呼び出しが同時にaltProtoを読み取ることが可能になり、並列リクエスト処理のボトルネックが解消されます。
  3. RegisterProtocolメソッドの最適化:

    • RegisterProtocolは、新しいプロトコルを登録する際にaltProtoマップを書き換えます。
    • 変更前はt.lk.Lock()を使用していましたが、変更後はt.altLk.Lock()を使用します。RWMutexの書き込みロックは排他的であるため、書き込み操作の一貫性が保証されます。
  4. CloseIdleConnectionsメソッドの最適化:

    • このメソッドは、アイドル接続をすべてクローズする際に呼び出されます。
    • 変更前は、t.lkをロックしたまま、idleConnマップのクリアと、マップ内のすべての接続をループしてクローズする処理を行っていました。接続のクローズ処理は時間がかかる可能性があり、その間ずっとt.lkが保持されるため、他の操作がブロックされていました。
    • 変更後は、t.idleLkをロックしてidleConnマップの参照をローカル変数mにコピーし、すぐにt.idleLk.Unlock()を呼び出します。その後、ロックを解放した状態でmをループして接続をクローズします。これにより、idleLkが保持される時間が最小限に抑えられ、他のアイドル接続関連の操作がブロックされる時間が短縮されます。
  5. putIdleConnおよびgetIdleConnメソッドの最適化:

    • これらのメソッドは、アイドル接続プールへの接続の追加と取得を行います。
    • 変更前は、t.lkをロックしていましたが、変更後はt.idleLkをロックするように変更され、かつロックのスコープがidleConnマップへの直接的なアクセス部分に限定されました。これにより、ロックが保持される時間が短縮され、競合が減少します。
  6. persistConn.isBroken()メソッドの最適化:

    • このメソッドは、接続が壊れているかどうかをチェックします。
    • 変更前は、pc.lkをロックしたままpc.brokenの値を返していました。
    • 変更後は、pc.lkをロックしてpc.brokenの値をローカル変数bにコピーし、すぐにpc.lk.Unlock()を呼び出してからbを返します。これにより、ロックが保持される時間がさらに短縮されます。

これらの変更は、Goの並行処理におけるベストプラクティスである「ロックの粒度を細かくする」「リード・ライトミューテックスを適切に利用する」「ロック保持時間を最小限にする」を実践したものであり、高並列環境でのnet/httpパッケージのパフォーマンスとスケーラビリティを大幅に向上させました。

関連リンク

参考にした情報源リンク