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

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

このコミットは、Go言語のnetパッケージ内のテストTestDialFailPDLeakが、GOMAXPROCSの値が大きい環境で正しく動作しない問題を修正するものです。具体的には、GOMAXPROCSが設定された際に発生する可能性のある偽陽性のメモリリーク検出を回避するために、テストのループ回数と失敗の許容回数を調整しています。

コミット

commit 649fc255a9c7b6e05249dbde1176aecd17135cc3
Author: Ian Lance Taylor <iant@golang.org>
Date:   Wed Oct 9 13:52:29 2013 -0700

    net: fix TestDialFailPDLeak to work when GOMAXPROCS is large
    
    Fixes #6553.
    
    R=golang-dev, mikioh.mikioh
    CC=golang-dev
    https://golang.org/cl/14526048

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

https://github.com/golang/go/commit/649fc255a9c7b6e05249dbde1176aecd17135cc3

元コミット内容

net: fix TestDialFailPDLeak to work when GOMAXPROCS is largenet: GOMAXPROCSが大きい場合にTestDialFailPDLeakが動作するように修正)

このコミットは、GoのnetパッケージにあるTestDialFailPDLeakというテストが、GOMAXPROCS環境変数が大きな値に設定されている場合に、誤ってメモリリークを検出してしまう問題を修正します。この問題はGoのIssue #6553として報告されていました。

変更の背景

TestDialFailPDLeakは、GoのネットワークI/O処理において、PollDescという内部リソースが適切に解放されているか、つまりメモリリークが発生していないかを検証するためのテストです。このテストは、多数のネットワーク接続試行と失敗を繰り返すことで、PollDescの割り当てと解放のサイクルをシミュレートし、最終的に残存するPollDescの数をチェックします。

問題は、GOMAXPROCSが大きな値に設定されている環境で発生しました。GOMAXPROCSはGoランタイムが同時に実行できるOSスレッドの最大数を制御します。この値が大きいと、より多くのゴルーチンが並行して実行される可能性があり、結果としてPollDescのような内部リソースの割り当てと解放のタイミングが複雑になります。

テストが固定のループ回数と固定の失敗許容回数で設計されていたため、GOMAXPROCSが大きい環境では、テストが終了するまでにまだ解放されていないPollDescが一時的に多く存在することがあり、これが誤ってメモリリークとして報告されてしまう偽陽性の問題を引き起こしていました。このコミットは、この偽陽性を排除し、テストがGOMAXPROCSの値に関わらず安定して動作するようにするためのものです。

前提知識の解説

GOMAXPROCS

GOMAXPROCSはGoランタイムの環境変数であり、Goスケジューラが同時に実行できるOSスレッド(M: Machine)の最大数を設定します。Goのプログラムはゴルーチン(G: Goroutine)と呼ばれる軽量な並行処理単位で動作し、これらのゴルーチンはGoスケジューラによってOSスレッドにマッピングされ、実行されます。

  • GOMAXPROCSの役割: GOMAXPROCSは、Goプログラムが利用できるCPUコアの数を制限するようなものです。例えば、GOMAXPROCS=1に設定すると、Goスケジューラは同時に1つのOSスレッドしか使用せず、すべてのゴルーチンはそのスレッド上で多重化されて実行されます。デフォルトでは、GOMAXPROCSは利用可能なCPUコア数に設定されます。
  • 並行性と並列性: GOMAXPROCSは並列性(実際に同時に実行されるゴルーチンの数)に影響を与えます。値が大きいほど、より多くのゴルーチンが真に並列に実行される可能性が高まります。
  • テストへの影響: GOMAXPROCSが大きいと、テスト中に同時に実行されるネットワーク操作の数が増え、リソースの割り当てと解放のタイミングがより複雑になります。

netパッケージとネットワークI/O

Goの標準ライブラリのnetパッケージは、TCP/IP、UDP、Unixドメインソケットなどのネットワークプログラミング機能を提供します。GoのネットワークI/Oは、ノンブロッキングI/Oとイベント駆動型モデルに基づいて効率的に設計されています。

netpollPollDesc

Goランタイムは、効率的なネットワークI/Oのためにnetpollというメカニズムを使用しています。これは、OSの提供するI/O多重化機能(Linuxのepoll、macOS/FreeBSDのkqueue、WindowsのIOCPなど)を抽象化したものです。

  • PollDesc: PollDesc(Polling Descriptor)は、Goランタイム内部でネットワークファイルディスクリプタ(ソケット)の状態を管理するために使用されるデータ構造です。各ネットワーク接続やリスナーは、対応するPollDescを持ちます。PollDescは、I/Oイベントの登録、待機、および完了通知を処理し、ゴルーチンをブロックせずにネットワーク操作を実行できるようにします。
  • runtime/netpoll.gocallocPollDesc: runtime/netpoll.gocは、Goランタイムのネットワークポーリングに関連するCコードが含まれるファイルです。allocPollDescは、新しいPollDesc構造体を割り当てるための内部関数です。ネットワーク接続が確立される際などに呼び出されます。
  • メモリリーク検出: TestDialFailPDLeakのようなテストは、ネットワーク操作が完了した後にPollDescが適切に解放されていることを確認することで、ランタイムレベルでのメモリリークがないかを検証します。もしPollDescが不要になった後も解放されずに残存している場合、それはメモリリークの兆候となります。

メモリリークの検出方法(テストにおける)

この種のテストでは、特定の操作(ここではネットワーク接続の試行と失敗)を多数回繰り返します。操作の前後で、特定の種類のオブジェクト(ここではPollDesc)の割り当て数を監視します。理想的には、操作が完了すれば、割り当てられたオブジェクトはすべて解放され、その数は元のレベルに戻るか、ごくわずかな差に収まるはずです。

テストは、残存するオブジェクトの数が許容範囲を超えた場合に「リークを検出した」と判断します。しかし、並行性の高い環境では、ガベージコレクションのタイミングや、まだ処理中のゴルーチンが存在するために、一時的にオブジェクトの数が多く見えることがあります。これが偽陽性の原因となります。

技術的詳細

TestDialFailPDLeakは、存在しないアドレスへのダイヤルを繰り返し試行することで、ネットワーク接続の失敗を意図的に引き起こします。この失敗の過程で、Goランタイムは内部的にPollDescを割り当てますが、接続が失敗した際にはこれらを適切に解放する必要があります。テストは、この割り当てと解放のサイクルが正しく行われているかを検証します。

元のテストでは、loopsという定数(10)と、failcountの許容値(3)がハードコードされていました。

	const loops = 10
	// ...
	if failcount > 3 {
		t.Error("detected possible memory leak in runtime")
		t.FailNow()
	}

GOMAXPROCSが小さい(例えばデフォルトの1)場合、ネットワーク操作は比較的逐次的に実行されるため、PollDescの割り当てと解放も予測可能なパターンで行われます。しかし、GOMAXPROCSが大きくなると、より多くのゴルーチンが並行してネットワーク操作を試行します。これにより、テストがfailcountをチェックする時点で、まだ完了していない(つまりPollDescが解放されていない)ネットワーク操作が多数存在する可能性が高まります。

例えば、GOMAXPROCSが8の場合、同時に8つのOSスレッドが動作し、それぞれがネットワーク操作に関連するゴルーチンを実行しているかもしれません。この状況下で、固定のfailcount > 3という閾値では、一時的に存在するPollDescの数がこの閾値を超えてしまい、実際にはリークではないにもかかわらず、テストが失敗する原因となっていました。

このコミットの修正は、この並行性の影響を考慮に入れています。loopsの数をGOMAXPROCSに比例して増やし、failcountの閾値もGOMAXPROCSに比例して調整することで、テストがより多くの並行操作を許容し、偽陽性を減らすようにしています。これにより、テストはGOMAXPROCSの値に関わらず、真のメモリリークのみを検出できるようになります。

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

--- a/src/pkg/net/dial_test.go
+++ b/src/pkg/net/dial_test.go
@@ -436,7 +436,8 @@ func TestDialFailPDLeak(t *testing.T) {
 		t.Skipf("skipping test on %q/%q", runtime.GOOS, runtime.GOARCH)
 	}
 
-	const loops = 10
+	maxprocs := runtime.GOMAXPROCS(0)
+	loops := 10 + maxprocs
 	// 500 is enough to turn over the chunk of pollcache.
 	// See allocPollDesc in runtime/netpoll.goc.
 	const count = 500
@@ -471,7 +472,7 @@ func TestDialFailPDLeak(t *testing.T) {
 			failcount++
 		}
 		// there are always some allocations on the first loop
-		if failcount > 3 {
+		if failcount > maxprocs+2 {
 			t.Error("detected possible memory leak in runtime")
 			t.FailNow()
 		}

コアとなるコードの解説

このコミットでは、src/pkg/net/dial_test.goファイル内のTestDialFailPDLeak関数に2つの主要な変更が加えられています。

  1. loops変数の動的な設定:

    • 変更前: const loops = 10
    • 変更後:
      maxprocs := runtime.GOMAXPROCS(0)
      loops := 10 + maxprocs
      
      runtime.GOMAXPROCS(0)を呼び出すことで、現在のGOMAXPROCSの値を取得します。そして、テストのループ回数loopsを、固定値10maxprocsの値を加算した動的な値に変更しています。これにより、GOMAXPROCSが大きい環境では、テストの反復回数が増加し、より多くの並行操作が実行される機会が与えられます。これは、並行性の高い状況下でのリソースの割り当てと解放のパターンをより正確にシミュレートするためです。
  2. failcountの閾値の動的な設定:

    • 変更前: if failcount > 3 { ... }
    • 変更後: if failcount > maxprocs+2 { ... } メモリリークを検出するためのfailcount(失敗としてカウントされるPollDescの残存数)の閾値も動的に変更されています。以前は固定値3でしたが、maxprocs+2という式に変更されました。 この変更により、GOMAXPROCSの値が大きくなるにつれて、許容されるfailcountの閾値も増加します。例えば、GOMAXPROCSが1の場合は1+2=3となり以前と同じですが、GOMAXPROCSが8の場合は8+2=10となり、より多くの残存PollDescが許容されるようになります。これは、並行性が高い環境では、テストがチェックする瞬間に一時的に解放されていないPollDescが多く存在する可能性があるため、その「ノイズ」を許容し、真のリークのみを検出するための調整です。

これらの変更により、TestDialFailPDLeakGOMAXPROCSの値に依存せず、より堅牢かつ正確にメモリリークを検出できるようになりました。

関連リンク

参考にした情報源リンク

  • コミットメッセージ自体
  • Go言語のnetパッケージおよびランタイムの一般的な知識
  • GOMAXPROCSに関するGoのドキュメントと解説
  • GoのネットワークI/O(netpollPollDesc)に関する一般的な情報
  • Goのテストにおけるメモリリーク検出の一般的なアプローチ
  • Go Issue #6553 (コミットメッセージに記載されているが、公開されているGoリポジトリでは直接参照できなかったため、コミットメッセージからその内容を推測)