1. シナリオと成功基準
このサービスはバッチごとに複数のワーカーを起動し、チャネルを通じて結果を集約します。いずれか 1 つのワーカーがエラーを返すと、呼び出し元は早期リターンします。ピーク時にはメモリと goroutine 数が増加し、インスタンスを再起動することで一時的に解消する状態です。アップグレードにあたっては、アンブロックできない goroutine を特定し、キャンセルセマンティクスを維持し、Go 1.26 の GC 挙動を比較し、ロールバック可能なロールアウトを提供する必要があります。
まずはベースラインの取得から始めます:リクエストレイテンシのパーセンタイル、スループット、ヒープサイズ、GC CPU、アクティブな goroutine、リークの傾き、およびエラー率です。runtime.NumGoroutine 単体では、どの goroutine が実際にリークしているかを判別できません。
2. 関連する Go 1.26 の変更点
Go 1.26 では、永久にブロックされて起動できなくなったクラスの goroutine を対象とした実験的な goroutineleak pprof プロファイルが導入されています。これは GOEXPERIMENT=goroutineleakprofile で有効化し、net/http/pprof 経由で公開します。これはガベージコレクタの到達可能性に依存しており、すべてのリークを特定できるわけではないため、不在の証明ではなく診断シグナルとして扱ってください。
このリリースではさらに、go fix のモダナイザーが提供され、new が式を受け取れるようになり、Green Tea GC がデフォルトで有効化されます。言語の利便性、実験的な診断機能、ランタイムの変更を個別に評価し、1 つのベンチマークが無関係な変数を混在させないようにします。
3. 最小限のリーク再現コードの構築
以下のパターンは、他のワーカーがバッファなしチャネルに送信を続け、最終的に永久にブロックされている間に、最初のエラーでリターンしてしまいます。
func processWorkItems(ctx context.Context, ws []WorkItem) ([]Result, error) {
ch := make(chan result)
for _, w := range ws {
go func() {
value, err := process(ctx, w)
ch <- result{value: value, err: err}
}()
}
results := make([]Result, 0, len(ws))
for range ws {
r := <-ch
if r.err != nil {
return nil, r.err
}
results = append(results, r.value)
}
return results, nil
}決定論的なエラーを注入し、実行を繰り返しながら、goroutine 数を監視しつつバッチサイズを増やします。修正が正常系のみをカバーすることのないよう、呼び出し元のキャンセル、タイムアウト、および正常終了のパスも含めます。
4. キャンセル処理、クローズ処理、およびバックプレッシャーの修正
送信側が ctx.Done() を尊重するようにし、受信側のキャンセルによって送信側が取り残されないようにします。明示的な容量を持つバッファ付きチャネルを使用するか、クローズと集約を担当する 1 つのコーディネーター goroutine を配置することで対応できます。複数のワーカーが 1 つのチャネルのクローズで競合してはなりません。
select {
case ch <- result{value: value, err: err}:
case <-ctx.Done():
}完全な修正では、最初のエラー時に派生コンテキストをキャンセルし、すべてのワーカーが終了するのを待ってからエラーを返します。errgroup.WithContext を使用してそのライフタイムを調整できますが、各ワーカーは引き続きコンテキストを伝播し、外部 I/O にタイムアウトを適用する必要があります。
5. goroutineleak プロファイルの有効化と証拠の収集
GOEXPERIMENT=goroutineleakprofile を指定して隔離されたバイナリをビルドし、保護された pprof エンドポイントをサンプリングします。同じ時間枠で、リークプロファイル、goroutine スタック、ヒーププロファイル、トレース、およびビジネスメトリクスを比較します。プロファイルが空であってもリークが存在しないことの証明にはなりません。グローバル変数から到達可能なブロッキングプリミティブは、このメカニズムによって分類されない可能性があります。
負荷時や障害注入時にサンプリングを行い、Go バージョン、実験フラグ、リクエスト負荷、サンプリング間隔を記録します。本番環境では、診断機能が情報漏洩の攻撃面にならないよう、エンドポイントへのアクセス、サンプリング頻度、および保持期間を制限してください。
6. Green Tea GC の評価とアップグレードの互換性
Go 1.26 では Green Tea GC がデフォルトで有効になります。リリースノートでは、小さなオブジェクトのマーキングとスキャン時における局所性と CPU スケーラビリティの向上が目標として記載されていますが、その恩恵はワークロードに依存します。ベンチマークにおいて GC CPU、一時停止時間、ピークヒープ、リクエストレイテンシを個別に測定し、同一のハードウェア、コンパイラフラグ、トラフィック条件下で古いバージョンと比較します。
リグレッションが発生した場合は、報告するかどうかを判断する前の診断コントロールとして一時的に GOEXPERIMENT=nogreenteagc を使用します。実験的なオプトアウトを恒久的なアーキテクチャにしないでください。トラフィックを増やす前に、カナリア環境でレース状態の挙動、cgo、プラグイン、および依存関係を検証します。
7. 監査可能な近代化としての go fix の利用
go fix は go vet と同じ分析フレームワークを使用して、挙動を維持するモダナイザーを適用します。Go 1.26 の new(expr) 構文はその代表的なターゲットです。ブランチ上で実行してすべての差分をレビューし、コンパイル、ユニットテスト、レース検出、ベンチマークを使用してセマンティクスを確認します。
単に新しいバージョンに追従するためだけにコードベース全体を書き直さないでください。自動修正はリークの修復や GC のアップグレードから分離しておき、別の修正を削除することなく 1 つのリスクをロールバックできるようにします。生成されたコードやバージョン間モジュールには、明示的な go ディレクティブとビルド制約を使用してください。
8. ルーブリックとフォローアップ
必須の説明事項
- ブロックの原因を説明し、キャンセル処理、クローズ処理、バックプレッシャーによってワーカーのライフタイムを修復すること。
goroutineleakが実験的な到達可能性ベースのプロファイルであり、すべてのリークをカバーできるわけではないことを理解していること。- Green Tea GC、go fix、およびバージョンのアップグレードを、独立したベースライン、カナリア検証、ロールバック手順に分離すること。
フォローアップの質問
- リークしているチャネルがグローバルレジストリによって保持されている場合、プロファイルが見逃す可能性がある理由と、どのような証拠を追加しますか?
- 最初のエラーが発生した後、ブロックされているネットワーク呼び出しも確実に終了させるにはどうしますか?
GOEXPERIMENT=nogreenteagcはどのような場合に使用し、根本的な問題を覆い隠さないようにするにはどうすればよいですか?
評価ガイド
優れた回答は、再現、ライフサイクルの修復、診断の証拠、およびアップグレードのガバナンスを結びつけます。すべてのワーカーをキャンセル可能にし、プロファイルとスタックで確認した上で、隔離されたベンチマークとカナリア環境によってランタイムの安全性を証明します。