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

[インデックス 19117] ファイルの概要

このコミットは、Go言語の net/http パッケージにおける、HTTPチャネルのシャットダウン時に発生する不要な警告メッセージを抑制するための変更です。特に、クライアントがタイムアウトによってリクエストをキャンセルし、接続が強制的に閉じられた際に、サーバーからの応答がまだ処理中であった場合に表示される「Unsolicited response received on idle HTTP channel」という警告を対象としています。

コミット

net/http: quiet useless warning during shutdown

What was happening on Issue 7010 was handler intentionally took 30
milliseconds and the proxy's client timeout was 35 milliseconds. Then it
slammed the proxy with a bunch of requests.

Sometimes the server would be too slow to respond in its 5 millisecond
window and the client code would cancel the request, force-closing the
persistConn. If this came at the right time, the server's reply was
already in flight, and one of the goroutines would report:

Unsolicited response received on idle HTTP channel starting with "H"; err=<nil>

... rightfully scaring the user.

But the error was already handled and returned to the user, and this
connection knows it's been shut down. So look at the closed flag after
acquiring the same mutex guarding another field we were checking, and
don't complain if it's a known shutdown.

Also move closed down below the mutex which guards it.

Fixes #7010

LGTM=dsymonds
R=golang-codereviews, dsymonds
CC=adg, golang-codereviews, rsc
https://golang.org/cl/86740044

GitHub上でのコミットページへのリンク

https://github.com/golang/go/commit/427a444f67544416a7e96b705a10b8be38269767

元コミット内容

このコミットは、Goの net/http パッケージにおいて、HTTP接続のシャットダウン時に発生する、ユーザーを不必要に不安にさせる警告メッセージを抑制することを目的としています。具体的には、クライアントがタイムアウトによりリクエストをキャンセルし、その結果として persistConn (永続的な接続) が強制的に閉じられた際、サーバーからの応答がまだ処理中であった場合に「Unsolicited response received on idle HTTP channel」という警告が表示される問題に対処しています。この警告は、エラーが既に適切に処理され、接続がシャットダウン中であることが既知であるにもかかわらず表示されるため、ユーザーにとって混乱を招くものでした。

変更の背景

この変更は、Go Issue 7010で報告された問題に対応するものです。問題のシナリオは以下の通りです。

  1. HTTPハンドラが意図的に30ミリ秒の処理時間を要する。
  2. プロキシのクライアントタイムアウトが35ミリ秒に設定されている。
  3. クライアントがプロキシに対して大量のリクエストを送信する。
  4. サーバーが5ミリ秒のタイムアウトウィンドウ内に応答できない場合がある。
  5. クライアントコードがリクエストをキャンセルし、persistConn を強制的に閉じる。
  6. このタイミングでサーバーからの応答が既に送信中であった場合、アイドル状態のHTTPチャネルで「Unsolicited response received on idle HTTP channel starting with "H"; err=」という警告がログに出力される。

この警告は、実際にはエラーが既にクライアント側で処理されており、接続がシャットダウン中であるという既知の状態であるにもかかわらず表示されるため、ユーザーを不必要に心配させていました。開発者は、この「無用な警告」を抑制し、よりクリーンなログ出力を実現するためにこの変更を行いました。

前提知識の解説

このコミットを理解するためには、以下のGo言語およびHTTPプロトコルに関する知識が必要です。

  • Go言語の並行処理 (GoroutinesとChannels): Goは軽量なスレッドであるゴルーチンと、それらの間の通信を可能にするチャネルを介して並行処理をサポートします。net/http パッケージは、これらの並行処理プリミティブを広範に利用して、複数の同時リクエストを効率的に処理します。
  • net/http パッケージ: Goの標準ライブラリの一部であり、HTTPクライアントとサーバーの実装を提供します。このコミットは、特にHTTPクライアント側の永続的な接続(keep-alive接続)を管理する transport.go ファイル内の persistConn 構造体に関連しています。
  • HTTP永続的接続 (Keep-Alive): HTTP/1.1では、複数のリクエスト/レスポンスサイクルで同じTCP接続を再利用する「永続的接続」がデフォルトで有効になっています。これにより、接続の確立と切断のオーバーヘッドが削減され、パフォーマンスが向上します。persistConn は、この永続的なHTTP接続を抽象化したものです。
  • bufio.Readerbufio.Writer: これらは、I/O操作をバッファリングするためのGoの標準ライブラリです。ネットワーク接続からの読み取り(br)と書き込み(bw)の効率を高めるために persistConn で使用されます。
  • sync.Mutex: Goの sync パッケージが提供する相互排他ロックです。共有リソース(この場合は persistConn のフィールド)への同時アクセスを防ぎ、データ競合を回避するために使用されます。
  • readLoop: persistConn 内のゴルーチンで実行されるループで、ネットワーク接続から応答を読み取ります。このループ内で、受信したデータが予期された応答であるかどうかを判断し、そうでない場合は警告をログに記録するロジックが含まれています。
  • closed フラグ: persistConn が閉じられたかどうかを示すブール値のフラグです。このコミットの核心は、このフラグの状態を適切にチェックすることによって、不要な警告を抑制することにあります。
  • numExpectedResponses: persistConn が現在処理中または予期している応答の数を追跡するカウンタです。これが0の場合、接続はアイドル状態と見なされます。

技術的詳細

このコミットの技術的な核心は、persistConnreadLoop メソッドにおける警告ロジックの変更と、closed フラグの配置の調整にあります。

以前の実装では、readLoop がアイドル状態のHTTPチャネルで予期しない応答(Unsolicited response)を受信した場合、pc.numExpectedResponses == 0 であれば、即座に pc.closeLocked() を呼び出し、その後警告をログに出力していました。この際、closed フラグのチェックが警告出力の前に行われていませんでした。

変更後、警告を出力する前に pc.closed フラグをチェックする条件が追加されました。具体的には、pc.numExpectedResponses == 0 であり、かつ !pc.closed (接続がまだ閉じられていない) の場合にのみ、pc.closeLocked() を呼び出し、警告をログに出力するように変更されました。これにより、接続が既にシャットダウン中であることが既知である場合には、警告が抑制されるようになりました。

また、closed フィールドの定義位置が persistConn 構造体内で変更されました。以前は sync.Mutex (lk) の上に定義されていましたが、lk がガードするフィールドのリストに closed も含まれることを明確にするため、lk の直下に移動されました。これはコードの可読性と保守性を向上させるための変更であり、lkclosed フィールドへのアクセスを保護していることを視覚的に示します。

この変更により、クライアントがタイムアウトなどで接続を強制的に閉じた際に、サーバーからの遅延応答が到着しても、それが既知のシャットダウンプロセスの一部であれば、不必要な警告がログに記録されなくなります。これにより、ログがよりクリーンになり、開発者が本当に問題のある状況に集中できるようになります。

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

変更は src/pkg/net/http/transport.go ファイルに集中しています。

--- a/src/pkg/net/http/transport.go
+++ b/src/pkg/net/http/transport.go
@@ -719,7 +719,6 @@ type persistConn struct {
 	cacheKey connectMethodKey
 	conn     net.Conn
 	tlsState *tls.ConnectionState
-	closed   bool                // whether conn has been closed
 	br       *bufio.Reader       // from conn
 	sawEOF   bool                // whether we've seen EOF from conn; owned by readLoop
 	bw       *bufio.Writer       // to conn
@@ -733,8 +732,9 @@ type persistConn struct {
 	// whether or not a connection can be reused. Issue 7569.
 	writeErrCh chan error
 
-	lk                   sync.Mutex // guards following 3 fields
+	lk                   sync.Mutex // guards following fields
 	numExpectedResponses int
+	closed               bool // whether conn has been closed
 	broken               bool // an error has happened on this connection; marked broken so it's not reused.
 	// mutateHeaderFunc is an optional func to modify extra
 	// headers on each outbound request before it's written. (the
@@ -774,12 +774,14 @@ func (pc *persistConn) readLoop() {
 
 		pc.lk.Lock()
 		if pc.numExpectedResponses == 0 {
-			pc.closeLocked()
-			pc.lk.Unlock()
-			if len(pb) > 0 {
-				log.Printf("Unsolicited response received on idle HTTP channel starting with %q; err=%v",
-					string(pb), err)
+			if !pc.closed { // 追加された条件
+				pc.closeLocked()
+				if len(pb) > 0 {
+					log.Printf("Unsolicited response received on idle HTTP channel starting with %q; err=%v",
+						string(pb), err)
+				}
 			}
+			pc.lk.Unlock() // ロック解除位置の変更
 			return
 		}
 		pc.lk.Unlock()

コアとなるコードの解説

  1. closed フィールドの移動:

    • persistConn 構造体内で、closed フィールドの宣言が lk sync.Mutex の下に移されました。
    • 変更前:
      type persistConn struct {
          // ...
          closed   bool                // whether conn has been closed
          // ...
          lk                   sync.Mutex // guards following 3 fields
          numExpectedResponses int
          broken               bool // an error has happened on this connection; marked broken so it's not reused.
      }
      
    • 変更後:
      type persistConn struct {
          // ...
          br       *bufio.Reader       // from conn
          // ...
          lk                   sync.Mutex // guards following fields
          numExpectedResponses int
          closed               bool // whether conn has been closed
          broken               bool // an error has happened on this connection; marked broken so it's not reused.
      }
      
    • この変更は、closed フィールドが lk ミューテックスによって保護されることをより明確にするためのものです。これにより、コードの意図がより分かりやすくなります。
  2. readLoop 内の警告ロジックの変更:

    • readLoop メソッド内で、pc.numExpectedResponses == 0 (接続がアイドル状態) の場合に警告をログに出力するロジックが変更されました。
    • 変更前:
      if pc.numExpectedResponses == 0 {
          pc.closeLocked()
          pc.lk.Unlock() // ここでロックが解除される
          if len(pb) > 0 {
              log.Printf("Unsolicited response received on idle HTTP channel starting with %q; err=%v",
                  string(pb), err)
          }
          return
      }
      
    • 変更後:
      if pc.numExpectedResponses == 0 {
          if !pc.closed { // 新しく追加された条件
              pc.closeLocked()
              if len(pb) > 0 {
                  log.Printf("Unsolicited response received on idle HTTP channel starting with %q; err=%v",
                      string(pb), err)
              }
          }
          pc.lk.Unlock() // ロック解除がifブロックの外に移動
          return
      }
      
    • 最も重要な変更点は、if !pc.closed という条件が追加されたことです。これにより、persistConn が既に閉じられている(シャットダウン中である)場合は、pc.closeLocked() の呼び出しと警告メッセージの出力がスキップされます。
    • また、pc.lk.Unlock() の位置が変更され、if !pc.closed ブロックの外に移動しました。これにより、pc.numExpectedResponses == 0 の条件が満たされた場合、pc.closed の状態にかかわらず、readLoop が終了する前に必ずロックが解除されるようになりました。これは、ミューテックスの適切な管理とデッドロックの回避のために重要です。

これらの変更により、net/http パッケージは、正常なシャットダウンプロセス中に発生する可能性のある誤解を招く警告を抑制し、より堅牢でユーザーフレンドリーな動作を実現しています。

関連リンク

  • Go Issue 7010: このコミットが修正した問題のトラッキング。
  • Gerrit Change-Id: I427a444f67544416a7e96b705a10b8be38269767 (コミットハッシュと同じ)
  • Go net/http パッケージのドキュメント: https://pkg.go.dev/net/http

参考にした情報源リンク