Keyboard shortcuts

Press or to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

[インデックス 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 構造体がリークしないことを検証します。テストの主要なロジックは以下の通りです。

  1. テストのスキップ条件:

    • 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 コマンドで実行された場合、テストはスキップされます。これは、このテストが多数のネットワーク接続試行を伴うため、実行に時間がかかる可能性があるためです。
  2. メモリ統計の監視:

    • const loops = 10const count = 20000: テストは、Dial 試行を loops 回(10回)繰り返し、各ループで count 回(20000回)の Dial 試行を行います。合計で 200,000 回の Dial 試行が行われます。
    • var old runtime.MemStatsruntime.ReadMemStats(&old): runtime.MemStats 構造体を使用して、テスト開始時のメモリ統計を記録します。
    • sysdelta := func() uint64 { ... }: sysdelta というクロージャが定義されており、これは現在のメモリ統計を読み取り、前回の old.Sys との差分(システムから取得したメモリの増減)を計算し、old を更新します。Sys は、GoランタイムがOSから取得したメモリの総量を示します。この値が増加し続ける場合、メモリリークの兆候となります。
  3. リーク検出ロジック:

    • 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

参考にした情報源リンク

これらの情報源は、runtime.PollDesc の概念、Goにおけるメモリリークの検出方法、およびこのコミットが対処しようとしている問題の一般的な背景を理解するのに役立ちます。