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

[インデックス 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接続を表現し、その接続上でのリクエスト/レスポンスの送受信を管理します。
  • persistConnreadLoopwriteLoop:
    • persistConn は、通常、2つの主要なGoroutineを起動して接続を管理します。
      • readLoop: サーバーからのHTTPレスポンスを読み取る役割を担います。
      • writeLoop: クライアントからのHTTPリクエストをサーバーに書き込む役割を担います。
    • これらのGoroutineはチャネルを介して通信し、リクエストの送信やレスポンスの受信を調整します。

技術的詳細

問題の核心は、persistConnreadLoopwriteLoop という2つのGoroutineが、pc.writech というチャネルのクローズ処理をどのように扱っていたかにあります。

元のコードでは、readLoop が接続の読み取り側が終了する際に、defer close(pc.writech) を使用して pc.writech をクローズしていました。pc.writech は、writeLoop がリクエストを書き込むために待機しているチャネルです。

しかし、readLooppc.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
 	}
 }
 

コアとなるコードの解説

このコミットの主要な変更点は、persistConnreadLoopwriteLoop の間のチャネルのクローズ処理の同期方法にあります。

  1. readLoop から defer close(pc.writech) の削除:

    • 変更前は、readLoop が終了する際に pc.writech をクローズしていました。これが「send on closed channel」エラーの原因でした。readLoop が終了しても、writeLoop がまだ pc.writech からの受信を待機している可能性があり、その間に別のGoroutineが pc.writech に送信しようとするとパニックが発生しました。
    • このコミットでは、この行を削除することで、readLoop が直接 pc.writech をクローズしないようにしました。
  2. writeLoop での select ステートメントの導入:

    • 変更後、writeLoopfor wr := range pc.writech ループの代わりに、無限ループ for {}select ステートメントを使用するようになりました。
    • select ステートメントは2つのケースを待機します。
      • case wr := <-pc.writech:: これは以前の for range ループと同じく、pc.writech からのリクエストの受信を処理します。リクエストが到着すると、HTTPリクエストの書き込みとフラッシュを行い、結果を wr.ch に送信します。
      • case <-pc.closech:: これは新しいケースです。pc.closechreadLoop が接続がクローズされたことを示すためにクローズするチャネルです。readLoopdefer close(pc.closech) を使用してこのチャネルをクローズします。writeLooppc.closech からの受信を検知すると(つまり、pc.closech がクローズされたことを検知すると)、return ステートメントによって writeLoop Goroutine自身が安全に終了します。

この変更により、writeLoopreadLoop が接続をクローズしたことを検知し、それに応じて自身も安全に終了できるようになります。これにより、readLooppc.writech をクローズする前に writeLoop がまだアクティブであるという競合状態が解消され、「send on closed channel」エラーが防止されます。

要するに、チャネルのクローズを、そのチャネルから受信する側(writeLoop)が、別のチャネル(pc.closech)のクローズをトリガーとして検知し、自身の処理を終了するという、より協調的なメカニズムに変更したものです。

関連リンク

参考にした情報源リンク