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

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

このコミットは、Goランタイムにおける並行GC(ガベージコレクション)スイープ処理を一時的に無効化するものです。ビルド環境で発生していた不具合(ビルダーの失敗)に対応するため、並行スイープを停止し、代わりにすべてのスパンを即座にスイープする同期的な処理に切り替えています。これは、問題の根本原因を特定し修正するまでの暫定的な措置と考えられます。

コミット

commit 3cac829ff403e229729434a33cb59ae6dfc23209
Author: Dmitriy Vyukov <dvyukov@google.com>
Date:   Thu Feb 13 00:03:27 2014 +0400

    runtime: temporary disable concurrent GC sweep
    We see failures on builders, e.g.:
    http://build.golang.org/log/70bb28cd6bcf8c4f49810a011bb4337a61977bf4
    
    LGTM=rsc, dave
    R=rsc, dave
    CC=golang-codereviews
    https://golang.org/cl/62360043

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

https://github.com/golang/go/commit/3cac829ff403e229729434a33cb59ae6dfc23209

元コミット内容

このコミットの目的は、Goランタイムのガベージコレクション(GC)における並行スイープ(concurrent GC sweep)機能を一時的に無効にすることです。並行スイープは、GCのマークフェーズと並行して、不要になったメモリ領域(スパン)を解放する処理です。これにより、アプリケーションの実行を長時間停止させることなくGCを実行し、レイテンシを低減することが期待されます。

変更の背景

変更の背景には、Goのビルド環境(ビルダー)で発生していた特定の失敗があります。コミットメッセージには、http://build.golang.org/log/70bb28cd6bcf8c4f49810a011bb4337a61977bf4 というログへのリンクが示されており、このリンク先のログには、並行GCスイープが原因で発生したと推測される問題の詳細が含まれている可能性があります。

並行GCスイープは、GCの複雑な部分であり、複数のゴルーチンが同時にメモリの解放や管理を行うため、競合状態やデッドロック、あるいは不正なメモリアクセスなどの問題が発生しやすい特性があります。ビルダーでの失敗は、このような並行処理における潜在的なバグが顕在化したものと考えられます。

このコミットは、問題の根本原因を特定し、修正するまでの間、システム全体の安定性を確保するための緊急的な措置として、並行スイープを一時的に無効化することを決定しました。これにより、ビルドの失敗を回避し、開発の継続性を維持することを目的としています。

前提知識の解説

Goのガベージコレクション (GC)

Goのガベージコレクションは、主に以下のフェーズで構成されます。

  1. マークフェーズ (Mark Phase):

    • GCの開始時に、アプリケーションの実行が一時的に停止(Stop-The-World: STW)されます。
    • 到達可能なオブジェクト(プログラムが参照しているオブジェクト)を特定し、マークします。これは、ルート(スタック、グローバル変数など)からポインタをたどって行われます。
    • Go 1.5以降では、このフェーズの大部分が並行して実行されるようになり、STW時間は大幅に短縮されました。
  2. マークターミネーションフェーズ (Mark Termination Phase):

    • マークフェーズの終了時に再度STWが発生し、マーク処理の最終的な調整が行われます。
  3. スイープフェーズ (Sweep Phase):

    • マークされなかったオブジェクト(到達不可能なオブジェクト、つまり不要なメモリ)を解放し、そのメモリを再利用可能にします。
    • このスイープフェーズは、Go 1.5以前は主にSTW中に実行されていましたが、Go 1.5以降では並行して実行されるようになりました。並行スイープは、アプリケーションの実行と並行してバックグラウンドでメモリを解放するため、アプリケーションのレイテンシを低減します。

並行スイープ (Concurrent Sweep)

並行スイープは、GCのスイープフェーズをアプリケーションの実行と並行して行う技術です。これにより、GCによるアプリケーションの一時停止時間を最小限に抑え、全体的なスループットと応答性を向上させます。

Goのランタイムでは、mheap(メモリヒープ)が管理するmspan(メモリのスパン)という単位でメモリが割り当てられます。スイープフェーズでは、これらのmspanを走査し、マークされていないオブジェクトが含まれるmspanを解放します。並行スイープでは、専用のゴルーチン(bgsweep)がバックグラウンドでこの処理を行います。

src/pkg/runtime/mgc0.c

このファイルは、GoランタイムのガベージコレクションのコアロジックをC言語で実装したものです。Goのランタイムは、パフォーマンスが重要な部分や、OSとのインタラクションが必要な部分でC言語(またはアセンブリ言語)が使用されています。mgc0.cは、GCの初期化、マーク、スイープなどの主要な処理を定義しています。

技術的詳細

このコミットの技術的な変更は、src/pkg/runtime/mgc0.c ファイル内の gc 関数に集中しています。具体的には、並行スイープを起動するための条件分岐が変更されています。

元のコードでは、sweep.g(並行スイープを担当するゴルーチン)がnilであれば新しくゴルーチンを起動し、すでに存在してparked(一時停止)状態であればready(実行可能)状態にする、というロジックがありました。これは、並行スイープゴルーチンを管理し、必要に応じて起動または再開するための標準的なメカニズムです。

このコミットでは、この並行スイープの起動ロジック全体が if(false) という条件分岐で囲まれています。

// Temporary disable concurrent sweep, because we see failures on builders.
if(false) {
    runtime·lock(&gclock);
    if(sweep.g == nil)
        sweep.g = runtime·newproc1(&bgsweepv, nil, 0, 0, runtime·gc);
    else if(sweep.parked) {
        sweep.parked = false;
        runtime·ready(sweep.g);
    }
    runtime·unlock(&gclock);
} else {
    // Sweep all spans eagerly.
    while(runtime·sweepone() != -1)
        gcstats.npausesweep++;
}

if(false) という条件は常に偽であるため、並行スイープを起動するコードブロックは実行されません。代わりに、else ブロック内のコードが実行されます。

else ブロックでは、while(runtime·sweepone() != -1) ループが実行されています。

  • runtime·sweepone() は、ヒープから1つのスパンを選択し、そのスパンをスイープ(不要なオブジェクトを解放)する関数です。スイープが完了すると、そのスパンは再利用可能な状態になります。
  • この関数は、スイープするスパンがなくなると -1 を返します。
  • したがって、while ループは、すべてのスパンがスイープされるまで runtime·sweepone() を繰り返し呼び出します。

この変更により、GCのスイープフェーズは並行してバックグラウンドで実行されるのではなく、gc 関数が呼び出された時点で、すべてのスパンが同期的に(即座に)スイープされるようになります。これは、並行処理による複雑さを回避し、ビルダーでの失敗の原因となっている可能性のある競合状態を一時的に排除するための直接的な方法です。

この変更は、アプリケーションの実行中にGCがより長い時間停止する可能性を意味します。なぜなら、スイープ処理がアプリケーションの実行と並行して行われるのではなく、GCサイクルの一部として同期的に完了するまで待機する必要があるためです。これは、パフォーマンス(特にレイテンシ)に影響を与える可能性がありますが、安定性を優先した暫定的な措置です。

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

diff --git a/src/pkg/runtime/mgc0.c b/src/pkg/runtime/mgc0.c
index 02872759b1..a6dc1d58ae 100644
--- a/src/pkg/runtime/mgc0.c
+++ b/src/pkg/runtime/mgc0.c
@@ -2383,14 +2383,21 @@ gc(struct gc_args *args)\n \tsweep.nspan = runtime·mheap.nspan;\n \tsweep.spanidx = 0;\n \n-\truntime·lock(&gclock);\n-\tif(sweep.g == nil)\n-\t\tsweep.g = runtime·newproc1(&bgsweepv, nil, 0, 0, runtime·gc);\n-\telse if(sweep.parked) {\n-\t\tsweep.parked = false;\n-\t\truntime·ready(sweep.g);\n+\t// Temporary disable concurrent sweep, because we see failures on builders.\n+\tif(false) {\n+\t\truntime·lock(&gclock);\n+\t\tif(sweep.g == nil)\n+\t\t\tsweep.g = runtime·newproc1(&bgsweepv, nil, 0, 0, runtime·gc);\n+\t\telse if(sweep.parked) {\n+\t\t\tsweep.parked = false;\n+\t\t\truntime·ready(sweep.g);\n+\t\t}\n+\t\truntime·unlock(&gclock);\n+\t} else {\n+\t\t// Sweep all spans eagerly.\n+\t\twhile(runtime·sweepone() != -1)\n+\t\t\tgcstats.npausesweep++;\n \t}\n-\truntime·unlock(&gclock);\n \n \truntime·MProf_GC();\n }\n```

## コアとなるコードの解説

変更は `src/pkg/runtime/mgc0.c` ファイルの `gc` 関数内で行われています。

*   **変更前**:
    ```c
    runtime·lock(&gclock);
    if(sweep.g == nil)
        sweep.g = runtime·newproc1(&bgsweepv, nil, 0, 0, runtime·gc);
    else if(sweep.parked) {
        sweep.parked = false;
        runtime·ready(sweep.g);
    }
    runtime·unlock(&gclock);
    ```
    このブロックは、並行スイープゴルーチン (`sweep.g`) のライフサイクルを管理していました。
    *   `runtime·lock(&gclock)` と `runtime·unlock(&gclock)`: `gclock` というミューテックス(ロック)を使用して、並行スイープゴルーチンの状態変更に対する排他制御を行っています。これは、複数のゴルーチンが同時に `sweep.g` の状態を変更しようとするのを防ぐためです。
    *   `if(sweep.g == nil)`: 並行スイープゴルーチンがまだ作成されていない場合、`runtime·newproc1` を呼び出して新しいゴルーチン (`bgsweepv` 関数を実行する) を作成し、`sweep.g` に割り当てます。
    *   `else if(sweep.parked)`: 並行スイープゴルーチンがすでに存在し、かつ `parked`(一時停止)状態である場合、`sweep.parked = false;` で状態を解除し、`runtime·ready(sweep.g);` でゴルーチンを実行可能状態に戻します。これにより、GCサイクルが開始されるたびに、既存の並行スイープゴルーチンが再開されます。

*   **変更後**:
    ```c
    // Temporary disable concurrent sweep, because we see failures on builders.
    if(false) {
        runtime·lock(&gclock);
        if(sweep.g == nil)
            sweep.g = runtime·newproc1(&bgsweepv, nil, 0, 0, runtime·gc);
        else if(sweep.parked) {
            sweep.parked = false;
            runtime·ready(sweep.g);
        }
        runtime·unlock(&gclock);
    } else {
        // Sweep all spans eagerly.
        while(runtime·sweepone() != -1)
            gcstats.npausesweep++;
    }
    ```
    *   `if(false)`: この条件式により、変更前の並行スイープゴルーチンを管理するコードブロック全体が実行されなくなります。これは、並行スイープ機能を完全にバイパスするための最も単純な方法です。
    *   `else` ブロック: `if(false)` が常に偽であるため、この `else` ブロックが常に実行されます。
        *   `// Sweep all spans eagerly.` というコメントは、このブロックの目的がすべてのスパンを「即座に(eagerly)」スイープすることであることを示しています。
        *   `while(runtime·sweepone() != -1)`: `runtime·sweepone()` 関数は、ヒープから1つのスパンを選んでスイープ処理を実行します。このループは、`runtime·sweepone()` が `-1` を返すまで(つまり、スイープすべきスパンがなくなるまで)繰り返し実行されます。これにより、GCサイクル中にすべての不要なスパンが同期的に解放されます。
        *   `gcstats.npausesweep++`: スイープ処理が行われるたびに、GC統計の `npausesweep` カウンタが増加します。これは、GCが一時停止してスイープを実行した回数を記録するためのものです。

この変更は、並行スイープの複雑なロジックを一時的に無効にし、より単純で予測可能な同期スイープにフォールバックすることで、ビルダーでの不安定性を解消することを目的としています。これは、問題の根本原因が特定され、修正されるまでの間、システムの安定性を確保するための暫定的な解決策です。

## 関連リンク

*   Goのガベージコレクションに関する公式ドキュメントやブログ記事:
    *   [Go's Garbage Collector: From 1.3 to 1.5](https://blog.golang.org/go15gc) (Go 1.5でのGCの進化について解説されていますが、並行スイープの概念を理解するのに役立ちます)
    *   [Go's new GC: less latency, more throughput](https://blog.golang.org/go-gc-from-1.3-to-1.5) (上記と同様、Go 1.5のGCに関する記事)

## 参考にした情報源リンク

*   コミットメッセージに記載されているビルドログ: `http://build.golang.org/log/70bb28cd6bcf8c4f49810a011bb4337a61977bf4` (ただし、このリンクは古い可能性があり、アクセスできない場合があります。)
*   Goのソースコード: `src/pkg/runtime/mgc0.c` (Goのバージョンによって内容は異なりますが、GCのコアロジックが記述されています。)
*   Goのガベージコレクションに関する一般的な知識。