フューチャー技術ブログ

Go 1.25リリース連載始まります & trace.FlightRecorder

Go 1.25 のリリースが近づいてきました。今回は比較的シンプルなアップデートかなと思います。

Date Title Author
7/30 この記事 渋川
7/31 log/slog 武田さん
8/1 sync 辻さん
8/4 net/http 島ノ江さん
8/5 testing/synctest 市川燿さん
8/6 json/v2 後藤さん
8/7 go vet 真野さん

Go 1.25 更新のオーバービュー

執筆時点では現在 RC2 が最新です。この時点のリリースノートを参考にしています。たまに直前で一部の機能のリリースがキャンセルになったりしますが、今回はどうでしょうか?

  • go build -asan オプションでプログラム終了時のリークのチェックが行われるようになった。C で割り当てられたメモリが C からも Go からも参照されなくなった状態で残っていたらエラーに
  • インストーラでバイナリとして提供されるツールが減って必要に応じてビルドすることでインストーラのサイズが削減(ARM の mac 向けで 73MB→56MB)
  • go doc -http で HTML で見るドキュメントサーバーが起動
  • go コマンドがリポジトリのサブディレクトリをモジュールのルートとして扱えるように
  • go vet が強化(取り扱い予定)
  • GOMAXPROCS が cgroups の CPU 制限を考慮してコンテナに割り当てられたコア数を検知するように
  • 実験的な greenteagc というガベージコレクタが追加。オーバーヘッドが 10-40%減る見込み。
  • 未処理の panic のときのメッセージの冗長性が減った
  • デバッグ情報のフォーマットが DWARF version 5 になってサイズが削減

greentea GC と GOMAXPROCS については kuro さんがすでに詳しく調べているのでスライドのリンクを紹介します。

go doc は過去いろいろ変遷があり、一度別のリポジトリにわかれて godoc になってなおかつ-http だけの機能のサポートにされたりしたのに、結局本体側が HTTP をサポートするようになったのですね。

標準ライブラリのアップデートは次の通りです

  • testing/synctest パッケージ追加(取り扱い予定)
  • 新しい実験的パッケージの encoding/json/v2 パッケージ追加(取り扱い予定)
  • archive/tar の Writer.AddFS がシンボリックリンクサポートを実装
  • encoding/asn1 の Unmarshal の安定性が向上
  • crypto パッケージで MessageSigner という署名のためのインターフェイスが追加
  • crypto パッケージはこまごまと関数が追加されたりとかしていますがパフォーマンスアップも報告されています。ecdsa, ed25519 では連邦情報処理標準規格(Federal Information Processing Standards)のセキュリティ基準に合致する厳密なモード FIPS 140-3 のパフォーマンスが 4 倍になり、モードの ON/OFF でパフォーマンスが変わらなくなったり、crypto/rsa のキー生成速度が 3 倍に、AMD64 での sha1 のハッシュ速度が 2 倍、sha3 の Apple の M でのハッシュ速度が 3 倍など。
  • io/fs でシンボリックリンクの読み込みのインタフェースが追加
  • log/slog で Attr をまとめて追加できる GroupAttrs が追加。Record にソースの場所を伝える Source()メソッドが追加
  • mime/multipart で Content-Disposition ヘッダーフィールドを設定しやすくするヘルパーの FileContentDisposition 関数が追加。このヘッダー何?という方はファイルダウンロード完全マスターを参照してください
  • net の LookupMXResolver が RFC 通りの実装をしていたが現実に合わせて IP アドレスのような DNS 名も返すように
  • net で Windows で他の OS との互換性が向上し、ファイルに紐づくネットワークが返せるようになったと。(FileConn など)
  • net/http で CrossOriginProtection が実装された(取り扱い予定)
  • os で Windows で非同期 I/O がサポート
  • os.Root のメソッドがさらに強化
  • reflect で TypeAssert 関数ができて Go の値に変換しやすくなった
  • runtime.AddCleanup で登録された後処理関数が並列動作するようになった
  • runtime/trace で FlightRecorder というのものが提供されるようになった
  • sync で WaitGroup.Go が追加 (取り扱い予定)
  • testing で Attr でテストログに属性を追加できるように

Go 1.24 からセキュリティが強化されるFIPS 140 モードなるものがあったのですね。気づきませんでした。

FlightRecorder

FlighRecorder は飛行機に積んであって、事故が起こった場合によく回収しただの解析しただの話題になるもののメタファーです。こちらのプロポーザルに理由が書かれています。Java に昔からあるフライトレコーダー(JFR)を参考にしています。

従来の trace はシステムコールの呼び出し、Goroutine、メモリなどさまざまな情報を取得し、何が内部で起こっているのかのレポートを作成します。それゆえに、データ量も膨大になってしまいます。FlightRecorder は記録は撮り続けるものの、最後の数秒の記録だけを残せば良い、という考え方に基づいています。

なお、当初は OOM を見たいというのが issue には要望がありましたが、OOM の場合はプロセス側でハンドリングするチャンスがないので難しいということが議論されています。共有メモリに書き出す、Linux の Memory Presure のイベントを取るなども例示されていましたが、特に機能としては入ってないです。

使い方は次の通りです。FlightRecorder としてはStart(), Stop(), WriteTo()というメソッドを提供しています。Stop()してからWriteTo()を呼んだらだめでした。まずはWriteTo()です。出力されたトレースは、従来通りgo tool trace ダンプファイルで閲覧できます。

package main

import (
"fmt"
"os"
"runtime/trace"
"time"
)

func main() {
// 1. トレースファイルを作成
f, err := os.Create("trace.out")
if err != nil {
fmt.Println("failed to create trace file:", err)
return
}
defer f.Close()

// 2. トレースの開始
fr := trace.NewFlightRecorder(trace.FlightRecorderConfig{
MinAge: 100 * time.Millisecond,
MaxBytes: 16 * 1024 * 1024, // 16MB
})
if err := fr.Start(); err != nil {
fmt.Println("failed to start trace:", err)
return
}
defer func() {
fr.WriteTo(f)
fr.Stop()
}()

fmt.Println("トレースを開始しました。")

// ここにパフォーマンスを測定したい処理を記述

fmt.Println("トレースを終了しました。")
}

参考までに従来のtrace.Start()/trace.Stop()の使い方はこちらです。WriteTo()にあたるところが隠蔽化されていてこちらの方がコードは短くなっています。

func main() {
// 1. トレースファイルを作成
f, err := os.Create("trace.out")
if err != nil {
fmt.Println("failed to create trace file:", err)
return
}
defer f.Close()

// 2. トレースの開始
if err := trace.Start(f); err != nil {
fmt.Println("failed to start trace:", err)
return
}
defer trace.Stop()

fmt.Println("トレースを開始しました。")

// ここにパフォーマンスを測定したい処理を記述

fmt.Println("トレースを終了しました。")
}

出力時に出力先があれば良い、というのであれば期待する使われ方としては出力ファイルは後で作れば良いかもしれません。ファイルディスクリプタ枯渇とかはまあ考えなくても良いのかな。心配なら最初にファイルを作る方が良いでしょう。なお、コンテナの場合はローカルに無意にファイルを置いても消えてしまうので、永続化するストレージをマウントしてそこに生成するか、いっそのことio.Pipeを使ってhttp.Post()で、トレース収集用サーバーなどを用意した上でアップロードする、とかですかね。

func main() {
// トレースの開始
fr := trace.NewFlightRecorder(trace.FlightRecorderConfig{
MinAge: 1 * time.Millisecond,
MaxBytes: 16 * 1024 * 1024, // 16MB
})
if err := fr.Start(); err != nil {
fmt.Println("failed to start trace:", err)
return
}
defer func() {
// 出力時にファイルを作る
f, err := os.Create("trace.out")
if err != nil {
fmt.Println("failed to create trace file:", err)
return
}
defer f.Close()
fr.WriteTo(f)
fr.Stop()
}()

fmt.Println("トレースを開始しました。")

// ここにパフォーマンスを測定したい処理を記述

fmt.Println("トレースを終了しました。")
}

まとめ

いままでも便利な Trace はありましたが、FlightRecorder の「いつでもしておく」というのはオブザーバービリティ的な考えで良いですね。常に回しておける、というのは今までになかった使い方も出てくると思います。

特定のエンドポイントやシグナルでダンプをどこかに出力するような仕組みを作ってみたり、ダウンロードできるエンドポイントを作っておく、というのも良いかもしれません。「なんか goroutine がブロックしているな」とか「なんかメモリ消費量が跳ね上がったな」みたいなタイミングで、過去 1 分間分のトレースをダウンロードして分析してみる、といったことができると、問題の分析の役に立つ気がします。

次は武田さんの log/slog です。