Go Flight Recorder 終于來了,線上問題可以“回放”了!
不知道大家在生產(chǎn)環(huán)境排查問題的時(shí)候,有沒有遇到過這樣的窘境:服務(wù)突然慢了,等你反應(yīng)過來想抓個(gè) trace 看看,問題已經(jīng)過去了。就像開車遇到異響,等你停下來檢查,聲音又沒了。
今天給大家分享 Go1.25 的一個(gè)重磅特性:Flight Recorder(飛行記錄器)。這玩意兒真的是救命神器,能讓你在問題發(fā)生后,回溯幾秒鐘前的執(zhí)行狀態(tài)。
圖片
背景
先說說為什么需要這個(gè)東西。
Go 的 execution trace 功能其實(shí)一直都有,通過runtime/trace包就能收集程序執(zhí)行時(shí)的各種事件。
這對于調(diào)試延遲問題特別有用,能清楚地看到 goroutine 什么時(shí)候在執(zhí)行,更重要的是,什么時(shí)候沒在執(zhí)行。
但問題來了。
對于短期運(yùn)行的程序,比如測試、基準(zhǔn)測試或者命令行工具,你可以從頭到尾收集完整的 trace。但對于長期運(yùn)行的 Web 服務(wù),這就不現(xiàn)實(shí)了。服務(wù)器可能要運(yùn)行好幾天甚至幾周,你總不能一直開著 trace 收集數(shù)據(jù)吧?那數(shù)據(jù)量得多恐怖。
更尷尬的是,往往是某個(gè)請求超時(shí)了,或者健康檢查失敗了,等你意識到問題,想調(diào)用trace.Start()的時(shí)候,早就晚了。
有人說,那我隨機(jī)采樣不就行了?這個(gè)思路是對的,但需要一大堆基礎(chǔ)設(shè)施支撐。你得存儲、分類、處理海量的 trace 數(shù)據(jù),而且大部分?jǐn)?shù)據(jù)其實(shí)都沒啥用。更關(guān)鍵的是,當(dāng)你想排查某個(gè)具體問題的時(shí)候,這種方式基本幫不上忙。
Flight Recorder 是什么
這就是 Flight Recorder 要解決的問題。
核心思路很簡單:程序通常能感知到出問題了,但根因可能早就發(fā)生了。Flight Recorder 讓你能收集問題發(fā)生前幾秒鐘的 trace 數(shù)據(jù)。
它的工作原理是這樣的:正常收集 trace 數(shù)據(jù),但不是寫到文件或 socket 里,而是在內(nèi)存里緩存最近幾秒的數(shù)據(jù)。
一旦程序檢測到問題,隨時(shí)可以把緩沖區(qū)的內(nèi)容快照下來,精準(zhǔn)定位到問題窗口。
實(shí)戰(zhàn)案例
我們用一個(gè)實(shí)際例子來看看怎么用。
假設(shè)有這么一個(gè) HTTP 服務(wù),實(shí)現(xiàn)了一個(gè)"猜數(shù)字"的游戲。它暴露了一個(gè)/guess-number端點(diǎn),接收一個(gè)整數(shù),告訴調(diào)用者猜得對不對。
同時(shí)還有個(gè) goroutine 每分鐘發(fā)送一次統(tǒng)計(jì)報(bào)告。
核心代碼大概是這樣:
type bucket struct {
mu sync.Mutex
guesses int
}
func main() {
buckets := make([]bucket, 100)
// 每分鐘發(fā)送報(bào)告
gofunc() {
forrange 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' value", 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: endpoint=/guess-number guess=%d duratinotallow=%s",
guess, time.Since(start))
})
log.Fatal(http.ListenAndServe(":8090", nil))
}發(fā)送報(bào)告的函數(shù)是這樣寫的:
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, err := json.Marshal(counts)
if err != nil {
log.Printf("failed to marshal report data: error=%s", err)
return
}
url := "http://localhost:8091/guess-number-report"
if _, err := http.Post(url, "application/json", bytes.NewReader(b)); err != nil {
log.Printf("failed to send report: %s", err)
}
}上線后,用戶開始反饋有些請求特別慢。
看日志發(fā)現(xiàn),大部分請求都是微秒級的,但偶爾會有超過 100 毫秒的:
2025/09/19 16:52:02 HTTP request: endpoint=/guess-number guess=69 duratinotallow=625ns
2025/09/19 16:52:02 HTTP request: endpoint=/guess-number guess=42 duratinotallow=1.417μs
2025/09/19 16:52:02 HTTP request: endpoint=/guess-number guess=86 duratinotallow=115.186167ms
2025/09/19 16:52:02 HTTP request: endpoint=/guess-number guess=0 duratinotallow=127.993375ms問題來了,能看出哪里有 bug 嗎?
用 Flight Recorder 排查
先別急著看答案,我們用 Flight Recorder 來排查。
首先,在 main 函數(shù)里配置并啟動 recorder:
// 配置Flight Recorder
fr := trace.NewFlightRecorder(trace.FlightRecorderConfig{
MinAge: 200 * time.Millisecond,
MaxBytes: 1 << 20, // 1 MiB
})
fr.Start()這里 MinAge 設(shè)置為 200 毫秒,大概是問題窗口的 2 倍。
MaxBytes 限制緩沖區(qū)大小,避免內(nèi)存爆炸。一般來說,每秒會產(chǎn)生幾 MB 的 trace 數(shù)據(jù),繁忙的服務(wù)可能達(dá)到 10MB/s。
接下來寫個(gè)輔助函數(shù)來捕獲快照:
var once sync.Once
func captureSnapshot(fr *trace.FlightRecorder) {
once.Do(func() {
f, err := os.Create("snapshot.trace")
if err != nil {
log.Printf("opening snapshot file %s failed: %s", f.Name(), err)
return
}
defer f.Close()
_, err = fr.WriteTo(f)
if err != nil {
log.Printf("writing snapshot to file %s failed: %s", f.Name(), err)
return
}
fr.Stop()
log.Printf("captured a flight recorder snapshot to %s", f.Name())
})
}然后在請求處理函數(shù)里,當(dāng)響應(yīng)時(shí)間超過 100 毫秒時(shí)觸發(fā)快照:
if fr.Enabled() && time.Since(start) > 100*time.Millisecond {
go captureSnapshot(fr)
}重新運(yùn)行服務(wù),等到觸發(fā)慢請求,我們就能拿到快照文件了。
分析 trace
拿到 trace 文件后,用 Go 自帶的工具分析:
go tool trace snapshot.trace這個(gè)工具會啟動一個(gè)本地 Web 服務(wù)器,然后在瀏覽器里打開。點(diǎn)擊"View trace by proc"可以看到時(shí)間線視圖。
在這個(gè)視圖里,我們能看到 goroutine 的執(zhí)行情況。重點(diǎn)關(guān)注右側(cè)那個(gè)巨大的空白期——大概 100 毫秒,啥都沒干!
圖片
放大這個(gè)區(qū)域后,可以看到很多 goroutine 都在等待一個(gè)特定的 goroutine。點(diǎn)擊這個(gè) goroutine,查看它的棧信息,發(fā)現(xiàn)它在執(zhí)行sendReport函數(shù)。
圖片
再仔細(xì)看那些"Outgoing flow"事件,它們都指向了sendReport里的Unlock操作。
圖片
問題找到了!
看這段代碼:
for index := range buckets {
b := &buckets[index]
b.mu.Lock()
defer b.mu.Unlock()
counts[index] = b.guesses
}我們本想給每個(gè) bucket 加鎖,拷貝完值就解鎖。但defer的執(zhí)行時(shí)機(jī)是函數(shù)返回時(shí),不是循環(huán)結(jié)束時(shí)。
所以這些鎖一直被持有,直到整個(gè) HTTP 請求完成后才釋放。
這就是典型的 defer 誤用場景。正確的寫法應(yīng)該是:
for index := range buckets {
b := &buckets[index]
b.mu.Lock()
counts[index] = b.guesses
b.mu.Unlock()
}總結(jié)
Flight Recorder 真的是個(gè)好東西。它讓我們能在問題發(fā)生后,回過頭去看發(fā)生了什么,而不需要一直開著 trace 收集海量數(shù)據(jù)。
簡單來說,它就像是給你的程序裝了個(gè)行車記錄儀,出了事故可以回放錄像。比起傳統(tǒng)的 trace 方式,既節(jié)省資源,又能精準(zhǔn)定位問題。
這個(gè)特性在 Go1.25 正式可用了,配合之前幾個(gè)版本對 tracing 的優(yōu)化(Go1.21 降低了開銷,Go1.22 改進(jìn)了 trace 格式),整個(gè)診斷工具鏈越來越成熟了。
如果你經(jīng)常需要排查生產(chǎn)環(huán)境的性能問題,強(qiáng)烈建議試試這個(gè)新特性。





























