[インデックス 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.PollDescleak 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におけるメモリリークの検出方法、およびこのコミットが対処しようとしている問題の一般的な背景を理解するのに役立ちます。