Goプログラミングの世界において、パフォーマンスのボトルネックや原因不明のレイテンシを特定することは、常にエンジニアの挑戦であり続けてきました。

2025年9月にリリースされたGo 1.25では、この問題を劇的に解決する強力な新機能「Flight Recorder(フライトレコーダー)」が登場しました。

これは、航空機のブラックボックスのように、プログラムの実行履歴を常にメモリ上に記録し、問題が発生した瞬間にその直前のデータを切り出すことができる革新的な診断ツールです。

ランタイム実行トレースの進化と課題

Go言語には以前から、ランタイムの動作を詳細に記録する実行トレース(Execution Traces)という機能が存在していました。

この機能を使用すると、ゴルーチンがいつ実行され、いつブロックされたか、ネットワークの待機時間はどれくらいか、といった情報を時系列で把握できます。

従来、実行トレースを取得するにはruntime/traceパッケージを使用し、trace.Starttrace.Stopを明示的に呼び出す必要がありました。

従来のトレース手法が抱えていた限界

テストコードや短時間のベンチマークであれば、実行の最初から最後までを記録しても問題ありません。

しかし、24時間稼働し続けるWebサービスのようなアプリケーションでは、以下の課題が障壁となっていました。

課題内容
データ量の肥大化数日間トレースを取得し続けると、解析不可能なほど巨大なデータ量になる
再現の難しさ特定の条件下で発生する稀な不具合の場合、発生を予知して記録を開始できない
運用の複雑さ無作為にサンプリングして記録する仕組みを構築するには、高度なインフラが必要になる

特に「問題が発生したときには、すでに記録を開始するには手遅れである」という点は、本番環境でのデバッグを極めて困難にしていました。

このジレンマを解消するために開発されたのが、Flight Recorderです。

Flight Recorder:過去を遡る診断ツール

Flight Recorderの仕組みは非常にシンプルかつ強力です。

トレースデータをファイルやネットワークへ直接書き出すのではなく、ランタイムが管理するメモリ上のリングバッファに「直近数秒間」のデータのみを保持し続けます。

何らかの異常(レスポンスの遅延、エラーの発生など)をプログラム自身が検知した際、そのバッファの内容をファイルに書き出すことで、「問題が起きる直前に何が起きていたのか」をピンポイントで可視化できます。

これは、いわば過去を遡ってデバッグを行う能力を開発者に与えるものです。

実践:Flight Recorderによるパフォーマンス診断

具体的にどのようにFlight Recorderを使用するのか、HTTPサーバーの遅延問題を解決する例を見ていきましょう。

診断対象のコード

以下のプログラムは、数値を当てるゲームを提供するシンプルなAPIサーバーです。

1分に一度、集計データを別のサービスへ送信する処理が含まれていますが、この処理に深刻なバグが潜んでいます。

go
// 診断対象となるHTTPサーバーのサンプル
package main

import (
    "bytes"
    "encoding/json"
    "fmt"
    "log"
    "math/rand"
    "net/http"
    "strconv"
    "sync"
    "time"
)

type bucket struct {
    mu      sync.Mutex
    guesses int
}

func main() {
    buckets := make([]bucket, 100)

    // 1分ごとに集計レポートを送信する
    go func() {
        for range time.Tick(1 * time.Minute) {
            sendReport(buckets)
        }
    }()

    answer := rand.Intn(len(buckets))

    http.HandleFunc("/guess-number", func(w http.ResponseWriter, r *http.Request) {
        start := time.Now()
        guess, err := strconv.Atoi(r.URL.Query().Get("guess"))
        if err != nil || !(0 <= guess && guess < len(buckets)) {
            http.Error(w, "invalid guess", http.StatusBadRequest)
            return
        }

        b := &buckets[guess]
        b.mu.Lock()
        b.guesses++
        b.mu.Unlock()

        fmt.Fprintf(w, "guess: %d, correct: %t", guess, guess == answer)
        log.Printf("HTTP request: duration=%s", time.Since(start))
    })
    log.Fatal(http.ListenAndServe(":8090", nil))
}

func sendReport(buckets []bucket) {
    counts := make([]int, len(buckets))
    for index := range buckets {
        b := &buckets[index]
        b.mu.Lock()
        // 【バグの温床】関数の終了までロックが解除されない
        defer b.mu.Unlock()
        counts[index] = b.guesses
    }

    b, _ := json.Marshal(counts)
    url := "http://localhost:8091/guess-number-report"
    // HTTPリクエストが完了するまでロックを保持し続けてしまう
    http.Post(url, "application/json", bytes.NewReader(b))
}

このサーバーを運用していると、稀にレスポンスに100ミリ秒以上の時間がかかるという報告を受けました。

しかし、ログを見ても原因がすぐにはわかりません。

Flight Recorderの組み込み

ここで、Go 1.25で導入されたtrace.NewFlightRecorderを使用して、遅延発生時の状況をキャプチャするように改造します。

go
import "runtime/trace"

// Flight Recorderの初期化
fr := trace.NewFlightRecorder(trace.FlightRecorderConfig{
    // 保持するデータの最小期間(イベントの2倍程度を推奨)
    MinAge:   200 * time.Millisecond,
    // メモリ使用量の上限
    MaxBytes: 1 << 20, // 1 MiB
})
fr.Start()

// ハンドラー内でのトリガー設定
if time.Since(start) > 100*time.Millisecond {
    go func() {
        f, _ := os.Create("snapshot.trace")
        defer f.Close()
        // バッファの内容を書き出し
        fr.WriteTo(f)
        fr.Stop()
        log.Printf("captured snapshot to %s", f.Name())
    }()
}

この設定により、100msを超えるリクエストが発生した瞬間、その直前の実行状態が snapshot.trace に保存されます。

保存されたトレースの解析

取得したsnapshot.traceは、標準ツールのgo tool traceで解析します。

Shell
go tool trace snapshot.trace

ブラウザで解析画面を開き、「View trace by proc」を選択すると、タイムライン上に各ゴルーチンの動きが表示されます。

原因の特定:不要なブロッキング

トレース図を確認すると、特定の期間において、すべてのプロセッサ(PROCS)が何もしていない「巨大な空白時間」があることに気づきます。

これは、実行可能なゴルーチンが存在しないか、あるいは何らかのリソースを待機していることを示唆しています。

詳細を確認すると、以下の事実が判明します。

  1. sendReport関数が実行されている。
  2. その中でループを回しながらdefer b.mu.Unlock()を呼び出している。
  3. ループの中でロックを取得したまま、外部へのHTTPリクエスト(http.Post)が完了するのを待機している。

その結果、APIリクエストを処理しようとする他のゴルーチンがすべてこのミューテックスのロック待ちで停止し、システム全体に深刻なレイテンシが発生していたのです。

Flight Recorderがなければ、この「稀に発生する外部通信の遅延に引きずられたロック競合」を特定するのは極めて困難だったでしょう。

Flight Recorderの最適な設定値

Flight Recorderを効果的に利用するためには、設定値の理解が不可欠です。

設定項目説明推奨値の目安
MinAgeトレースデータを保持する最短時間。監視したいイベントの長さの2倍程度(例:5秒のタイムアウトを追うなら10秒)。
MaxBytesメモリバッファのサイズ。秒間数MB程度の生成が目安。高負荷なサービスでは10MB/秒程度を想定。

メモリへの影響: Flight Recorderは非常に効率的に設計されており、有効化によるパフォーマンスの低下は極めて限定的です。

Go 1.21以降で進められたトレース機能の軽量化により、多くの本番環境で常時有効にしても支障がないレベルに達しています。

まとめ

Go 1.25で導入されたFlight Recorderは、Goの診断エコシステムにおけるミッシングピースを埋める重要な機能です。

「問題が起きてから記録する」のではなく、「常に記録しておき、必要に応じて過去を切り出す」というアプローチへの転換は、運用フェーズでのトラブルシューティングを劇的に効率化します。

今回紹介したような、コード上のわずかなミス(deferの使い方など)が引き起こす複雑なパフォーマンス問題も、このツールがあれば確信を持って修正することができます。

Goの開発チームは、これまでも実行トレースのオーバーヘッド削減(1.21)や、トレースフォーマットの堅牢化(1.22)を積み重ねてきました。

Flight Recorderはその集大成とも言える機能です。

「何か起きた時のために、常にFlight Recorderを回しておく」ことが、今後のGoアプリケーション運用のスタンダードになっていくでしょう。

是非、ご自身のプロジェクトでも導入を検討してみてください。