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

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

このコミットは、GoランタイムのTestStackMemというテストが不安定(flaky)である問題を修正するものです。具体的には、以前のテスト実行で残された「死んだGoroutine(dead G's)」がスタックセグメントを消費し続けることが原因で、テストが誤って失敗する可能性があったため、この問題を解決しています。

コミット

commit 7f070af515b40fd1e7f1576b2327779df56fb782
Author: Dmitriy Vyukov <dvyukov@google.com>
Date:   Tue Mar 12 15:19:06 2013 +0400

    runtime: deflake TestStackMem
    The problem is that there are lots of dead G's from previous tests,
    each dead G consumes 1 stack segment.
    Fixes #5034.
    
    R=golang-dev, bradfitz
    CC=golang-dev
    https://golang.org/cl/7749043

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

https://github.com/golang/go/commit/7f070af515b40fd1e7f1576b2327779df56fb782

元コミット内容

runtime: deflake TestStackMem
The problem is that there are lots of dead G's from previous tests,
each dead G consumes 1 stack segment.
Fixes #5034.

変更の背景

このコミットの主な背景は、Goのランタイムテストスイートに含まれるTestStackMemというテストが、実行環境やタイミングによって不安定に失敗する「flaky test」であったことです。コミットメッセージによると、この不安定さの原因は「以前のテストから残された多数の死んだGoroutine(dead G's)が、それぞれ1つのスタックセグメントを消費し続ける」ことにありました。

Goのテストは通常、独立して実行されるべきですが、ランタイムレベルのテストでは、システム全体の状態(特にメモリ管理やGoroutineの状態)が前のテスト実行の影響を受けることがあります。TestStackMemはスタックメモリの使用状況を検証するテストであり、もし前のテストで大量のGoroutineが生成され、それらが終了した後もスタックセグメントがすぐに解放されずに残っている場合、TestStackMemが期待するスタックメモリ使用量よりも大きな値を見てしまい、誤ってテストが失敗する可能性がありました。

この問題はIssue #5034として報告されており、このコミットはその修正を目的としています。テストの不安定さはCI/CDパイプラインにおいて大きな問題となります。なぜなら、コード自体に問題がないにもかかわらずテストが失敗するため、開発者はその失敗が本当のバグによるものなのか、それとも単なるテストの不安定さによるものなのかを判断するのに時間を費やすことになるからです。このような不安定なテストは、開発者の生産性を低下させ、信頼性を損なうため、修正が強く求められます。

前提知識の解説

Goroutine (G)

Go言語におけるGoroutineは、軽量な並行実行単位です。OSのスレッドよりもはるかに軽量であり、数百万のGoroutineを同時に実行することも可能です。GoroutineはGoランタイムによって管理され、必要に応じてOSスレッドにマッピングされます。各Goroutineは独自のスタックを持ち、関数呼び出しやローカル変数の保存に使用されます。

スタックメモリとスタックセグメント

Goroutineのスタックは、最初は小さなサイズ(例えば数KB)で割り当てられます。関数呼び出しが深くなったり、大きなローカル変数が使用されたりしてスタックが不足しそうになると、Goランタイムは自動的にスタックを拡張します。この拡張は、より大きなスタックセグメントを割り当て、既存のスタックの内容を新しいセグメントにコピーすることで行われます。スタックは通常、連続したメモリ領域として扱われますが、Goのスタックはセグメント化されており、必要に応じて複数のセグメントにまたがって存在することができます。

「死んだGoroutine(dead G's)」が「スタックセグメントを消費する」とは、Goroutineがその実行を完了し、論理的には「死んだ」状態になったとしても、そのGoroutineが使用していたスタックメモリ(スタックセグメント)がすぐにOSに返却されず、Goランタイムのメモリプール内に残っている状態を指します。これは、メモリの再利用効率を高めるためのランタイムの最適化戦略の一部である場合がありますが、テストの文脈では、予期せぬメモリ使用量として現れることがあります。

t.Fatalft.Logf

Goの標準テストパッケージtestingで提供される関数です。

  • t.Fatalf(format string, args ...interface{}): テストを失敗としてマークし、指定されたフォーマット文字列と引数でログメッセージを出力し、現在のテストの実行を停止します。これは、テストが続行できない致命的なエラーが発生した場合に使用されます。
  • t.Logf(format string, args ...interface{}): 指定されたフォーマット文字列と引数でログメッセージを出力します。これはテストが成功した場合でも表示され、デバッグ情報やテストの進行状況を示すために使用されます。

Flaky Test (不安定なテスト)

Flaky testとは、コードの変更がないにもかかわらず、実行するたびに成功したり失敗したりするテストのことです。これは、テストが外部要因(ネットワークの遅延、並行処理のタイミング、リソースの競合、環境の状態など)に依存している場合に発生しやすいです。Flaky testはCI/CDパイプラインの信頼性を損ない、開発者の生産性を低下させるため、修正が非常に重要視されます。

技術的詳細

TestStackMemは、Goランタイムがスタックメモリをどのように管理しているかを検証するテストです。このテストは、特定の操作を行った後のスタックメモリの使用量を測定し、それが期待される範囲内にあるかどうかを確認します。

元のコードでは、s1.StackInuseという値が直接4<<20(4MB)と比較されていました。s1.StackInuseは、テスト実行終了時点でのGoランタイムが使用しているスタックメモリの総量を示します。

// Before
if s1.StackInuse > 4<<20 {
    t.Fatalf("Stack inuse: want %v, got %v", 4<<20, s1.StackInuse)
}

問題は、このs1.StackInuseの値が、現在のTestStackMemの実行によって消費されたスタックメモリだけでなく、以前のテスト実行で生成され、まだ解放されていない「死んだGoroutine」が消費しているスタックメモリも含む点にありました。これにより、TestStackMem自体が消費したメモリは少なくても、前のテストの影響でs1.StackInuse4MBを超えてしまい、テストが誤って失敗することがありました。

このコミットによる修正は、この問題を解決するために、テスト開始時と終了時のスタックメモリ使用量の差分を計算するように変更しました。

// After
inuse := s1.StackInuse - s0.StackInuse
t.Logf("Inuse %vMB for stack mem", inuse>>20)
if inuse > 4<<20 {
    t.Fatalf("Stack inuse: want %v, got %v", 4<<20, inuse)
}

ここで、s0.StackInuseTestStackMemが実行される直前のスタックメモリ使用量を示します。s1.StackInuse - s0.StackInuseとすることで、TestStackMemの実行中に実際に増加したスタックメモリ量のみを測定することができます。これにより、以前のテストが残した「死んだGoroutine」によるスタックメモリの消費が、現在のテストの評価に影響を与えることがなくなります。

t.Logf("Inuse %vMB for stack mem", inuse>>20)という行は、デバッグ目的で、テスト中に実際に使用されたスタックメモリ量をログに出力するためのものです。これは、テストが失敗した際に、どの程度のメモリが使用されたのかを把握するのに役立ちます。

この変更により、TestStackMemはより安定し、そのテストが本当に測定すべき「現在のテスト実行によるスタックメモリ使用量」を正確に評価できるようになりました。

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

--- a/src/pkg/runtime/stack_test.go
+++ b/src/pkg/runtime/stack_test.go
@@ -1576,7 +1576,9 @@ func TestStackMem(t *testing.T) {
 	if consumed > estimate {
 		t.Fatalf("Stack mem: want %v, got %v", estimate, consumed)
 	}
-	if s1.StackInuse > 4<<20 {
-		t.Fatalf("Stack inuse: want %v, got %v", 4<<20, s1.StackInuse)
+	inuse := s1.StackInuse - s0.StackInuse
+	t.Logf("Inuse %vMB for stack mem", inuse>>20)
+	if inuse > 4<<20 {
+		t.Fatalf("Stack inuse: want %v, got %v", 4<<20, inuse)
 	}
 }

コアとなるコードの解説

変更はsrc/pkg/runtime/stack_test.goファイルのTestStackMem関数内で行われています。

  1. 変更前:

    if s1.StackInuse > 4<<20 {
        t.Fatalf("Stack inuse: want %v, got %v", 4<<20, s1.StackInuse)
    }
    

    ここでは、テスト終了時の総スタックメモリ使用量s1.StackInuseが直接4MB4<<20は4を20ビット左シフト、つまり4 * 2^20 = 4 * 1024 * 1024 = 4MB)を超えていないかをチェックしていました。このs1.StackInuseには、現在のテストで消費されたメモリだけでなく、以前のテストで生成されたGoroutineが使用していたがまだ解放されていないスタックメモリも含まれていました。

  2. 変更後:

    inuse := s1.StackInuse - s0.StackInuse
    t.Logf("Inuse %vMB for stack mem", inuse>>20)
    if inuse > 4<<20 {
        t.Fatalf("Stack inuse: want %v, got %v", 4<<20, inuse)
    }
    
    • inuse := s1.StackInuse - s0.StackInuse: この行が最も重要な変更です。s0.StackInuseTestStackMem関数が開始される直前のスタックメモリ使用量を示します。s1.StackInuseからs0.StackInuseを引くことで、TestStackMemの実行中に実際に増加したスタックメモリ量、つまりこのテスト自体が消費した純粋なスタックメモリ量を正確に計算しています。
    • t.Logf("Inuse %vMB for stack mem", inuse>>20): これはデバッグ用のログ出力です。計算されたinuse値をMB単位で表示し、テストの実行中にどれくらいのスタックメモリが使用されたかを開発者が確認できるようにします。
    • if inuse > 4<<20: 最後に、計算された純粋なスタックメモリ使用量inuse4MBを超えていないかをチェックします。これにより、テストは他のテストの影響を受けずに、自身のメモリ使用量のみを評価できるようになり、不安定さが解消されます。

この修正により、TestStackMemはより堅牢になり、Goランタイムのスタックメモリ管理の正確なテストが可能になりました。

関連リンク

参考にした情報源リンク

  • Go言語の公式ドキュメント (Goroutine, Stack Memoryに関する一般的な情報)
  • Go言語のテストに関するドキュメント (testingパッケージ)
  • Go言語のソースコード (特にruntimeパッケージのテストファイル)
  • Go言語のIssueトラッカー (Issue #5034の詳細)
  • Go言語のコードレビューシステム (CL 7749043の詳細)
  • Stack Overflowや技術ブログ (Goのスタック管理、flaky testに関する一般的な情報)