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

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

このコミットは、Goランタイムにヒープダンプ機能を導入するものです。具体的には、runtime/debug.WriteHeapDump 関数が追加され、Goプログラムの実行中にヒープの状態をファイルに書き出すことができるようになりました。これにより、メモリリークのデバッグや、Goプログラムのメモリ使用状況の詳細な分析が可能になります。

コミット

commit fff63c2448109cb5a889f99dc2ab330f31d53213
Author: Keith Randall <khr@golang.org>
Date:   Tue Mar 25 15:09:49 2014 -0700

    runtime: WriteHeapDump dumps the heap to a file.
    
    See http://golang.org/s/go13heapdump for the file format.
    
    LGTM=rsc
    R=rsc, bradfitz, dvyukov, khr
    CC=golang-codereviews
    https://golang.org/cl/37540043

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

https://github.com/golang/go/commit/fff63c2448109cb5a889f99dc2ab330f31d53213

元コミット内容

runtime: WriteHeapDump dumps the heap to a file.

See http://golang.org/s/go13heapdump for the file format.

変更の背景

このコミットの主な背景は、Goプログラムのデバッグとプロファイリング能力の向上にあります。特に、メモリリークの特定や、アプリケーションが実行時にどのようにメモリを使用しているかを理解することは、複雑なシステムを開発する上で不可欠です。

Goのガベージコレクタは自動的にメモリ管理を行いますが、それでもなお、プログラマが意図しない参照を保持してしまい、メモリが解放されない「メモリリーク」が発生する可能性があります。従来のデバッグ手法では、このような問題の根本原因を特定するのが困難でした。

ヒープダンプ機能は、特定の時点でのヒープ全体の「スナップショット」を提供します。このスナップショットには、ヒープ上に存在するすべてのオブジェクト、それらの型、サイズ、そしてオブジェクト間の参照関係が含まれます。これにより、開発者はヒープダンプを分析ツール(例えば、Goのpprofツールなど)で読み込むことで、どのオブジェクトがメモリを消費しているのか、なぜそれらが解放されないのかといった情報を視覚的に、あるいは詳細なデータとして把握できるようになります。

コミットメッセージに記載されている http://golang.org/s/go13heapdump は、Go 1.3で導入されたヒープダンプのファイルフォーマットを定義するドキュメントへのリンクです。このフォーマットは、ヒープダンプがどのように構造化され、どのような情報が含まれるかを規定しており、ツールがヒープダンプファイルを正確に解析するために不可欠なものです。このコミットは、この新しいヒープダンプフォーマットに準拠したダンプ生成機能の実装を担っています。

前提知識の解説

このコミットの理解を深めるためには、以下のGoランタイムとメモリ管理に関する基本的な知識が必要です。

  • Goのメモリ管理とガベージコレクション (GC): Goは自動メモリ管理を採用しており、ガベージコレクタが不要になったメモリを自動的に解放します。GoのGCは、並行マーク&スイープ方式をベースとしており、プログラムの実行と並行して動作することで、GCによる一時停止(ストップ・ザ・ワールド)を最小限に抑えるように設計されています。ヒープダンプは、このGCが管理するヒープ領域の状態を可視化する手段です。
  • ヒープ (Heap): プログラムが動的に確保するメモリ領域です。Goでは、makenewで作成されるオブジェクト、関数呼び出しでエスケープ解析によってヒープに割り当てられる変数などがヒープに配置されます。
  • スタック (Stack): 関数呼び出しごとに作成されるメモリ領域で、ローカル変数や関数の引数、戻りアドレスなどが格納されます。Goのスタックは動的に伸縮します。
  • Goroutine: Goの軽量な並行処理単位です。各Goroutineは独自のスタックを持ち、GoスケジューラによってOSスレッド上で実行されます。ヒープダンプには、各Goroutineのスタック情報も含まれ、どのGoroutineがどのオブジェクトを参照しているかを追跡するのに役立ちます。
  • インターフェース (Interface): Goのインターフェースは、メソッドのセットを定義する型です。インターフェース型の変数には、そのインターフェースを実装する任意の具象型の値が格納できます。内部的には、インターフェース値は「型情報」と「データ」の2つのポインタで構成されます。ヒープダンプでは、これらのインターフェース値が参照する具象オブジェクトも追跡されます。
  • itab (Interface Table): Goランタイムがインターフェースの動的ディスパッチを効率的に行うために使用する内部データ構造です。特定の具象型が特定のインターフェースを実装していることを記録します。ヒープダンプにはitabの情報も含まれ、インターフェースの利用状況を把握するのに役立ちます。
  • ファイナライザ (Finalizer): オブジェクトがガベージコレクトされる直前に実行される関数です。リソースの解放など、クリーンアップ処理に使用されます。ヒープダンプには、登録されているファイナライザの情報も含まれます。
  • メモリ統計 (MemStats): Goランタイムが提供するメモリ使用状況に関する統計情報です。ヒープダンプには、ダンプ時点での詳細なメモリ統計も含まれます。
  • ポインタマップ (Pointer Map): Goのガベージコレクタがヒープ上のオブジェクトをスキャンする際に、どのメモリ位置がポインタであるかを識別するために使用する情報です。これにより、GCはポインタをたどって到達可能なオブジェクトをマークし、到達不能なオブジェクトを解放します。ヒープダンプの生成においても、オブジェクト内のポインタを正確に識別するためにこの情報が利用されます。

技術的詳細

このコミットの主要な実装は、src/pkg/runtime/heapdump.c ファイルに集約されています。このファイルは、GoランタイムのC言語部分で記述されており、ヒープダンプの生成ロジックを担っています。

runtime/debug.WriteHeapDump 関数は、Go言語から呼び出されるエントリポイントであり、内部的にはC言語のruntime∕debug·WriteHeapDump関数を呼び出します。この関数は、以下の主要なステップを実行します。

  1. Stop the World (STW): ヒープダンプの整合性を保つため、一時的にすべてのGoroutineの実行を停止します。これにより、ヒープの状態がダンプ中に変化するのを防ぎます。
  2. メモリ統計の更新: ダンプ時点での正確なメモリ統計を取得するために、runtime·updatememstatsを呼び出します。これにより、GCキャッシュがフラッシュされ、MSpanのフリーリストにすべてのフリーオブジェクトが含まれるようになります。
  3. ダンプファイルの準備: 引数として渡されたファイルディスクリプタ(fd)を内部変数dumpfdに設定し、ダンプの書き込み先を準備します。
  4. Mスタックでのダンプルーチン呼び出し: ヒープダンプの生成処理は、現在のM(OSスレッド)のスタック上で実行されるmdump関数に委譲されます。これは、Goランタイムの内部的な制約と、GC関連の処理が特定のコンテキストで実行される必要があるためです。
  5. ヒープダンプの書き込み: mdump関数が、Go 1.3ヒープダンプフォーマットに従って、ヒープの各要素をdumpfdに書き込みます。これには以下の情報が含まれます。
    • ヘッダ: "go1.3 heap dump\n" という文字列で始まる。
    • パラメータ: ポインタのエンディアン、ポインタサイズ、ヒープのアリーナ情報、CPU数など、ランタイムの基本的なパラメータ。
    • itab情報: 現在割り当てられているすべてのitabのエントリ。
    • オブジェクト: ヒープ上のすべての割り当て済みオブジェクト。各オブジェクトについて、そのアドレス、サイズ、型情報、そしてオブジェクトの内容がダンプされます。特に、オブジェクト内のポインタフィールドは、playgcprog関数とポインタマップ(GCプログラム)を使用して正確に識別され、ダンプされます。
    • Goroutineとスタック: 実行中のすべてのGoroutineとそのスタック情報。各スタックフレームについて、関数名、PC(プログラムカウンタ)、SP(スタックポインタ)、ローカル変数、引数などがダンプされます。
    • OSスレッド: Goランタイムが使用しているOSスレッドの情報。
    • ルート: GCのルートセット(グローバル変数、スタック上のポインタ、レジスタなど)の情報。これには、データセグメント、BSSセグメント、MSpanの型情報、登録済みファイナライザ、ファイナライザキューなどが含まれます。
    • メモリ統計: ダンプ時点での詳細なメモリ統計(mstats)。
    • EOFタグ: ダンプの終了を示すタグ。
  6. バッファリング: 書き込み処理は、bufという内部バッファを使用して効率化されています。write関数は、データがバッファに収まる限りバッファリングし、バッファがいっぱいになった場合やflushが呼び出された場合に実際のファイル書き込みを行います。
  7. Stop the Worldの解除: ダンプが完了すると、すべてのGoroutineの実行が再開されます。

heapdump.cには、ヒープダンプフォーマットの各タグ(TagObject, TagGoRoutine, TagTypeなど)に対応するdump関数群が定義されています。例えば、dumpobjはヒープオブジェクトを、dumpgoroutineはGoroutineの情報をダンプします。

特に注目すべきは、dumptype関数による型情報のダンプと、dumpefacetypesdumpbvtypesによるEface(空インターフェース)内の型情報のダンプです。ヒープダンプを解析するツールが、ダンプされたオブジェクトの型を正確に再構築できるように、すべての関連する型情報がダンプされます。型情報のキャッシュ(typecache)も導入されており、重複する型情報のダンプを避けることで、ダンプファイルのサイズを最適化しています。

また、playgcprog関数は、GoのGCがオブジェクト内のポインタを識別するために使用するGCプログラム(ポインタマップ)を解釈し、ポインタフィールドのオフセットと種類を特定する汎用的なメカニズムを提供します。これは、ヒープダンプにおいてオブジェクトの内容を正確にダンプするために再利用されています。

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

このコミットで変更された主要なファイルとその役割は以下の通りです。

  • src/pkg/runtime/debug/garbage.go:
    • WriteHeapDump関数の宣言が追加されました。これはGo言語からヒープダンプ機能を呼び出すための公開APIです。
  • src/pkg/runtime/heapdump.c:
    • 新規追加ファイル。ヒープダンプの生成ロジックの大部分がここに実装されています。runtime∕debug·WriteHeapDump関数、各種dump関数(dumpobj, dumpgoroutine, dumptypeなど)、バッファリング機構、型キャッシュなどが含まれます。
  • src/pkg/runtime/iface.goc:
    • runtime·iterate_itabs関数が追加されました。これは、現在割り当てられているすべてのitabをイテレートし、コールバック関数を呼び出すための関数です。ヒープダンプでitab情報を収集するために使用されます。
  • src/pkg/runtime/malloc.h:
    • runtime·updatememstats関数の宣言が追加されました。これは、メモリ統計を更新し、GCキャッシュをフラッシュするために使用されます。
  • src/pkg/runtime/mgc0.c:
    • runtime·updatememstats関数の定義が移動され、runtime∕debug·WriteHeapDumpから呼び出せるようになりました。
    • runtime·iterate_finq関数が追加されました。これは、ファイナライザキューをイテレートし、コールバック関数を呼び出すための関数です。ヒープダンプでキューに入れられたファイナライザ情報を収集するために使用されます。
    • GCビットマップ関連の定数(wordsPerBitmapWord, bitShift, bitAllocatedなど)の定義がmgc0.hに移動されました。
  • src/pkg/runtime/mgc0.h:
    • mgc0.cからGCビットマップ関連の定数定義が移動されました。
  • src/pkg/runtime/runtime.h:
    • runtime·iterate_itabsruntime·iterate_finq関数の宣言が追加されました。

コアとなるコードの解説

src/pkg/runtime/debug/garbage.go

// WriteHeapDump writes a description of the heap and the objects in
// it to the given file descriptor.
// The heap dump format is defined at http://golang.org/s/go13heapdump.
func WriteHeapDump(fd uintptr)

このGo言語の関数宣言は、runtime/debugパッケージにWriteHeapDumpという新しい関数が追加されたことを示しています。この関数は、fd(ファイルディスクリプタ)を引数として受け取り、ヒープダンプをそのファイルディスクリプタに書き込みます。この関数自体はGoで実装されているわけではなく、GoランタイムのC言語部分で実装された同名の関数へのバインディングとして機能します。

src/pkg/runtime/heapdump.c

このファイルは、ヒープダンプ機能の心臓部です。

  • runtime∕debug·WriteHeapDump(uintptr fd):

    void
    runtime∕debug·WriteHeapDump(uintptr fd)
    {
        // Stop the world.
        runtime·semacquire(&runtime·worldsema, false);
        m->gcing = 1;
        m->locks++;
        runtime·stoptheworld();
    
        // Update stats so we can dump them.
        // As a side effect, flushes all the MCaches so the MSpan.freelist
        // lists contain all the free objects.
        runtime·updatememstats(nil);
    
        // Set dump file.
        dumpfd = fd;
    
        // Call dump routine on M stack.
        g->status = Gwaiting;
        g->waitreason = "dumping heap";
        runtime·mcall(mdump);
    
        // Reset dump file.
        dumpfd = 0;
    
        // Start up the world again.
        m->gcing = 0;
        runtime·semrelease(&runtime·worldsema);
        runtime·starttheworld();
        m->locks--;
    }
    

    この関数は、ヒープダンプ生成の全体的なフローを制御します。

    1. runtime·stoptheworld(): すべてのGoroutineを停止し、ヒープの変更を防ぎます。
    2. runtime·updatememstats(nil): 最新のメモリ統計を収集し、MCacheをフラッシュしてMSpanのフリーリストを更新します。
    3. dumpfd = fd: グローバル変数dumpfdに、ダンプを書き込むファイルディスクリプタを設定します。
    4. runtime·mcall(mdump): mdump関数を現在のM(OSスレッド)のスタックで実行します。これは、Goランタイムの内部的な制約によるものです。
    5. runtime·starttheworld(): Goroutineの実行を再開します。
  • mdump(G *gp):

    static void
    mdump(G *gp)
    {
        byte *hdr;
    
        runtime·memclr((byte*)&typecache[0], sizeof(typecache));
        hdr = (byte*)"go1.3 heap dump\\n";
        write(hdr, runtime·findnull(hdr));
        dumpparams();
        dumpitabs();
        dumpobjs();
        dumpgs();
        dumpms();
        dumproots();
        dumpmemstats();
        dumpint(TagEOF);
        flush();
    
        gp->param = nil;
        gp->status = Grunning;
        runtime·gogo(&gp->sched);
    }
    

    mdump関数は、ヒープダンプの具体的な内容を書き出すシーケンスを定義します。

    1. typecacheをクリアします。
    2. ヘッダ文字列 "go1.3 heap dump\n" を書き込みます。
    3. dumpparams(): ランタイムパラメータをダンプします。
    4. dumpitabs(): itab情報をダンプします。
    5. dumpobjs(): ヒープ上のすべてのオブジェクトをダンプします。
    6. dumpgs(): すべてのGoroutineとそのスタックをダンプします。
    7. dumpms(): OSスレッド情報をダンプします。
    8. dumproots(): GCルートセットをダンプします。
    9. dumpmemstats(): メモリ統計をダンプします。
    10. dumpint(TagEOF): ダンプの終了タグを書き込みます。
    11. flush(): バッファリングされたデータをすべてファイルに書き出します。
  • dumpobj(byte *obj, uintptr size, Type *type, uintptr kind):

    static void
    dumpobj(byte *obj, uintptr size, Type *type, uintptr kind)
    {
        if(type != nil) {
            dumptype(type);
            dumpefacetypes(obj, size, type, kind);
        }
    
        dumpint(TagObject);
        dumpint((uintptr)obj);
        dumpint((uintptr)type);
        dumpint(kind);
        dumpmemrange(obj, size);
    }
    

    個々のヒープオブジェクトをダンプする関数です。オブジェクトの型情報、アドレス、サイズ、種類、そして実際のメモリ内容を書き込みます。特に、Eface(空インターフェース)に含まれる型情報もdumpefacetypesを通じてダンプされます。

  • dumptype(Type *t):

    static void
    dumptype(Type *t)
    {
        // ... type caching logic ...
    
        // dump the type
        dumpint(TagType);
        dumpint((uintptr)t);
        dumpint(t->size);
        dumpstr(*t->string);
        dumpbool(t->size > PtrSize || (t->kind & KindNoPointers) == 0);
        dumpfields((uintptr*)t->gc + 1);
    }
    

    Goの型情報をダンプする関数です。型のサイズ、文字列表現、ポインタを含むかどうか、そしてGCプログラム(ポインタマップ)をダンプします。型情報の重複ダンプを避けるためのキャッシュ機構(typecache)も実装されています。

  • dumpgoroutine(G *gp):

    static void
    dumpgoroutine(G *gp)
    {
        // ... goroutine info dumping ...
    
        // dump stack
        // ...
        runtime·gentraceback(pc, sp, lr, gp, 0, nil, 0x7fffffff, dumpframe, &child, false);
    
        // dump defer & panic records
        // ...
    }
    

    個々のGoroutineの情報をダンプする関数です。GoroutineのID、ステータス、待機理由などの基本情報に加え、runtime·gentracebackを使用してスタックトレースをダンプします。また、Goroutineに関連付けられたdeferpanicレコードもダンプされます。

  • dumpframe(Stkframe *s, void *arg):

    static bool
    dumpframe(Stkframe *s, void *arg)
    {
        // ... stack frame info dumping ...
    
        // Dump fields in the outargs section
        // ...
        // Dump fields in the local vars section
        // ...
    }
    

    スタックフレームの情報をダンプするコールバック関数です。runtime·gentracebackから呼び出され、各スタックフレームのPC、SP、関数名、そしてローカル変数や引数内のポインタ情報をダンプします。

  • playgcprog(uintptr offset, uintptr *prog, void (*callback)(void*,uintptr,uintptr), void *arg):

    static uintptr*
    playgcprog(uintptr offset, uintptr *prog, void (*callback)(void*,uintptr,uintptr), void *arg)
    {
        // ... GC program interpretation logic ...
    }
    

    GCプログラム(ポインタマップ)を解釈し、オブジェクト内のポインタフィールドを特定するための汎用的なヘルパー関数です。ヒープダンプでは、オブジェクトやスタックフレーム内のポインタを正確に識別するために使用されます。

src/pkg/runtime/iface.goc

  • runtime·iterate_itabs(void (*callback)(Itab*)):
    void
    runtime·iterate_itabs(void (*callback)(Itab*))
    {
        int32 i;
        Itab *tab;
    
        for(i = 0; i < nelem(hash); i++) {
            for(tab = hash[i]; tab != nil; tab = tab->link) {
                callback(tab);
            }
        }
    }
    
    Goランタイムが管理するすべてのitabをイテレートし、引数として渡されたコールバック関数を各itabに対して呼び出します。これにより、ヒープダンプ時にすべてのitab情報を収集することが可能になります。

src/pkg/runtime/mgc0.c

  • runtime·iterate_finq(void (*callback)(FuncVal*, byte*, uintptr, Type*, PtrType*)):
    void
    runtime·iterate_finq(void (*callback)(FuncVal*, byte*, uintptr, Type*, PtrType*))
    {
        FinBlock *fb;
        Finalizer *f;
        uintptr i;
    
        for(fb = allfin; fb; fb = fb->alllink) {
            for(i = 0; i < fb->cnt; i++) {
                f = &fb->fin[i];
                callback(f->fn, f->arg, f->nret, f->fint, f->ot);
            }
        }
    }
    
    ファイナライザキュー内のすべてのファイナライザをイテレートし、コールバック関数を呼び出します。これにより、ヒープダンプ時にキューに入れられたファイナライザの情報を収集できます。

関連リンク

  • Go 1.3 Heap Dump Format: http://golang.org/s/go13heapdump
  • Goのガベージコレクションに関する公式ドキュメントやブログ記事 (Goのバージョンによって内容は異なりますが、基本的な概念は共通です):
    • The Go Blog: Go's new GC: https://go.dev/blog/go15gc (Go 1.5以降のGCに関する記事ですが、GCの基本的な考え方を理解するのに役立ちます)
  • Goのpprofツールに関するドキュメント: https://pkg.go.dev/runtime/pprof

参考にした情報源リンク

  • http://golang.org/s/go13heapdump のWeb検索結果 (Google Search)
  • Go言語のソースコード (特にsrc/pkg/runtime/ディレクトリ内のファイル)
  • Go言語のメモリ管理とガベージコレクションに関する一般的な知識
  • Goのインターフェースとitabに関する一般的な知識
  • GoのGoroutineとスタックに関する一般的な知識
  • Goのデバッグとプロファイリングに関する一般的な知識