[インデックス 13661] ファイルの概要
このコミットは、src/pkg/net/http/transport.go
ファイルに対して行われた変更です。具体的には、17行の追加と13行の削除が行われています。
コミット
commit 2bdc60f8e71aabafccb1c414a7732a265faac3dd
Author: Dave Cheney <dave@cheney.net>
Date: Tue Aug 21 11:18:16 2012 +1000
net/http: fix send on close channel error
Fixes #3793.
Tested using GOMAXPROCS=81 which was able to trigger a panic
in TestStressSurpriseServerCloses continually on a Core i5.
R=fullung, bradfitz
CC=golang-dev
https://golang.org/cl/6445069
GitHub上でのコミットページへのリンク
https://github.com/golang/go/commit/2bdc60f8e71aabafccb1c414a7732a265faac3dd
元コミット内容
net/http: fix send on close channel error
Fixes #3793.
Tested using GOMAXPROCS=81 which was able to trigger a panic
in TestStressSurpriseServerCloses continually on a Core i5.
変更の背景
このコミットは、Go言語の標準ライブラリである net/http
パッケージにおける「closed channelへの送信」エラー("send on close channel error")を修正することを目的としています。具体的には、GoのHTTPクライアントがサーバーとの永続的な接続(persistent connection)を管理する際に発生する可能性のあるパニック(panic)を解決します。
この問題は、GOMAXPROCS
環境変数を非常に大きな値(例: GOMAXPROCS=81
)に設定し、TestStressSurpriseServerCloses
というテストを実行した際に、Core i5プロセッサ上で継続的にパニックが発生するという形で顕在化しました。これは、並行処理の負荷が高い状況下で、接続のクローズ処理とリクエストの書き込み処理の間に競合状態(race condition)が存在したことを示唆しています。
このバグは、Go issue #3793 として報告されており、このコミットはその修正として提出されました。
前提知識の解説
このコミットの変更内容を理解するためには、以下のGo言語およびネットワークプログラミングに関する基本的な概念を理解しておく必要があります。
- Go言語の並行処理 (Goroutines and Channels):
- Goroutine: Go言語における軽量なスレッドのようなものです。非常に低コストで生成でき、並行処理を実現するための基本的な構成要素です。
- Channel: Goroutine間でデータを安全にやり取りするための通信メカニズムです。チャネルは、データの送信(
chan <- data
)と受信(data <- chan
)を同期的に行い、競合状態を防ぎます。チャネルがクローズされた後にデータを送信しようとすると、パニックが発生します。 select
ステートメント: 複数のチャネル操作を待機し、準備ができた最初の操作を実行するためのGoの構文です。これにより、非ブロッキングなチャネル操作やタイムアウト処理などを実現できます。
- HTTP/1.1 の永続的な接続 (Persistent Connections):
- HTTP/1.1では、複数のリクエスト/レスポンスを単一のTCP接続上で送受信できる永続的な接続("Keep-Alive"接続とも呼ばれる)がサポートされています。これにより、接続の確立と切断のオーバーヘッドが削減され、パフォーマンスが向上します。
net/http
パッケージのTransport
:- Goの
net/http
パッケージは、HTTPクライアントとサーバーの実装を提供します。クライアント側では、http.Transport
がHTTPリクエストの送信、レスポンスの受信、接続の管理(永続的な接続を含む)を担当します。 http.Transport
の内部では、persistConn
という構造体が個々の永続的なTCP接続を表現し、その接続上でのリクエスト/レスポンスの送受信を管理します。
- Goの
persistConn
のreadLoop
とwriteLoop
:persistConn
は、通常、2つの主要なGoroutineを起動して接続を管理します。readLoop
: サーバーからのHTTPレスポンスを読み取る役割を担います。writeLoop
: クライアントからのHTTPリクエストをサーバーに書き込む役割を担います。
- これらのGoroutineはチャネルを介して通信し、リクエストの送信やレスポンスの受信を調整します。
技術的詳細
問題の核心は、persistConn
の readLoop
と writeLoop
という2つのGoroutineが、pc.writech
というチャネルのクローズ処理をどのように扱っていたかにあります。
元のコードでは、readLoop
が接続の読み取り側が終了する際に、defer close(pc.writech)
を使用して pc.writech
をクローズしていました。pc.writech
は、writeLoop
がリクエストを書き込むために待機しているチャネルです。
しかし、readLoop
が pc.writech
をクローズした後も、writeLoop
がまだアクティブであり、そのクローズされたチャネルにデータを送信しようとすると、「send on closed channel」というパニックが発生する可能性がありました。これは、readLoop
が接続の読み取り側が終了したと判断しても、writeLoop
がまだ書き込みを試みる可能性があるためです。特に、GOMAXPROCS
が大きい環境では、Goroutineのスケジューリングがより並行になり、この競合状態が発生しやすくなります。
このパニックは、HTTPクライアントがサーバーとの接続を予期せず切断された場合や、サーバーが応答を返す前にクライアントがリクエストの送信を完了した場合など、特定のタイミングで発生する可能性がありました。
コアとなるコードの変更箇所
--- a/src/pkg/net/http/transport.go
+++ b/src/pkg/net/http/transport.go
@@ -538,7 +538,6 @@ func remoteSideClosed(err error) bool {
func (pc *persistConn) readLoop() {
defer close(pc.closech)
- defer close(pc.writech)
alive := true
var lastbody io.ReadCloser // last response body, if any, read on this connection
@@ -640,19 +639,24 @@ func (pc *persistConn) readLoop() {
}
func (pc *persistConn) writeLoop() {
- for wr := range pc.writech {
- if pc.isBroken() {
- wr.ch <- errors.New("http: can't write HTTP request on broken connection")
- continue
- }
- err := wr.req.Request.write(pc.bw, pc.isProxy, wr.req.extra)
- if err == nil {
- err = pc.bw.Flush()
- }
- if err != nil {
- pc.markBroken()
+ for {
+ select {
+ case wr := <-pc.writech:
+ if pc.isBroken() {
+ wr.ch <- errors.New("http: can't write HTTP request on broken connection")
+ continue
+ }
+ err := wr.req.Request.write(pc.bw, pc.isProxy, wr.req.extra)
+ if err == nil {
+ err = pc.bw.Flush()
+ }
+ if err != nil {
+ pc.markBroken()
+ }
+ wr.ch <- err
+ case <-pc.closech:
+ return
}
- wr.ch <- err
}
}
コアとなるコードの解説
このコミットの主要な変更点は、persistConn
の readLoop
と writeLoop
の間のチャネルのクローズ処理の同期方法にあります。
-
readLoop
からdefer close(pc.writech)
の削除:- 変更前は、
readLoop
が終了する際にpc.writech
をクローズしていました。これが「send on closed channel」エラーの原因でした。readLoop
が終了しても、writeLoop
がまだpc.writech
からの受信を待機している可能性があり、その間に別のGoroutineがpc.writech
に送信しようとするとパニックが発生しました。 - このコミットでは、この行を削除することで、
readLoop
が直接pc.writech
をクローズしないようにしました。
- 変更前は、
-
writeLoop
でのselect
ステートメントの導入:- 変更後、
writeLoop
はfor wr := range pc.writech
ループの代わりに、無限ループfor {}
とselect
ステートメントを使用するようになりました。 select
ステートメントは2つのケースを待機します。case wr := <-pc.writech:
: これは以前のfor range
ループと同じく、pc.writech
からのリクエストの受信を処理します。リクエストが到着すると、HTTPリクエストの書き込みとフラッシュを行い、結果をwr.ch
に送信します。case <-pc.closech:
: これは新しいケースです。pc.closech
はreadLoop
が接続がクローズされたことを示すためにクローズするチャネルです。readLoop
はdefer close(pc.closech)
を使用してこのチャネルをクローズします。writeLoop
がpc.closech
からの受信を検知すると(つまり、pc.closech
がクローズされたことを検知すると)、return
ステートメントによってwriteLoop
Goroutine自身が安全に終了します。
- 変更後、
この変更により、writeLoop
は readLoop
が接続をクローズしたことを検知し、それに応じて自身も安全に終了できるようになります。これにより、readLoop
が pc.writech
をクローズする前に writeLoop
がまだアクティブであるという競合状態が解消され、「send on closed channel」エラーが防止されます。
要するに、チャネルのクローズを、そのチャネルから受信する側(writeLoop
)が、別のチャネル(pc.closech
)のクローズをトリガーとして検知し、自身の処理を終了するという、より協調的なメカニズムに変更したものです。
関連リンク
- Go issue #3793: https://github.com/golang/go/issues/3793 (ただし、このリンクは現在のGoリポジトリのIssue #3793とは異なる可能性があります。元のIssueは古いGoのコードレビューシステムに存在した可能性があります。)
- Go CL 6445069: https://golang.org/cl/6445069
参考にした情報源リンク
- Web search results for "Go issue 3793":
- https://vertexaisearch.cloud.google.com/grounding-api-redirect/AUZIYQG4n0i5gzOeWIwYOgw0mRXXDIA1HEkwOrDkoNDC31V_osP7TB5F3CbQ4vNAFr0PIkeqJXx5Ys3Wlfw7NQ2oGWOkOqPm53FPn0mQvk4GzAGilGU55C9AuA6-jr72TqBzoQ6A6tkqvJi_5i1y
- https://vertexaisearch.cloud.google.com/grounding-api-redirect/AUZIYQGKLC4EK0yyzu2WeDGVxrlSP_VoAkfTi3uQk9gP1eWUbGZZ2hkOQzaLBIDPrApgDgD---uPEE4kPHtgGfED-2r8FWBrE0DcxHZ1YHGwSFtsoxAYldDPbjEoDnYR7NyPn8v42ebi6QJJ
- https://vertexaisearch.cloud.google.com/grounding-api-redirect/AUZIYQEJJbzN7ZiPFcX3G0X573Ax8lzrRK_Nh8LlPjDe2uan_t9u26bIVe8S8LQDaV7LfeFcZQVtWwUFnDCMZwEB_xF6iKiHpYtn-EE6WmhhF_V0Ks50-Vdcr-Yzhrbj7UX5XVpeiiLsYCzZixKJcQl4da_eQenNrTZ2zG78sIFq1UmPIraqb4Cw7O8qqnHe1ulKVCM7MruxqKNIfX8mV_vs385PhLzeODyWPtj0IKWILCuC-lNf2757Rz82caWvZ5iR5Xy6b0jd_XFKK4eY-Q==
- https://vertexaisearch.cloud.google.com/grounding-api-redirect/AUZIYQEpX0gOOmCHkbKFwqqkVsx8KCcaWlT-dE0SEJcfxZg0XWdB_pu3nGNoEkT7eS-jXMiik7hlwGaqF-K8w8QrldcgbJQxOBxd3xMdvPNzVw9XHw2qoDjSLi6YSyGXSBgZBV1Q
- Web search results for "golang.org/cl/6445069":
- https://vertexaisearch.cloud.google.com/grounding-api-redirect/AUZIYQF3xn-7c-wMNF6x1OUUIbkal4qFcXQx5AAMYj8NmNaslbvSe_pWVgEMmD1TlgMAMOq6TdGVilgIWEX5b-QkHxZn2QbFaczoSYakHG4xEmwoC20xN3uq_jKcxvMjWBpY1R3S
- https://vertexaisearch.cloud.google.com/grounding-api-redirect/AUZIYQGFhlW7OA8bBA9tKFuAt3uazyCz6DPNZ2UCqjQJMGrLvN3rCiCrjfBToeZ5cDGYJAj35j_X8NQPLfDvMs2FU1Qhxf9YamGFPisUWptvFJncXWRglHhCXPtj