[インデックス 16142] ファイルの概要
このコミットは、Go言語の net
パッケージにおいて、runtime.PollDesc
構造体のメモリリークを検出するための新しいテストを追加するものです。特に、net.Dial
の失敗時に runtime.PollDesc
が適切に解放されない問題を特定し、将来的な回帰を防ぐことを目的としています。
コミット
commit 77a0b96f2f779717f801173aacd35f2b2041dd9e
Author: Dave Cheney <dave@cheney.net>
Date: Tue Apr 9 11:14:22 2013 +1000
net: add test for runtime.PollDesc leak
See 8318044
R=bradfitz
CC=golang-dev
https://golang.org/cl/8547043
GitHub上でのコミットページへのリンク
https://github.com/golang/go/commit/77a0b96f2f779717f801173aacd35f2b2041dd9e
元コミット内容
net: add test for runtime.PollDesc leak
See 8318044
このコミットは、net
パッケージに runtime.PollDesc
のリークを検出するためのテストを追加します。コミットメッセージには See 8318044
とあり、これは関連する問題追跡番号または変更リスト(CL)を参照していると考えられます。
変更の背景
Go言語のランタイムには、ネットワークI/O操作を効率的に処理するための「ネットワークポーラー(network poller)」というメカニズムがあります。このポーラーは、ファイルディスクリプタ(FD)やソケットなどのI/Oイベントを監視し、準備ができたときにゴルーチンを再開します。この際、runtime.PollDesc
という内部構造体が使用され、各I/O操作の状態を管理します。
変更の背景には、net.Dial
のようなネットワーク接続試行が失敗した場合に、この runtime.PollDesc
構造体が適切に解放されず、メモリリークを引き起こす可能性があったという問題があります。メモリリークは、アプリケーションが長時間稼働するにつれてメモリ使用量が増加し続け、最終的にはシステムリソースを枯渇させ、パフォーマンスの低下やクラッシュにつながる深刻な問題です。
このコミットは、特定の変更リスト(CL 8318044)で修正される予定のリーク問題に対するテストカバレッジを確保するために追加されました。テストを事前に導入することで、問題が修正されたことを検証し、将来的に同様のリークが再発するのを防ぐ「回帰テスト」としての役割を果たします。
前提知識の解説
runtime.PollDesc
と Goのネットワークポーラー
Goのランタイムは、ノンブロッキングI/Oとイベント駆動型プログラミングを組み合わせることで、多数の同時接続を効率的に処理します。この中心にあるのが「ネットワークポーラー」です。
- ネットワークポーラー: OSの提供するI/O多重化メカニズム(Linuxのepoll、macOS/FreeBSDのkqueue、WindowsのIOCPなど)を利用して、複数のネットワーク接続からのイベント(読み取り可能、書き込み可能など)を効率的に監視します。
runtime.PollDesc
: 各ネットワーク接続(ソケット)に関連付けられる内部的なデータ構造です。これには、その接続が現在どのゴルーチンによって待機されているか、どのようなイベントを待機しているかなどの情報が含まれます。I/O操作が完了すると、PollDesc
は解放されるか、再利用可能な状態に戻される必要があります。- リークのメカニズム:
net.Dial
のような操作が失敗した場合、例えば接続先のポートが開いていない、ネットワークが到達不能であるなどの理由でエラーが発生した際に、PollDesc
が適切にクリーンアップされないと、その構造体がメモリ上に残り続け、メモリリークとなります。これは、ガベージコレクタが到達できない参照によって保持されているか、あるいはポーラーの内部リストから削除されない場合に発生します。
Goにおけるメモリリークの検出
Goはガベージコレクション(GC)を備えていますが、GCは「到達可能なオブジェクト」のみを回収します。したがって、不要になったオブジェクトであっても、どこかから参照されている限りはGCの対象外となり、メモリリークとして扱われます。
メモリリークの検出には、主に以下のツールや手法が用いられます。
runtime.MemStats
: Goのruntime
パッケージが提供する構造体で、現在のメモリ使用状況に関する詳細な統計情報を含みます。ヒープの割り当て量、GCの実行回数、システムから取得したメモリ量(Sys
)などをプログラムから取得できます。このコミットのテストでは、Sys
フィールドの変化を監視することで、システムメモリのリークを間接的に検出しています。pprof
: Goに組み込まれているプロファイリングツールです。CPU使用率、メモリ割り当て、ゴルーチンスタックトレースなどを視覚化できます。メモリリークの疑いがある場合、pprof
のヒーププロファイルやゴルーチンプロファイルを見ることで、どのオブジェクトが保持されているか、どのゴルーチンがブロックされているかなどを特定するのに役立ちます。特に、runtime.PollDesc
のリークは、internal/poll.runtime_pollWait
のようなI/O待機状態のゴルーチンが多数存在することで示唆されることがあります。
testing
パッケージ
Goの標準ライブラリである testing
パッケージは、ユニットテスト、ベンチマークテスト、例(Example)テストを記述するためのフレームワークを提供します。
*testing.T
: テスト関数に渡される構造体で、テストの失敗を報告したり、テストをスキップしたり、ヘルパー関数を呼び出したりするためのメソッドを提供します。t.Skip()
: テストをスキップするために使用されます。このコミットのテストでは、特定の条件(testing.Short()
モードや、関連するCLがまだサブミットされていない場合)でテストをスキップしています。t.Error()
/t.FailNow()
: テストの失敗を報告します。t.FailNow()
は、現在のテストゴルーチンを即座に終了させます。
技術的詳細
このコミットで追加された TestDialPollDescLeak
テストは、net.Dial
が失敗した場合に runtime.PollDesc
構造体がリークしないことを検証します。テストの主要なロジックは以下の通りです。
-
テストのスキップ条件:
t.Skip("Test skipped pending submission of CL 8318044")
: このテストは、関連するバグ修正(CL 8318044)がまだGoのメインブランチにマージされていない間はスキップされるように設定されています。これは、修正が適用される前にテストが失敗するのを防ぐための一時的な措置です。if testing.Short() { t.Skip("skipping PollDesc leak test in -short mode") }
:go test -short
コマンドで実行された場合、テストはスキップされます。これは、このテストが多数のネットワーク接続試行を伴うため、実行に時間がかかる可能性があるためです。
-
メモリ統計の監視:
const loops = 10
とconst count = 20000
: テストは、Dial
試行をloops
回(10回)繰り返し、各ループでcount
回(20000回)のDial
試行を行います。合計で 200,000 回のDial
試行が行われます。var old runtime.MemStats
とruntime.ReadMemStats(&old)
:runtime.MemStats
構造体を使用して、テスト開始時のメモリ統計を記録します。sysdelta := func() uint64 { ... }
:sysdelta
というクロージャが定義されており、これは現在のメモリ統計を読み取り、前回のold.Sys
との差分(システムから取得したメモリの増減)を計算し、old
を更新します。Sys
は、GoランタイムがOSから取得したメモリの総量を示します。この値が増加し続ける場合、メモリリークの兆候となります。
-
リーク検出ロジック:
for i := 0; i < loops; i++
: メインのループ。for i := 0; i < count; i++
: 内部ループで、Dial("tcp", "127.0.0.1:1")
を実行します。127.0.0.1:1
は通常、どのサービスもリッスンしていないポートであるため、このDial
呼び出しは常に失敗することが期待されます。if err == nil { ... }
:Dial
が成功した場合、それは予期せぬ動作であるため、テストはエラーを報告し、即座に終了します。if delta := sysdelta(); delta > 0 { failcount++ }
: 各ループの終わりにsysdelta()
を呼び出し、システムメモリの増加をチェックします。もしdelta
が0より大きい場合(メモリが増加した場合)、failcount
をインクリメントします。if failcount > 3 { ... }
: 最初の数回のループでは、Goランタイムの内部的な初期化やGCの動作により、一時的にメモリが増加することがあります。そのため、failcount
が3回を超えた場合にのみ、メモリリークと判断し、テストを失敗させます。このしきい値は、テストの安定性とリーク検出のバランスを取るために設定されています。
このテストは、大量の失敗するネットワーク接続試行をシミュレートすることで、runtime.PollDesc
のような内部リソースが適切にクリーンアップされているかを、システムメモリ使用量の変化という形で間接的に検証しています。
コアとなるコードの変更箇所
--- a/src/pkg/net/dial_test.go
+++ b/src/pkg/net/dial_test.go
@@ -330,3 +330,45 @@ func numFD() int {
// All tests using this should be skipped anyway, but:
panic("numFDs not implemented on " + runtime.GOOS)
}\n+\n+// Assert that a failed Dial attempt does not leak
+// runtime.PollDesc structures
+func TestDialPollDescLeak(t *testing.T) {\n+\t// remove once CL 8318044 is submitted
+\tt.Skip("Test skipped pending submission of CL 8318044")\n+\n+\tif testing.Short() {\n+\t\tt.Skip("skipping PollDesc leak test in -short mode")\n+\t}\n+\n+\tconst loops = 10\n+\tconst count = 20000\n+\tvar old runtime.MemStats // used by sysdelta
+\truntime.ReadMemStats(&old)\n+\tsysdelta := func() uint64 {\n+\t\tvar new runtime.MemStats
+\t\truntime.ReadMemStats(&new)\n+\t\tdelta := old.Sys - new.Sys
+\t\told = new
+\t\treturn delta
+\t}\n+\tfailcount := 0\n+\tfor i := 0; i < loops; i++ {\n+\t\tfor i := 0; i < count; i++ {\n+\t\t\tconn, err := Dial("tcp", "127.0.0.1:1")\n+\t\t\tif err == nil {\n+\t\t\t\tt.Error("dial should not succeed")\n+\t\t\t\tconn.Close()\n+\t\t\t\tt.FailNow()\n+\t\t\t}\n+\t\t}\n+\t\tif delta := sysdelta(); delta > 0 {\n+\t\t\tfailcount++\n+\t\t}\n+\t\t// there are always some allocations on the first loop
+\t\tif failcount > 3 {\n+\t\t\tt.Error("net.Dial leaked memory")\n+\t\t\t\tt.FailNow()\n+\t\t}\n+\t}\n+}\n
コアとなるコードの解説
追加された TestDialPollDescLeak
関数は、src/pkg/net/dial_test.go
に記述されています。
// Assert that a failed Dial attempt does not leak
// runtime.PollDesc structures
func TestDialPollDescLeak(t *testing.T) {
// remove once CL 8318044 is submitted
t.Skip("Test skipped pending submission of CL 8318044")
if testing.Short() {
t.Skip("skipping PollDesc leak test in -short mode")
}
const loops = 10
const count = 20000
var old runtime.MemStats // used by sysdelta
runtime.ReadMemStats(&old)
sysdelta := func() uint64 {
var new runtime.MemStats
runtime.ReadMemStats(&new)
delta := old.Sys - new.Sys
old = new
return delta
}
failcount := 0
for i := 0; i < loops; i++ {
for i := 0; i < count; i++ {
conn, err := Dial("tcp", "127.0.0.1:1")
if err == nil {
t.Error("dial should not succeed")
conn.Close()
t.FailNow()
}
}
if delta := sysdelta(); delta > 0 {
failcount++
}
// there are always some allocations on the first loop
if failcount > 3 {
t.Error("net.Dial leaked memory")
t.FailNow()
}
}
}
- 関数シグネチャ:
func TestDialPollDescLeak(t *testing.T)
は、Goのテスト関数として標準的な形式です。*testing.T
はテストの状態とユーティリティメソッドを提供します。 - スキップロジック:
t.Skip("Test skipped pending submission of CL 8318044")
: この行は、関連する修正がまだ適用されていない場合にテストが失敗するのを防ぐための一時的な措置です。修正がマージされた後には削除されるべきコメントが添えられています。if testing.Short() { t.Skip(...) }
:go test -short
フラグが指定された場合にテストをスキップします。これは、このテストが比較的長い時間実行される可能性があるため、クイックテストの際には除外されるようにしています。
- 定数:
loops = 10
: メモリリークの検出を繰り返す回数。count = 20000
: 各ループでDial
を試行する回数。これにより、合計で10 * 20000 = 200,000
回のDial
試行が行われ、リークの可能性を顕著にするのに十分な負荷をかけます。
- メモリ統計の追跡:
var old runtime.MemStats
: 前回のメモリ統計を保持するための変数。runtime.ReadMemStats(&old)
: テスト開始時のメモリ統計を読み込みます。sysdelta := func() uint64 { ... }
: この無名関数は、現在のメモリ統計を読み取り、old.Sys
(システムから取得したメモリの総量) との差分を計算します。old
は新しい統計で更新されるため、次の呼び出しでは前回の呼び出しからの差分が得られます。
- リーク検出ループ:
- 外側の
for i := 0; i < loops; i++
ループは、複数回の試行サイクルを実行します。 - 内側の
for i := 0; i < count; i++
ループは、指定された回数だけDial
を試行します。"127.0.0.1:1"
は通常、接続できないアドレスとポートの組み合わせであり、Dial
はエラーを返すことが期待されます。 if err == nil { ... }
: もしDial
が成功してしまった場合(これは予期せぬ動作)、テストはエラーを報告し、t.FailNow()
で即座に終了します。if delta := sysdelta(); delta > 0 { failcount++ }
: 各ループの終わりにsysdelta()
を呼び出し、システムメモリが増加したかどうかを確認します。増加していればfailcount
を増やします。if failcount > 3 { ... }
:failcount
が3を超えた場合、メモリリークが発生していると判断し、テストを失敗させます。最初の数回のループで一時的なメモリ増加が発生する可能性があるため、このしきい値が設けられています。
- 外側の
このテストは、Goのネットワークスタックにおける潜在的なメモリリークを、大量の失敗するネットワーク接続試行を通じて、システムメモリ使用量の変化を監視することで検出する堅牢な方法を提供しています。
関連リンク
- Go Change-list: https://golang.org/cl/8547043
参考にした情報源リンク
- Go issue/CL 8318044 (Web検索結果から推測される関連情報):
runtime.PollDesc
leak golang 8318044 の検索結果から、runtime.PollDesc
のリークは、ネットワークポーラーのディスクリプタが適切に解放されないことによって引き起こされるメモリまたはゴルーチンリークの一種であることが示唆されています。特に、io.ReadCloser
の未クローズや、終了しないゴルーチン、http.Client
の不適切な使用などが原因となることがあります。- Go issue/CL 8318044 (具体的なリンクは不明だが、コミットメッセージから参照されている) (このCLはウェブ検索では直接見つかりませんでしたが、コミットメッセージで参照されています。)
- Goの
pprof
ツールに関する情報: - Goの
net
パッケージに関する情報: - Goの
runtime
パッケージに関する情報: - Goの
testing
パッケージに関する情報: - Stack Overflow: Golang http.Client connection leak (
runtime.PollDesc
リークの一般的な原因に関連する議論) - GitHub Issues:
runtime.PollDesc
やゴルーチンリークに関するGoリポジトリの既存の議論や修正。- Example of a related issue on GitHub (具体的なコミットとは直接関係ないが、同様のリーク問題の例)
- Another related issue on GitHub (具体的なコミットとは直接関係ないが、同様のリーク問題の例)
これらの情報源は、runtime.PollDesc
の概念、Goにおけるメモリリークの検出方法、およびこのコミットが対処しようとしている問題の一般的な背景を理解するのに役立ちます。