[インデックス 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の参照元)