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

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

このコミットは、Go言語の標準ライブラリである net/http パッケージにおけるクライアント側のゴルーチンリーク(goroutine leak)を修正するものです。特に、HTTPの永続的な接続(persistent connections)を使用している場合に発生する可能性のあるリソースリーク問題に対処しています。

コミット

commit d0a7d01ff2f62d83cc7ebc1c593aae652e205d66
Author: Brad Fitzpatrick <bradfitz@golang.org>
Date:   Tue Feb 14 12:48:56 2012 +1100

    net/http: fix client goroutine leak with persistent connections
    
    Thanks to Sascha Matzke & Florian Weimer for diagnosing.
    
    R=golang-dev, adg, bradfitz, kevlar
    CC=golang-dev
    https://golang.org/cl/5656046

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

https://github.com/golang/go/commit/d0a7d01ff2f62d83cc7ebc1c593aae652e205d66

元コミット内容

net/http: fix client goroutine leak with persistent connections

Thanks to Sascha Matzke & Florian Weimer for diagnosing.

R=golang-dev, adg, bradfitz, kevlar
CC=golang-dev
https://golang.org/cl/5656046

変更の背景

このコミットは、Goの net/http クライアントが永続的な接続(Keep-Alive)を使用する際に発生していたゴルーチンリークの問題を解決するために導入されました。この問題は、特にHTTPレスポンスボディが適切に閉じられない場合に顕著に現れ、persistConn.readLooppersistConn.writeLoop といった接続管理のためのゴルーチンが終了せずに残り続けることで、徐々にゴルーチンが蓄積され、メモリリークやリソース消費の増加を引き起こしていました。

2012年2月、Sascha Matzke氏がGoogle Groupsのディスカッションでこの挙動を報告し、persistConn.readLoop() ゴルーチンがリークしてアプリケーションクラッシュを引き起こす可能性があることを指摘しました。Florian Weimer氏もこの議論に貢献し、接続が最大接続数に達したためにプールから破棄された際に、putIdleConn 関数がそのゴルーチンを適切に終了させないことに関連している可能性を示唆するパッチを提供しました。

この問題は、公式のGo GitHubリポジトリでも「net/http: leaking connections in Client」(Issue #4049)として追跡され、http.Client のインスタンスをリクエストごとに新しく作成したり、HTTPリクエスト後に defer resp.Body.Close() を呼び出さないことがリークを悪化させることが強調されました。

このコミットは、これらの診断結果に基づき、永続接続の管理ロジックを改善し、不要になったゴルーチンが確実に終了するようにすることで、リークを防止することを目的としています。

前提知識の解説

  • ゴルーチン (Goroutine): Go言語における軽量な並行処理の単位です。OSのスレッドよりもはるかに軽量で、数百万のゴルーチンを同時に実行することも可能です。しかし、適切に管理されないと、終了すべきゴルーチンが終了せずに残り続け、メモリやCPUリソースを消費し続ける「ゴルーチンリーク」を引き起こす可能性があります。
  • 永続接続 (Persistent Connections / HTTP Keep-Alive): HTTP/1.1で導入された機能で、一つのTCP接続上で複数のHTTPリクエスト/レスポンスをやり取りすることを可能にします。これにより、接続の確立・切断にかかるオーバーヘッドを削減し、パフォーマンスを向上させます。
  • net/http パッケージ: Go言語の標準ライブラリで、HTTPクライアントとサーバーの実装を提供します。クライアント側では、http.Client がHTTPリクエストの送信とレスポンスの受信を管理し、内部的に接続プール(Transport)を使用して永続接続を効率的に再利用します。
  • Transport: net/http パッケージのクライアント側で、実際のHTTPリクエストの送信とレスポンスの受信、および接続の管理(接続の確立、再利用、クローズなど)を行うインターフェースです。デフォルトの http.DefaultTransport は、永続接続をサポートしています。
  • persistConn: net/http パッケージの内部構造体で、単一の永続的なTCP接続を表します。この構造体には、接続からの読み取り(readLoop)と書き込み(writeLoop)を処理するためのゴルーチンが関連付けられています。
  • putIdleConn: Transport のメソッドで、使用済みでアイドル状態になった永続接続を接続プールに戻す役割を担います。この関数が適切に接続の状態を判断し、不要な接続をクローズしないと、リークの原因となります。
  • readLoop: persistConn に関連付けられたゴルーチンで、永続接続からHTTPレスポンスを読み取る役割を担います。レスポンスボディの読み取りが完了した際や、エラーが発生した際に、このゴルーチンが適切に終了するか、接続がプールに戻されるかが重要になります。

技術的詳細

このコミットの技術的な核心は、net/http パッケージの Transport 構造体におけるアイドル接続の管理と、persistConnreadLoop ゴルーチンのライフサイクル管理の改善にあります。

以前の実装では、Transport.putIdleConn メソッドが pconn (永続接続) をアイドル接続のリストに追加する際に、接続が不要になった場合(例: DisableKeepAlives が有効、MaxIdleConnsPerHost を超えた場合、または接続が壊れている場合)に pconn.close() を呼び出していましたが、その後の処理フローがゴルーチンリークを引き起こす可能性がありました。特に、putIdleConnvoid を返していたため、呼び出し元(readLoop)は接続が実際にプールに戻されたのか、それともクローズされたのかを判断できませんでした。

この修正では、putIdleConn メソッドのシグネチャが変更され、bool 型の戻り値が追加されました。この戻り値は、接続がアイドル接続プールに正常に追加された場合に true を返し、接続が不要と判断されてクローズされた場合には false を返します。

// putIdleConn adds pconn to the list of idle persistent connections awaiting
// a new request.
// 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 // 変更点: false を返す
	}
	if pconn.isBroken() {
		return false // 変更点: false を返す
	}
	key := pconn.cacheKey
	max := t.MaxIdleConnsPerHost
	if max == 0 { // DefaultMaxIdleConnsPerHost
		max = DefaultMaxIdleConnsPerHost
	}
	if len(t.idleConn[key]) >= max {
		pconn.close()
		return false // 変更点: false を返す
	}
	t.idleConn[key] = append(t.idleConn[key], pconn)
	return true // 変更点: true を返す
}

この変更により、persistConn.readLoop ゴルーチン内で putIdleConn が呼び出された際に、その戻り値を確認できるようになりました。readLoop は、HTTPレスポンスボディの読み取りが完了した後、またはレスポンスボディがない場合に、接続をアイドルプールに戻そうとします。

修正前は、putIdleConn(pc) を呼び出した後、readLoop は常に alive = true と仮定してループを継続していました。しかし、putIdleConn が内部で接続をクローズしていた場合でも、readLoop はその事実を知らずに、不要になった接続に対してゴルーチンを維持し続けてしまう可能性がありました。

修正後は、putIdleConn の戻り値が false の場合、つまり接続がプールに戻されずにクローズされた場合、readLoopalive = false を設定し、自身のループを終了するように変更されました。これにより、不要になった persistConn に関連する readLoop ゴルーチンが適切に終了し、リークが防止されます。

// src/pkg/net/http/transport.go の readLoop 内の変更
// ...
				resp.Body.(*bodyEOFSignal).fn = func() {
					if !pc.t.putIdleConn(pc) { // 変更点: 戻り値を確認
						alive = false // 戻り値が false ならループを終了
					}
					waitForBodyRead <- true
				}
// ...
			} else { // レスポンスボディがない場合
// ...
				if !pc.t.putIdleConn(pc) { // 変更点: 戻り値を確認
					alive = false // 戻り値が false ならループを終了
				}
			}
// ...

この修正は、接続のライフサイクル管理をより堅牢にし、特に多数のHTTPリクエストを処理するクライアントアプリケーションにおいて、リソースの枯渇を防ぐ上で非常に重要です。

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

--- a/src/pkg/net/http/transport.go
+++ b/src/pkg/net/http/transport.go
@@ -235,15 +235,19 @@ func (cm *connectMethod) proxyAuth() string {
 	return ""
 }
 
-func (t *Transport) putIdleConn(pconn *persistConn) {
+// putIdleConn adds pconn to the list of idle persistent connections awaiting
+// a new request.
+// 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()
-\t\treturn
+\t\treturn false
 	}
 	if pconn.isBroken() {
-\t\treturn
+\t\treturn false
 	}
 	key := pconn.cacheKey
 	max := t.MaxIdleConnsPerHost
@@ -252,9 +256,10 @@ func (t *Transport) putIdleConn(pconn *persistConn) {
 	}
 	if len(t.idleConn[key]) >= max {
 		pconn.close()
-\t\treturn
+\t\treturn false
 	}
 	t.idleConn[key] = append(t.idleConn[key], pconn)
+\treturn true
 }
 
 func (t *Transport) getIdleConn(cm *connectMethod) (pconn *persistConn) {
@@ -565,7 +570,9 @@ func (pc *persistConn) readLoop() {
 				lastbody = resp.Body
 				waitForBodyRead = make(chan bool)
 				resp.Body.(*bodyEOFSignal).fn = func() {
-\t\t\t\t\tpc.t.putIdleConn(pc)
+\t\t\t\t\tif !pc.t.putIdleConn(pc) {
+\t\t\t\t\t\talive = false
+\t\t\t\t\t}
 					waitForBodyRead <- true
 				}
 			} else {
@@ -578,7 +585,9 @@ func (pc *persistConn) readLoop() {
 				// read it (even though it'll just be 0, EOF).
 				lastbody = nil
 
-\t\t\t\tpc.t.putIdleConn(pc)
+\t\t\t\tif !pc.t.putIdleConn(pc) {
+\t\t\t\t\talive = false
+\t\t\t\t}
 			}
 		}
 
--- a/src/pkg/net/http/transport_test.go
+++ b/src/pkg/net/http/transport_test.go
@@ -16,6 +16,7 @@ import (
 	. "net/http"
 	"net/http/httptest"
 	"net/url"
+\t"runtime"
 	"strconv"
 	"strings"
 	"testing"
@@ -632,6 +633,66 @@ func TestTransportGzipRecursive(t *testing.T) {
 	}
 }
 
+// tests that persistent goroutine connections shut down when no longer desired.
+func TestTransportPersistConnLeak(t *testing.T) {
+\tgotReqCh := make(chan bool)
+\tunblockCh := make(chan bool)
+\tts := httptest.NewServer(HandlerFunc(func(w ResponseWriter, r *Request) {
+\t\tgotReqCh <- true
+\t\t<-unblockCh
+\t\tw.Header().Set("Content-Length", "0")
+\t\tw.WriteHeader(204)
+\t}))
+\tdefer ts.Close()
+\n+\ttr := &Transport{}\n+\tc := &Client{Transport: tr}\n+\n+\tn0 := runtime.Goroutines()\n+\n+\tconst numReq = 100
+\tdidReqCh := make(chan bool)
+\tfor i := 0; i < numReq; i++ {\n+\t\tgo func() {\n+\t\t\tc.Get(ts.URL)\n+\t\t\tdidReqCh <- true\n+\t\t}()
+\t}\n+\n+\t// Wait for all goroutines to be stuck in the Handler.\n+\tfor i := 0; i < numReq; i++ {\n+\t\t<-gotReqCh\n+\t}\n+\n+\tnhigh := runtime.Goroutines()\n+\n+\t// Tell all handlers to unblock and reply.\n+\tfor i := 0; i < numReq; i++ {\n+\t\tunblockCh <- true\n+\t}\n+\n+\t// Wait for all HTTP clients to be done.\n+\tfor i := 0; i < numReq; i++ {\n+\t\t<-didReqCh\n+\t}\n+\n+\ttime.Sleep(100 * time.Millisecond)\n+\truntime.GC()\n+\truntime.GC() // even more.\n+\tnfinal := runtime.Goroutines()\n+\n+\tgrowth := nfinal - n0\n+\n+\t// We expect 5 extra goroutines, empirically. That number is at least\n+\t// DefaultMaxIdleConnsPerHost * 2 (one reader goroutine, one writer),\n+\t// and something else.\n+\texpectedGoroutineGrowth := DefaultMaxIdleConnsPerHost*2 + 1\n+\n+\tif int(growth) > expectedGoroutineGrowth*2 {\n+\t\tt.Errorf("goroutine growth: %d -> %d -> %d (delta: %d)", n0, nhigh, nfinal, growth)\n+\t}\n+}\n+\n type fooProto struct{}\n```

## コアとなるコードの解説

このコミットにおける主要な変更点は以下の2つです。

1.  **`Transport.putIdleConn` メソッドの戻り値の変更**:
    *   変更前: `func (t *Transport) putIdleConn(pconn *persistConn)` (戻り値なし)
    *   変更後: `func (t *Transport) putIdleConn(pconn *persistConn) bool` (bool型を返す)
    *   この変更により、`putIdleConn` が接続をアイドルプールに正常に追加した場合は `true` を、接続が不要と判断されてクローズされた場合は `false` を返すようになりました。これにより、呼び出し元は接続が実際に再利用可能になったのか、それとも破棄されたのかを正確に判断できるようになります。

2.  **`persistConn.readLoop` ゴルーチン内のロジック変更**:
    *   `readLoop` は、HTTPレスポンスの読み取りが完了した後、またはレスポンスボディがない場合に、`pc.t.putIdleConn(pc)` を呼び出して接続をアイドルプールに戻そうとします。
    *   変更前は、`putIdleConn` の呼び出し後も `readLoop` は `alive = true` のままループを継続する可能性がありました。
    *   変更後: `if !pc.t.putIdleConn(pc) { alive = false }` という条件が追加されました。
        *   これは、`putIdleConn` が `false` を返した場合(つまり、接続がアイドルプールに戻されずにクローズされた場合)、`readLoop` は自身の `alive` フラグを `false` に設定し、ループを終了するように指示します。
        *   これにより、不要になった永続接続に関連する `readLoop` ゴルーチンが適切に終了し、ゴルーチンリークが防止されます。

これらの変更により、`net/http` クライアントは、永続接続の管理をより厳密に行い、不要になった接続に関連するゴルーチンを確実にクリーンアップできるようになりました。これにより、長期間稼働するアプリケーションや多数のHTTPリクエストを処理するアプリケーションにおけるリソースの効率的な利用と安定性が向上します。

また、このコミットには、ゴルーチンリークをテストするための新しいテストケース `TestTransportPersistConnLeak` が追加されています。このテストは、多数のHTTPリクエストを並行して実行し、リクエスト処理中にゴルーチンをブロックし、その後解放することで、ゴルーチンの数が期待値を超えて増加しないことを検証します。これにより、将来的な回帰を防ぐための安全網が提供されます。

## 関連リンク

*   Go言語の公式ドキュメント: [https://golang.org/pkg/net/http/](https://golang.org/pkg/net/http/)
*   Go言語のIssueトラッカー: [https://github.com/golang/go/issues/4049](https://github.com/golang/go/issues/4049) (関連する可能性のあるIssue)
*   Go言語のコードレビューシステム (Gerrit): [https://golang.org/cl/5656046](https://golang.org/cl/5656046) (このコミットの元の変更リスト)

## 参考にした情報源リンク

*   GitHubのコミットページ: [https://github.com/golang/go/commit/d0a7d01ff2f62d83cc7ebc1c593aae652e205d66](https://github.com/golang/go/commit/d0a7d01ff2f62d83cc7ebc1c593aae652e205d66)
*   Web検索結果 (Google Search):
    *   [https://vertexaisearch.cloud.google.com/grounding-api-redirect/AUZIYQGZEx9W5fGKLJGphyDHyOXFeWIQvPYEbJuVmQwaCWoXyki-LObPb6k8-HWkOPzpwp6r7mWcoQMZioXWveRFlXukzq_5e2g58ya2s_X2BKH-qXIZY8jiqMnxnKjc8WD2RsiION4=](https://vertexaisearch.cloud.google.com/grounding-api-redirect/AUZIYQGZEx9W5fGKLJGphyDHyOXFeWIQvPYEbJuVmQwaCWoXyki-LObPb6k8-HWkOPzpwp6r7mWcoQMZioXWveRFlXukzq_5e2g58ya2s_X2BKH-qXIZY8jiqMnxnKjc8WD2RsiION4=)
    *   [https://vertexaisearch.cloud.google.com/grounding-api-redirect/AUZIYQHO7ymk_pypu0-iX7gxMgxO21u_c1xW2Tl9i8a12_eHZ13datgvMC-VC4Pxnpyc6gRBHzzk23_squrWI6bLO57ydT-bXYYKEt6ERALB_2zGnKVI6miDeGyCjpQofuWzFssf0mU=](https://vertexaisearch.cloud.google.com/grounding-api-redirect/AUZIYQHO7ymk_pypu0-iX7gxMgxO21u_c1xW2Tl9i8a12_eHZ13datgvMC-VC4Pxnpyc6gRBHzzk23_squrWI6bLO57ydT-bXYYKEt6ERALB_2zGnKVI6miDeGyCjpQofuWzFssf0mU=)
    *   [https://vertexaisearch.cloud.google.com/grounding-api-redirect/AUZIYQG-rM0kc5Cu_e2AokepMRmv6wyhhhPFRL77VVw1zoZ8F25wFH17hc00HXOk3ew9IK1-o10sMnsCFT0FMyxH7QkfNVJ8J432wqmlaAwLHzRY9mfCeuacL4op04BLE6TozeYvJIcOtxXJbTkXu8ZdwQbZEuGGDAaF_p7XreQbiOS48yfkqabMX28OBFrS7htKFvEvFYEWt4D1_R60ow3TmtLKanj3kUYFVAb7u0-bMBrKtXOT1Q==](https://vertexaisearch.cloud.google.com/grounding-api-redirect/AUZIYQG-rM0kc5Cu_e2AokepMRmv6wyhhhPFRL77VVw1zoZ8F25wFH17hc00HXOk3ew9IK1-o10sMnsCFT0FMyxH7QkfNVJ8J432wqmlaAwLHzRY9mfCeuacL4op04BLE6TozeYvJIcOtxXJbTkXu8ZdwQbZEuGGDAaF_p7XreQbiOS48yfkqabMX28OBFrS7htKFvEvFYEWt4D1_R60ow3TmtLKanj3kUYFVAb7u0-bMBrKtXOT1Q==)
    *   [https://vertexaisearch.cloud.google.com/grounding-api-redirect/AUZIYQHE6738nCAcSTORyIj5mj_cIV43a3VbV8FTEcs-to8yASz49jGbMwdEWdvIIb9sIUenUnEVwKlFGRs5NyrdF-PBr4PpUnUKQsvmR7kDkdfXbiixJIVgWX_Sbw3-LtfVER2P6KwJ7vWqflnjbcxnlrnV](https://vertexaisearch.cloud.google.com/grounding-api-redirect/AUZIYQHE6738nCAcSTORyIj5mj_cIV43a3VbV8FTEcs-to8yASz49jGbMwdEWdvIIb9sIUenUnEVwKlFGRs5NyrdF-PBr4PpUnUKQsvmR7kDkdfXbiixJIVgWX_Sbw3-LtfVER2P6KwJ7vWqflnjbcxnlrnV)
    *   [https://vertexaisearch.cloud.google.com/grounding-api-redirect/AUZIYQEuwZ51RAW722EBUTM9v9OPb4DS0zl4c2xe4CJx3LqNykm1YO83j1R6LgTFwQeEuok5Z1AnhdDPE9SoYEQiYsF7h4TVWbOERwJEYdt2LR0NEIGDHBt-IL1NqsNUNjKFTnn9-U1bwGWK78YjlwdnkzCb](https://vertexaisearch.cloud.google.com/grounding-api-redirect/AUZIYQEuwZ51RAW722EBUTM9v9OPb4DS0zl4c2xe4CJx3LqNykm1YO83j1R6LgTFwQeEuok5Z1AnhdDPE9SoYEQiYsF7h4TVWbOERwJEYdt2LR0NEIGDHBt-IL1NqsNUNjKFTnn9-U1bwGWK78YjlwdnkzCb)