[インデックス 11880] ファイルの概要
このコミットは、Go言語の標準ライブラリであるnet/http
パッケージ内のtransport_test.go
ファイルに対する変更です。このファイルは、HTTPクライアントがリクエストを送信する際に使用するhttp.Transport
の動作をテストするためのものです。特に、TestTransportPersistConnLeak
というテスト関数に焦点を当てています。このテストは、HTTPの永続的なコネクション(Keep-Alive)が適切に管理され、不要なファイルディスクリプタ(FD)やGoルーチンがリークしないことを検証することを目的としています。
コミット
commit c2a11576802227df0a5b3a507a430420192bef70
Author: Brad Fitzpatrick <bradfitz@golang.org>
Date: Tue Feb 14 15:26:09 2012 +1100
net/http: re-enable test
Now with a bit more paranoia and lower number of requests
to keep it under the default OS X 256 fd limit.
R=golang-dev, dsymonds, rsc
CC=golang-dev
https://golang.org/cl/5659051
GitHub上でのコミットページへのリンク
https://github.com/golang/go/commit/c2a11576802227df0a5b3a507a430420192bef70
元コミット内容
net/http: re-enable test
Now with a bit more paranoia and lower number of requests
to keep it under the default OS X 256 fd limit.
R=golang-dev, dsymonds, rsc
CC=golang-dev
https://golang.org/cl/5659051
変更の背景
このコミットの主な背景は、以前に無効化されていたTestTransportPersistConnLeak
テストを再有効化することにあります。このテストは、HTTPクライアントのコネクション管理におけるファイルディスクリプタ(FD)やGoルーチンのリークを検出するために設計されていましたが、それ自体がFDリークの問題を抱えていたため、一時的に無効化されていました(t.Logf("test is buggy - appears to leak fds")
とreturn
でスキップされていた)。
特に、OS X環境ではデフォルトのファイルディスクリプタ制限が256と比較的低く設定されており、多数のHTTPリクエストを同時に処理するテストでは、この制限に容易に達してしまい、テストが失敗する原因となっていました。このコミットは、テストの信頼性を向上させ、実際のリークを正確に検出できるようにするために、テスト自体の動作を改善し、FD制限に抵触しないように調整することを目的としています。
前提知識の解説
このコミットを理解するためには、以下のGo言語およびネットワークプログラミングに関する基本的な知識が必要です。
- Go言語の
net/http
パッケージ: Go言語でHTTPクライアントおよびサーバーを構築するための標準ライブラリです。HTTPリクエストの送信、レスポンスの受信、コネクションの管理など、HTTP通信に必要な機能を提供します。 http.Transport
:net/http
パッケージの一部で、HTTPリクエストの実際の送信メカニズムを制御します。具体的には、TCPコネクションの確立、TLSハンドシェイク、プロキシ設定、そして最も重要な**コネクションプーリング(Keep-Alive)**を担当します。これにより、複数のHTTPリクエストで同じTCPコネクションを再利用し、パフォーマンスを向上させることができます。httptest.NewServer
: テスト目的で一時的なHTTPサーバーを簡単に起動するためのユーティリティ関数です。実際のネットワークポートをリッスンし、テスト対象のHTTPクライアントが接続できるエンドポイントを提供します。- Goルーチン (Goroutine): Go言語の軽量な並行処理単位です。OSのスレッドよりもはるかに軽量で、数百万のGoルーチンを同時に実行することも可能です。GoルーチンはGoランタイムによってスケジューリングされ、効率的な並行処理を実現します。しかし、適切に管理されないと、不要なGoルーチンが残り続け、メモリリークやリソース枯渇の原因となることがあります。
- ファイルディスクリプタ (File Descriptor, FD): オペレーティングシステムがファイル、ソケット、パイプなどのI/Oリソースを識別するために使用する抽象的なハンドル(整数値)です。HTTPコネクションもソケットを通じて行われるため、各コネクションはFDを消費します。FDが適切に閉じられないと、FDリークが発生し、システムが新しいコネクションを確立できなくなる可能性があります。
DefaultMaxIdleConnsPerHost
:http.Transport
のフィールドの一つで、ホストごとにアイドル状態(使用されていないが、再利用のために開かれたままになっている)のコネクションを最大でいくつ保持するかを定義します。この値は、コネクションプーリングの動作に影響を与えます。(*http.Transport).CloseIdleConnections()
:http.Transport
のメソッドで、現在アイドル状態にあるすべてのコネクションを強制的に閉じます。これにより、関連するGoルーチンやファイルディスクリプタが解放されます。runtime.Goroutines()
: 現在実行中のGoルーチンの数を返すGo標準ライブラリの関数です。Goルーチンリークの検出に役立ちます。t.Errorf
,t.Logf
: Goのtesting
パッケージで提供される関数で、テスト中にエラーを報告したり、ログメッセージを出力したりするために使用されます。
技術的詳細
このコミットは、net/http
パッケージのTestTransportPersistConnLeak
テストの信頼性と正確性を向上させるための複数の重要な変更を含んでいます。
-
テストの再有効化: 以前のコードでは、
t.Logf("test is buggy - appears to leak fds")
とreturn
によってテストが早期に終了し、実質的に無効化されていました。このコミットでは、これらの行を削除することで、テストが再び実行されるようにしました。これは、テストが抱えていたFDリークの問題が解決された、あるいは少なくとも軽減されたことを示唆しています。 -
リクエスト数の削減 (
numReq = 100
からnumReq = 25
へ): テスト内で発行されるHTTPリクエストの数を100から25に減らしました。これは、コミットメッセージにもあるように「OS Xのデフォルト256 FD制限を下回るように」するためです。多数の同時リクエストは、短期間に多くのファイルディスクリプタを消費し、OSのFD制限に達するとtoo many open files
のようなエラーでテストが失敗する可能性があります。リクエスト数を減らすことで、テストがFD不足でクラッシュするのを防ぎ、真のリーク問題に焦点を当てられるようにします。 -
res.Body.Close()
の追加: HTTPレスポンスのボディ(res.Body
)は、読み取りが完了した後、またはエラーが発生した場合に必ず閉じられる必要があります。これを怠ると、基盤となるTCPコネクションが解放されず、コネクションが再利用されなくなったり、ファイルディスクリプタや関連するGoルーチンがリークしたりする原因となります。この変更では、c.Get(ts.URL)
の呼び出し後にres.Body.Close()
を明示的に呼び出すことで、リソースの適切な解放を保証しています。 -
エラーハンドリングの追加:
c.Get(ts.URL)
の呼び出しにエラーハンドリングが追加されました。以前はエラーが発生しても無視されていましたが、if err != nil { t.Errorf("client fetch error: %v", err); return }
というコードが追加され、クライアントからのフェッチ中にエラーが発生した場合にテストが適切に失敗するようになりました。これにより、テストの堅牢性が向上します。 -
tr.CloseIdleConnections()
の追加: テストの終盤で、tr.CloseIdleConnections()
が呼び出されるようになりました。これは、http.Transport
が保持しているアイドル状態の永続コネクションをすべて強制的に閉じるための重要なステップです。これにより、テストが終了する際に、これらのコネクションに関連するGoルーチンやFDが確実に解放され、テスト後のリソースリークを防ぎ、Goルーチン数の計測がより正確になります。 -
Goルーチン成長の期待値の調整: Goルーチンのリークを検出するためのロジックが変更されました。
- 以前は、
expectedGoroutineGrowth := DefaultMaxIdleConnsPerHost*2 + 1
という経験的な値に基づいており、if int(growth) > expectedGoroutineGrowth*2
という条件でリークを判断していました。 - 新しいロジックでは、
// We expect 0 or 1 extra goroutine, empirically. Allow up to 5.
というコメントが追加され、if int(growth) > 5
というより厳密な条件でリークを判断するようになりました。 これは、上記のres.Body.Close()
やtr.CloseIdleConnections()
といったリソース管理の改善により、テスト終了後のGoルーチン数が大幅に削減されることを期待しているためです。これにより、テストはより少ないGoルーチンの増加でもリークとして検出できるようになり、テストの感度が向上します。
- 以前は、
これらの変更は、net/http
パッケージのコネクション管理の堅牢性を高め、テストの信頼性を向上させる上で非常に重要です。
コアとなるコードの変更箇所
src/pkg/net/http/transport_test.go
ファイルにおける変更の差分は以下の通りです。
--- a/src/pkg/net/http/transport_test.go
+++ b/src/pkg/net/http/transport_test.go
@@ -635,9 +635,6 @@ func TestTransportGzipRecursive(t *testing.T) {
// tests that persistent goroutine connections shut down when no longer desired.
func TestTransportPersistConnLeak(t *testing.T) {
- t.Logf("test is buggy - appears to leak fds")
- return
-
gotReqCh := make(chan bool)
unblockCh := make(chan bool)
ts := httptest.NewServer(HandlerFunc(func(w ResponseWriter, r *Request) {
@@ -653,12 +650,17 @@ func TestTransportPersistConnLeak(t *testing.T) {
n0 := runtime.Goroutines()
- const numReq = 100
+ const numReq = 25
didReqCh := make(chan bool)
for i := 0; i < numReq; i++ {
go func() {
- c.Get(ts.URL)
+ res, err := c.Get(ts.URL)
didReqCh <- true
+ if err != nil {
+ t.Errorf("client fetch error: %v", err)
+ return
+ }
+ res.Body.Close()
}()
}
@@ -679,6 +681,7 @@ func TestTransportPersistConnLeak(t *testing.T) {
<-didReqCh
}
+ tr.CloseIdleConnections()
time.Sleep(100 * time.Millisecond)
runtime.GC()
runtime.GC() // even more.
@@ -686,13 +689,11 @@ func TestTransportPersistConnLeak(t *testing.T) {
growth := nfinal - n0
- // We expect 5 extra goroutines, empirically. That number is at least
- // DefaultMaxIdleConnsPerHost * 2 (one reader goroutine, one writer),
- // and something else.
- expectedGoroutineGrowth := DefaultMaxIdleConnsPerHost*2 + 1
-
- if int(growth) > expectedGoroutineGrowth*2 {
- t.Errorf("goroutine growth: %d -> %d -> %d (delta: %d)", n0, nhigh, nfinal, growth)
+ // We expect 0 or 1 extra goroutine, empirically. Allow up to 5.
+ // Previously we were leaking one per numReq.
+ t.Logf("goroutine growth: %d -> %d -> %d (delta: %d)", n0, nhigh, nfinal, growth)
+ if int(growth) > 5 {
+ t.Error("too many new goroutines")
}
}
コアとなるコードの解説
上記の差分に基づいて、各変更箇所の詳細な解説を行います。
-
テストの再有効化:
- t.Logf("test is buggy - appears to leak fds") - return
この2行が削除されたことで、
TestTransportPersistConnLeak
関数は実行時にスキップされなくなり、テストスイートの一部として再び機能するようになりました。これは、テストが以前抱えていたファイルディスクリプタリークの問題が、このコミットで行われる他の変更によって解決される、または許容範囲に収まるという判断がなされたことを意味します。 -
リクエスト数の削減:
- const numReq = 100 + const numReq = 25
テスト内で並行して発行されるHTTPリクエストの数を100から25に減らしています。これは、特にOS Xのようなデフォルトのファイルディスクリプタ制限が低い環境(通常256)において、テストがFD不足で失敗するのを防ぐための重要な調整です。リクエスト数を減らすことで、テストがシステムリソースの限界に達することなく、本来検出したいGoルーチンやコネクションのリーク問題に集中できるようになります。
-
レスポンスボディのクローズとエラーハンドリングの追加:
- c.Get(ts.URL) + res, err := c.Get(ts.URL) didReqCh <- true + if err != nil { + t.Errorf("client fetch error: %v", err) + return + } + res.Body.Close()
res, err := c.Get(ts.URL)
:c.Get
の戻り値として、レスポンスオブジェクトres
とエラーオブジェクトerr
を受け取るように変更されました。これにより、HTTPリクエストの実行結果を詳細に確認できるようになります。if err != nil { t.Errorf("client fetch error: %v", err); return }
: HTTPリクエストの実行中にエラーが発生した場合、t.Errorf
を使ってテストエラーとして報告し、Goルーチンを早期に終了させます。これにより、ネットワークの問題やサーバー側の問題がテストの失敗として適切に扱われるようになります。res.Body.Close()
: 最も重要な変更点の一つです。http.Response.Body
はio.ReadCloser
インターフェースを実装しており、レスポンスボディの読み取りが完了した後、または読み取りを中断する場合には、必ずClose()
メソッドを呼び出す必要があります。これを怠ると、基盤となるTCPコネクションが閉じられず、コネクションが再利用されなかったり、ファイルディスクリプタや関連するGoルーチンがリークしたりする原因となります。この明示的なクローズ処理により、リソースの適切な解放が保証され、テストの信頼性が大幅に向上します。
-
アイドルコネクションの明示的なクローズ:
+ tr.CloseIdleConnections() time.Sleep(100 * time.Millisecond) runtime.GC() runtime.GC() // even more.
tr.CloseIdleConnections()
が追加されました。これは、http.Transport
インスタンスtr
が保持している、現在アイドル状態にあるすべての永続コネクションを強制的に閉じるメソッドです。このテストはコネクションリークを検出することを目的としているため、テストの最後に明示的にアイドルコネクションを閉じることで、テストが終了する際に不要なGoルーチンやFDが確実に解放されるようにします。これにより、runtime.Goroutines()
で計測されるGoルーチン数がより正確になり、テストのクリーンアップが確実に行われます。 -
Goルーチン成長の期待値の調整とエラーメッセージの改善:
- // We expect 5 extra goroutines, empirically. That number is at least - // DefaultMaxIdleConnsPerHost * 2 (one reader goroutine, one writer), - // and something else. - expectedGoroutineGrowth := DefaultMaxIdleConnsPerHost*2 + 1 - - if int(growth) > expectedGoroutineGrowth*2 { - t.Errorf("goroutine growth: %d -> %d -> %d (delta: %d)", n0, nhigh, nfinal, growth) + // We expect 0 or 1 extra goroutine, empirically. Allow up to 5. + // Previously we were leaking one per numReq. + t.Logf("goroutine growth: %d -> %d -> %d (delta: %d)", n0, nhigh, nfinal, growth) + if int(growth) > 5 { + t.Error("too many new goroutines")
- Goルーチンの成長に関するコメントが更新され、以前の経験的な期待値(
DefaultMaxIdleConnsPerHost*2 + 1
)から、「経験的に0または1の追加Goルーチンを期待し、最大5まで許容する」というより厳密な期待値に変更されました。これは、res.Body.Close()
やtr.CloseIdleConnections()
といったリソース管理の改善により、テスト終了後のGoルーチン数が大幅に削減されることを期待しているためです。 - エラー条件も
if int(growth) > expectedGoroutineGrowth*2
からif int(growth) > 5
へと変更されました。これにより、テストはより少ないGoルーチンの増加でもリークとして検出できるようになり、テストの感度が向上します。 t.Errorf
の代わりにt.Error
を使用し、より簡潔なエラーメッセージ"too many new goroutines"
を出力するように変更されました。また、t.Logf
でGoルーチンの成長に関する詳細なログを出力するようになりました。
- Goルーチンの成長に関するコメントが更新され、以前の経験的な期待値(
これらの変更は、net/http
パッケージのコネクション管理の正確性を保証し、テストがリソースリークをより効果的に検出できるようにするための重要な改善です。
関連リンク
- Go言語
net/http
パッケージ: https://pkg.go.dev/net/http - Go言語
http.Transport
構造体: https://pkg.go.dev/net/http#Transport - Go言語
httptest
パッケージ: https://pkg.go.dev/net/http/httptest - Go言語
runtime
パッケージ: https://pkg.go.dev/runtime - Go言語
testing
パッケージ: https://pkg.go.dev/testing
参考にした情報源リンク
- Go言語公式ドキュメント
- HTTP/1.1 Persistent Connections (Keep-Alive) の概念
- ファイルディスクリプタとリソースリークに関する一般的な知識
- Goルーチンと並行処理に関する一般的な知識