[インデックス 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 large
(net
: 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とイベント駆動型モデルに基づいて効率的に設計されています。
netpoll
とPollDesc
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.goc
のallocPollDesc
: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つの主要な変更が加えられています。
-
loops
変数の動的な設定:- 変更前:
const loops = 10
- 変更後:
maxprocs := runtime.GOMAXPROCS(0) loops := 10 + maxprocs
runtime.GOMAXPROCS(0)
を呼び出すことで、現在のGOMAXPROCS
の値を取得します。そして、テストのループ回数loops
を、固定値10
にmaxprocs
の値を加算した動的な値に変更しています。これにより、GOMAXPROCS
が大きい環境では、テストの反復回数が増加し、より多くの並行操作が実行される機会が与えられます。これは、並行性の高い状況下でのリソースの割り当てと解放のパターンをより正確にシミュレートするためです。
- 変更前:
-
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
が多く存在する可能性があるため、その「ノイズ」を許容し、真のリークのみを検出するための調整です。
- 変更前:
これらの変更により、TestDialFailPDLeak
はGOMAXPROCS
の値に依存せず、より堅牢かつ正確にメモリリークを検出できるようになりました。
関連リンク
参考にした情報源リンク
- コミットメッセージ自体
- Go言語の
net
パッケージおよびランタイムの一般的な知識 GOMAXPROCS
に関するGoのドキュメントと解説- GoのネットワークI/O(
netpoll
、PollDesc
)に関する一般的な情報 - Goのテストにおけるメモリリーク検出の一般的なアプローチ
- Go Issue #6553 (コミットメッセージに記載されているが、公開されているGoリポジトリでは直接参照できなかったため、コミットメッセージからその内容を推測)