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

[インデックス 17576] ファイルの概要

このコミットは、Go言語の標準ライブラリnetパッケージ内のテストTestDialFailPDLeakの改善に関するものです。具体的には、テストの実行時間を短縮し、特定の環境(Windows/386)でのテストをスキップすることで、開発ワークフローの効率化を図っています。同時に、ランタイムと統合されたネットワークポーラーにおけるメモリリークの検出能力を維持しています。

コミット

commit 81737a9a512cc0a52857d7c9d8137faa6ba7e5c1
Author: Mikio Hara <mikioh.mikioh@gmail.com>
Date:   Thu Sep 12 11:10:25 2013 +0900

    net: make TestDialFailPDLeak shorter

    Reduces a number of trials but it still can detect memory leak
    when we make blunders in runtime-integarted network poller work,
    like just forgetting to call runtime_pollClose in code paths.

    Also disables the test on windows/386.

    R=alex.brainman, r
    CC=golang-dev
    https://golang.org/cl/13022046

GitHub上でのコミットページへのリンク

https://github.com/golang/go/commit/81737a9a512cc0a52857d7c9d8137faa6ba7e5c1

元コミット内容

net: make TestDialFailPDLeak shorter

このコミットは、TestDialFailPDLeakテストを短縮することを目的としています。試行回数を減らすことでテスト時間を短縮しますが、ランタイムと統合されたネットワークポーラーの作業で、runtime_pollCloseの呼び出し忘れのようなミスがあった場合に、メモリリークを検出する能力は維持されます。また、Windows/386環境ではこのテストを無効にします。

変更の背景

TestDialFailPDLeakは、Goのネットワークポーラーが適切にリソースを解放しているか、特に接続試行が失敗した場合にメモリリークが発生しないかを検証するためのテストです。元の実装では、count変数が20000という大きな値に設定されており、多数の失敗するダイヤル試行を繰り返していました。これにより、テストの実行時間が非常に長くなり、特にリソースが限られている環境やCI/CDパイプラインにおいて、開発のボトルネックとなる可能性がありました。

コミットメッセージにあるように、このテストは「ランタイムと統合されたネットワークポーラーの作業で、runtime_pollCloseの呼び出し忘れのようなミスがあった場合に、メモリリークを検出する」ことを目的としています。テストの目的を達成しつつ、実行時間を短縮することが求められました。

また、Windows/386環境ではこのテストが特に時間がかかるため、その環境でのテストをスキップする判断がなされました。これは、特定のプラットフォームでのテストの非効率性を解消し、全体的なテストスイートの実行時間を最適化するためです。

前提知識の解説

Goのネットワークポーラー (Network Poller)

Goのランタイムには、効率的なI/O処理を実現するための「ネットワークポーラー」が組み込まれています。これは、ノンブロッキングI/Oとイベント通知メカニズム(Linuxのepoll、macOS/BSDのkqueue、WindowsのIOCPなど)を利用して、多数のネットワーク接続を同時に処理できるようにするものです。

Goのgoroutineは、I/O操作がブロックされると自動的にスケジューリングされ、他のgoroutineが実行されます。ネットワークポーラーは、I/O操作が完了した際にgoroutineを再開させる役割を担います。これにより、Goは高い並行性と効率的なネットワーク処理を実現しています。

pollDescruntime_pollClose

Goのネットワークポーラーは、各ネットワーク接続(ファイルディスクリプタやソケット)の状態を管理するためにpollDescという内部構造体を使用します。pollDescは、I/Oイベントの登録、待機、通知などの情報を含んでいます。

runtime_pollCloseは、Goランタイム内部で呼び出される関数で、pollDesc構造体に関連付けられたリソースを解放する役割を担います。ネットワーク接続が閉じられたり、ダイヤル試行が失敗したりしてpollDescが不要になった場合、この関数が呼び出されてリソースが適切にクリーンアップされる必要があります。もしruntime_pollCloseの呼び出しが忘れられた場合、pollDescオブジェクトがメモリ上に残り続け、メモリリークを引き起こす可能性があります。

メモリリークの検出

メモリリークは、プログラムが確保したメモリを解放し忘れることで、利用可能なメモリが徐々に減少していく現象です。Goのようなガベージコレクション(GC)を持つ言語でも、GCが到達できない(参照されていない)が、実際には解放されるべきリソース(この場合はpollDesc)が存在する場合にメモリリークが発生することがあります。

TestDialFailPDLeakは、意図的に失敗するネットワークダイヤルを多数実行し、その前後でシステムのメモリ使用量(runtime.MemStatsSysフィールドなど)を比較することで、pollDescのリークがないかを検出します。もしpollDescが適切に解放されていなければ、メモリ使用量が増加し、テストが失敗するようになっています。

testing.Short()

Goのtestingパッケージには、テストの実行時間を制御するためのtesting.Short()関数が提供されています。これは、go test -shortコマンドでテストを実行した場合にtrueを返します。開発者は、時間がかかるテストやリソースを多く消費するテストをtesting.Short()で囲むことで、通常の開発サイクルではスキップし、CI/CD環境やリリース前の完全なテストスイート実行時のみに実行するといった使い分けが可能です。

sync.WaitGroup

sync.WaitGroupは、Goのsyncパッケージが提供する同期プリミティブの一つで、複数のgoroutineの完了を待機するために使用されます。Add(delta int)でカウンタを増やし、Done()でカウンタを減らし、Wait()でカウンタがゼロになるまでブロックします。このコミットでは、多数のダイヤル試行を並行して実行するために使用されています。

技術的詳細

このコミットの主要な変更点は以下の通りです。

  1. Windows/386環境でのテストスキップ: runtime.GOOS == "windows" && runtime.GOARCH == "386"という条件が追加され、もし実行環境がWindowsの32ビットアーキテクチャである場合、テストがスキップされるようになりました。これは、この環境でのテスト実行が非常に時間がかかるためです。t.Skipfを使用することで、テストがスキップされた理由が明確に報告されます。

  2. count変数の削減: 失敗するダイヤル試行の回数を制御するcount変数の値が、20000から500に大幅に削減されました。コミットメッセージには「500はpollcacheのチャンクを使い切るのに十分」とあり、これはruntime/netpoll.goc内のallocPollDesc関数に関連しています。つまり、pollDescのキャッシュメカニズムを十分にテストし、リークを検出するためには500回の試行で十分であるという判断です。これにより、テストの実行時間が劇的に短縮されます。

  3. 並行ダイヤル試行の導入: 元のテストでは、ダイヤル試行がforループ内で逐次的に実行されていました。この変更では、sync.WaitGroupを導入し、各ダイヤル試行を新しいgoroutineで並行して実行するようにしました。

    • var wg sync.WaitGroupWaitGroupを宣言。
    • 各ダイヤル試行の前にwg.Add(1)でカウンタをインクリメント。
    • ダイヤル試行を行う匿名関数をgo func() { ... }()でgoroutineとして起動し、defer wg.Done()で完了時にカウンタをデクリメント。
    • 内側のループの最後にwg.Wait()を呼び出し、すべての並行ダイヤル試行が完了するまで待機します。

    これにより、テストの実行時間が短縮されるだけでなく、並行処理下でのpollDescのリーク検出能力も向上する可能性があります。

コアとなるコードの変更箇所

src/pkg/net/dial_test.goファイルのTestDialFailPDLeak関数が変更されています。

--- a/src/pkg/net/dial_test.go
+++ b/src/pkg/net/dial_test.go
@@ -431,9 +431,15 @@ func TestDialFailPDLeak(t *testing.T) {
  	if testing.Short() {
  		t.Skip("skipping test in short mode")
  	}
+	if runtime.GOOS == "windows" && runtime.GOARCH == "386" {
+		// Just skip the test because it takes too long.
+		t.Skipf("skipping test on %q/%q", runtime.GOOS, runtime.GOARCH)
+	}
  
  	const loops = 10
-	const count = 20000
+	// 500 is enough to turn over the chunk of pollcache.
+	// See allocPollDesc in runtime/netpoll.goc.
+	const count = 500
  	var old runtime.MemStats // used by sysdelta
  	runtime.ReadMemStats(&old)
  	sysdelta := func() uint64 {
@@ -446,13 +452,20 @@ func TestDialFailPDLeak(t *testing.T) {
  	d := &Dialer{Timeout: time.Nanosecond} // don't bother TCP with handshaking
  	failcount := 0
  	for i := 0; i < loops; i++ {
+		var wg sync.WaitGroup
  		for i := 0; i < count; i++ {
-			conn, err := d.Dial("tcp", "127.0.0.1:1")
-			if err == nil {
-				t.Error("dial should not succeed")
-				conn.Close()
-				t.FailNow()
-			}
+			wg.Add(1)
+			go func() {
+				defer wg.Done()
+				if c, err := d.Dial("tcp", "127.0.0.1:1"); err == nil {
+					t.Error("dial should not succeed")
+					c.Close()
+				}
+			}()
+		}
+		wg.Wait()
+		if t.Failed() {
+			t.FailNow()
  		}
  		if delta := sysdelta(); delta > 0 {
  			failcount++

コアとなるコードの解説

1. Windows/386でのスキップロジック

	if runtime.GOOS == "windows" && runtime.GOARCH == "386" {
		// Just skip the test because it takes too long.
		t.Skipf("skipping test on %q/%q", runtime.GOOS, runtime.GOARCH)
	}

runtime.GOOSruntime.GOARCHは、それぞれ現在のオペレーティングシステムとアーキテクチャを示すGoの定数です。このコードは、テストがWindowsの32ビット環境で実行されている場合に、t.Skipfを呼び出してテストをスキップします。t.Skipfは、テストをスキップし、指定されたフォーマット文字列と引数で理由を報告します。これにより、特定の環境でのテストの非効率性が解消されます。

2. count変数の変更

 	const loops = 10
-	const count = 20000
+	// 500 is enough to turn over the chunk of pollcache.
+	// See allocPollDesc in runtime/netpoll.goc.
+	const count = 500

count変数の値が20000から500に減らされました。コメントにあるように、500という値はpollcacheのチャンクを十分に「使い切る」(つまり、pollDescの割り当てと解放のサイクルを十分に発生させる)のに十分であると判断されています。これは、テストの目的であるメモリリークの検出能力を損なうことなく、テスト時間を大幅に短縮するための最適化です。

3. 並行ダイヤル試行の導入

 	for i := 0; i < loops; i++ {
+		var wg sync.WaitGroup
  		for i := 0; i < count; i++ {
-			conn, err := d.Dial("tcp", "127.0.0.1:1")
-			if err == nil {
-				t.Error("dial should not succeed")
-				conn.Close()
-				t.FailNow()
-			}
+			wg.Add(1)
+			go func() {
+				defer wg.Done()
+				if c, err := d.Dial("tcp", "127.0.0.1:1"); err == nil {
+					t.Error("dial should not succeed")
+					c.Close()
+				}
+			}()
+		}
+		wg.Wait()
+		if t.Failed() {
+			t.FailNow()
  		}

この部分が最も大きな変更点です。

  • 内側のループの開始時にvar wg sync.WaitGroupが宣言され、各loopsイテレーションごとに新しいWaitGroupが使用されます。
  • for i := 0; i < count; i++ループ内で、各ダイヤル試行の前にwg.Add(1)が呼び出されます。
  • d.Dialの呼び出しとエラーチェックは、go func() { ... }()という匿名関数内で実行され、新しいgoroutineとして起動されます。
  • 匿名関数の冒頭にはdefer wg.Done()が追加されており、goroutineが終了する際にWaitGroupのカウンタをデクリメントします。これにより、すべてのダイヤル試行goroutineが完了したことがWaitGroupに通知されます。
  • 内側のループの直後にwg.Wait()が呼び出され、count個のすべてのダイヤル試行goroutineが完了するまでメインのテストgoroutineがブロックされます。
  • if t.Failed() { t.FailNow() }は、並行実行されたgoroutine内でt.Errorが呼び出された場合に、メインのテストgoroutineも即座に失敗させるためのものです。t.Errorはテストを失敗としてマークしますが、即座にテストを終了させるわけではないため、t.FailNowで明示的に終了させています。

この並行化により、countが削減されたことと相まって、テストの実行時間が大幅に短縮されます。また、複数のgoroutineが同時にネットワークポーラーのリソースを操作することで、より現実的なシナリオでのメモリリークの検出が可能になります。

関連リンク

  • Goのネットワークポーラーに関する一般的な情報:
    • Goのネットワークプログラミングに関する公式ドキュメントやブログ記事
    • Goのnetパッケージのソースコード
  • sync.WaitGroupに関するGoの公式ドキュメント:
  • testingパッケージに関するGoの公式ドキュメント:

参考にした情報源リンク