[インデックス 19231] ファイルの概要
このコミットは、Go言語の標準ライブラリ net/http/httputil
パッケージにおける DumpRequestOut
関数が引き起こしていたゴルーチンリークを修正するものです。具体的には、HTTPリクエストのダンプ処理中に不要なコネクションが閉じられずに残り、それによって関連するゴルーチンが終了しない問題に対処しています。
コミット
commit d0402cb416b1e39bd6efba102c2c0c4cf0244bf6
Author: Brad Fitzpatrick <bradfitz@golang.org>
Date: Fri Apr 25 15:19:32 2014 -0700
net/http/httputil: don't leak goroutines in DumpRequestOut
Fixes #7869
LGTM=dsymonds
R=golang-codereviews
CC=adg, dsymonds, golang-codereviews, rsc
https://golang.org/cl/91770048
GitHub上でのコミットページへのリンク
https://github.com/golang/go/commit/d0402cb416b1e39bd6efba102c2c0c4cf0244bf6
元コミット内容
net/http/httputil: don't leak goroutines in DumpRequestOut
Fixes #7869
LGTM=dsymonds
R=golang-codereviews
CC=adg, dsymonds, golang-codereviews, rsc
https://golang.org/cl/91770048
変更の背景
このコミットは、Go言語の net/http/httputil
パッケージ内の DumpRequestOut
関数に存在していたゴルーチンリークの問題を解決するために導入されました。DumpRequestOut
は、HTTPリクエストの内容をデバッグやロギングのためにバイト列としてダンプする機能を提供します。この関数は内部的に http.Transport
を使用してリクエストを「送信」し、その過程でリクエストとレスポンスの生データをキャプチャします。
問題は、この内部的な http.Transport
が使用された後に、そのコネクションプールが適切にクリーンアップされず、アイドル状態のコネクションが閉じられないことにありました。Goの http.Transport
は、パフォーマンス向上のためにコネクションを再利用する仕組みを持っていますが、DumpRequestOut
のような単発のダンプ操作では、これらのコネクションは再利用されることなく放置される可能性がありました。これにより、コネクションに関連付けられたゴルーチンが終了せず、メモリとCPUリソースを消費し続ける「ゴルーチンリーク」が発生していました。
特に、DumpRequestOut
が頻繁に呼び出されるようなシナリオ(例えば、プロキシやデバッグツールなど)では、このリークが深刻な問題となり、アプリケーションのパフォーマンス低下やリソース枯渇を引き起こす可能性がありました。コミットメッセージにある Fixes #7869
は、この問題がGoのイシュートラッカーで報告されていたことを示しています。
前提知識の解説
このコミットの理解には、以下のGo言語およびHTTPプロトコルに関する前提知識が役立ちます。
-
ゴルーチン (Goroutine): Go言語の軽量な並行処理単位です。OSのスレッドよりもはるかに軽量で、数百万のゴルーチンを同時に実行することも可能です。しかし、適切に管理されないと、終了すべきゴルーチンが終了せずに残り続け、リソースを消費し続ける「ゴルーチンリーク」が発生します。これはメモリリークと同様に、アプリケーションの安定性とパフォーマンスに悪影響を与えます。
-
net/http
パッケージ: Go言語の標準ライブラリで、HTTPクライアントとサーバーの実装を提供します。http.Request
: HTTPリクエストを表す構造体です。http.Transport
: HTTPクライアントが実際にネットワークリクエストを送信するためのメカニズムを提供します。コネクションの確立、再利用(キープアライブ)、プロキシの処理、TLSハンドシェイクなどを担当します。http.Transport
は内部的にコネクションプールを持ち、HTTP/1.xのキープアライブコネクションを再利用することで、リクエストごとのTCPハンドシェイクやTLSハンドシェイクのオーバーヘッドを削減し、パフォーマンスを向上させます。Transport.RoundTrip(req *Request)
:http.Transport
の主要なメソッドで、与えられたリクエストを送信し、レスポンスを受け取ります。Transport.CloseIdleConnections()
:http.Transport
のメソッドで、現在アイドル状態(使用されていない)のすべてのキープアライブコネクションを閉じます。これにより、これらのコネクションに関連付けられたリソース(ソケット、ゴルーチンなど)が解放されます。
-
net/http/httputil
パッケージ:net/http
パッケージのユーティリティ機能を提供します。httputil.DumpRequestOut(req *http.Request, body bool)
: 指定されたhttp.Request
を、ネットワーク上で送信される形式(ワイヤーフォーマット)でバイト列としてダンプする関数です。デバッグやプロキシの実装などで利用されます。この関数は、リクエストを実際に送信するわけではなく、送信されたかのように見せかけるために内部的にhttp.Transport
を利用します。
-
リソース管理と
defer
: Go言語では、リソース(ファイルハンドル、ネットワークコネクションなど)の解放を確実に行うためにdefer
ステートメントがよく使われます。defer
に指定された関数は、それを囲む関数がリターンする直前に実行されます。これにより、エラーパスを含め、どのような状況でもリソースが適切にクリーンアップされることが保証されます。
このゴルーチンリークは、DumpRequestOut
が内部的に http.Transport
を使用する際に、その Transport
が持つアイドルコネクションを明示的に閉じなかったために発生しました。http.Transport
はデフォルトでコネクションを再利用しようとするため、ダンプ目的で一時的に作成された Transport
であっても、使用後に CloseIdleConnections()
を呼び出さないと、そのコネクションがプールに残り、関連するゴルーチンも生き残ってしまうのです。
技術的詳細
DumpRequestOut
関数は、与えられた http.Request
をネットワーク上で送信される形式でダンプするために、一時的な http.Transport
インスタンスを作成します。この Transport
は、リクエストの生データをキャプチャするために、カスタムの Dial
関数と io.MultiWriter
を使用して、ネットワークへの書き込みをバッファにリダイレクトします。
修正前のコードでは、この一時的な http.Transport
インスタンス t
を作成し、t.RoundTrip(reqSend)
を呼び出してリクエストのダンプ処理を行っていました。しかし、RoundTrip
の呼び出し後、t
が持つコネクションプール内のアイドルコネクションを明示的に閉じる処理がありませんでした。
http.Transport
は、HTTP/1.xのキープアライブコネクションを管理し、再利用することで効率を高めます。RoundTrip
が完了しても、基盤となるTCPコネクションはすぐに閉じられるわけではなく、将来の再利用のためにプールに保持される可能性があります。このプールされたコネクションは、通常、タイムアウトするか、CloseIdleConnections()
が明示的に呼び出されるまで、関連するゴルーチン(例えば、コネクションの読み取りを担当するゴルーチン)をアクティブに保ちます。
DumpRequestOut
の目的はリクエストをダンプすることであり、実際にネットワークコネクションを確立して再利用することではありません。そのため、ダンプ処理が完了した時点で、この一時的な Transport
が保持するすべてのアイドルコネクションは不要になります。これらを閉じずに放置すると、関連するゴルーチンがリークし、アプリケーションのリソースを不必要に消費し続けることになります。
このコミットでは、defer t.CloseIdleConnections()
を追加することで、この問題を解決しています。defer
ステートメントにより、DumpRequestOut
関数がリターンする直前に t.CloseIdleConnections()
が確実に呼び出されます。これにより、ダンプ処理のために一時的に作成された http.Transport
が保持するすべてのアイドルコネクションが閉じられ、それに関連するゴルーチンも適切に終了するようになります。結果として、ゴルーチンリークが防止され、リソースが解放されます。
テストコードの変更も重要です。TestDumpRequest
関数に runtime.NumGoroutine()
を使用したゴルーチン数のチェックが追加されています。これは、テストの開始時と終了時のゴルーチン数を比較し、DumpRequestOut
の呼び出しによって予期せぬ数のゴルーチンが増加していないかを確認するためのものです。これにより、将来的に同様のゴルーチンリークが再発するのを防ぐための回帰テストが確立されました。
コアとなるコードの変更箇所
変更は src/pkg/net/http/httputil/dump.go
と src/pkg/net/http/httputil/dump_test.go
の2つのファイルにわたります。
src/pkg/net/http/httputil/dump.go
--- a/src/pkg/net/http/httputil/dump.go
+++ b/src/pkg/net/http/httputil/dump.go
@@ -107,6 +107,7 @@ func DumpRequestOut(req *http.Request, body bool) ([]byte, error) {
return &dumpConn{io.MultiWriter(&buf, pw), dr}, nil
},
}
+ defer t.CloseIdleConnections()
_, err := t.RoundTrip(reqSend)
src/pkg/net/http/httputil/dump_test.go
--- a/src/pkg/net/http/httputil/dump_test.go
+++ b/src/pkg/net/http/httputil/dump_test.go
@@ -11,6 +11,7 @@ import (
"io/ioutil"
"net/http"
"net/url"
+ "runtime"
"strings"
"testing"
)
@@ -113,6 +114,7 @@ var dumpTests = []dumpTest{
}
func TestDumpRequest(t *testing.T) {
+ numg0 := runtime.NumGoroutine()
for i, tt := range dumpTests {
setBody := func() {
if tt.Body == nil {
@@ -156,6 +158,9 @@ func TestDumpRequest(t *testing.T) {
}
}
}
+ if dg := runtime.NumGoroutine() - numg0; dg > 4 {
+ t.Errorf("Unexpectedly large number of new goroutines: %d new", dg)
+ }
}
func chunk(s string) string {
コアとなるコードの解説
src/pkg/net/http/httputil/dump.go
の変更
追加された行 defer t.CloseIdleConnections()
がこのコミットの核心です。
t
はDumpRequestOut
関数内で一時的に作成されたhttp.Transport
のインスタンスです。CloseIdleConnections()
メソッドは、http.Transport
が現在保持しているアイドル状態の(つまり、使用されていない)すべてのキープアライブコネクションを閉じます。これにより、これらのコネクションに関連付けられたリソース(ソケット、バッファ、そして最も重要なゴルーチン)が解放されます。defer
キーワードは、DumpRequestOut
関数が正常に終了するか、パニックによって終了するかにかかわらず、このCloseIdleConnections()
メソッドが必ず呼び出されることを保証します。これにより、DumpRequestOut
の実行が完了した時点で、一時的なTransport
が残した不要なリソースが確実にクリーンアップされ、ゴルーチンリークが防止されます。
src/pkg/net/http/httputil/dump_test.go
の変更
テストファイルには、ゴルーチンリークを検出するための重要な変更が加えられました。
numg0 := runtime.NumGoroutine()
:TestDumpRequest
の開始時に、現在のシステムで実行中のゴルーチンの総数を取得し、numg0
に保存します。runtime.NumGoroutine()
は、Goランタイムが管理しているすべてのゴルーチンの数を返します。if dg := runtime.NumGoroutine() - numg0; dg > 4 { ... }
:TestDumpRequest
のすべてのテストケースが実行された後、再度runtime.NumGoroutine()
を呼び出し、テスト開始時からのゴルーチン数の増加分dg
を計算します。dg > 4
という条件は、DumpRequestOut
のテスト実行中に、許容範囲を超える(この場合は4つを超える)新しいゴルーチンが作成され、それが終了せずに残っている場合にテストを失敗させるためのものです。なぜ4
という具体的な数値が選ばれたのかは、テストの性質や内部的なゴルーチンの生成パターンに依存しますが、一般的にはテスト実行に必要な最小限のゴルーチン数を超えた場合にリークと判断するための閾値です。これにより、DumpRequestOut
がゴルーチンをリークしていないことを自動的に検証できるようになりました。
これらの変更により、DumpRequestOut
関数はリソースを適切に管理し、ゴルーチンリークを防ぐようになりました。また、テストカバレッジが強化され、将来的な回帰を防ぐための仕組みが導入されました。
関連リンク
- Go言語の
net/http
パッケージ公式ドキュメント: https://pkg.go.dev/net/http - Go言語の
net/http/httputil
パッケージ公式ドキュメント: https://pkg.go.dev/net/http/httputil - Go言語の
runtime
パッケージ公式ドキュメント: https://pkg.go.dev/runtime - Go言語の
defer
ステートメントに関する公式ブログ記事やドキュメント(一般的な情報源)
参考にした情報源リンク
- Go言語の公式ドキュメントおよびソースコード
- Go言語の並行処理とゴルーチンに関する一般的な解説記事
- HTTPプロトコルとコネクション管理に関する一般的な情報
- Go言語のイシュートラッカー(
Fixes #7869
の参照元)