大家好,我是煎鱼。
不知道大家在生产环境排查问题的时候,有没有遇到过这样的窘境:服务突然慢了,等你反应过来想抓个 trace 看看,问题已经过去了。就像开车遇到异响,等你停下来检查,声音又没了。
今天给大家分享 Go1.25 的一个重磅特性:Flight Recorder(飞行记录器)。这玩意儿真的是救命神器,能让你在问题发生后,回溯几秒钟前的执行状态。
背景
先说说为什么需要这个东西。
Go 的 execution trace 功能其实一直都有,通过runtime/trace包就能收集程序执行时的各种事件。
这对于调试延迟问题特别有用,能清楚地看到 goroutine 什么时候在执行,更重要的是,什么时候没在执行。
但问题来了。
对于短期运行的程序,比如测试、基准测试或者命令行工具,你可以从头到尾收集完整的 trace。但对于长期运行的 Web 服务,这就不现实了。服务器可能要运行好几天甚至几周,你总不能一直开着 trace 收集数据吧?那数据量得多恐怖。
更尴尬的是,往往是某个请求超时了,或者健康检查失败了,等你意识到问题,想调用trace.Start()的时候,早就晚了。
有人说,那我随机采样不就行了?这个思路是对的,但需要一大堆基础设施支撑。你得存储、分类、处理海量的 trace 数据,而且大部分数据其实都没啥用。更关键的是,当你想排查某个具体问题的时候,这种方式基本帮不上忙。
Flight Recorder 是什么
这就是 Flight Recorder 要解决的问题。
核心思路很简单:程序通常能感知到出问题了,但根因可能早就发生了。Flight Recorder 让你能收集问题发生前几秒钟的 trace 数据。
它的工作原理是这样的:正常收集 trace 数据,但不是写到文件或 socket 里,而是在内存里缓存最近几秒的数据。
一旦程序检测到问题,随时可以把缓冲区的内容快照下来,精准定位到问题窗口。
实战案例
我们用一个实际例子来看看怎么用。
假设有这么一个 HTTP 服务,实现了一个"猜数字"的游戏。它暴露了一个/guess-number端点,接收一个整数,告诉调用者猜得对不对。
同时还有个 goroutine 每分钟发送一次统计报告。
核心代码大概是这样:
type bucket struct {
mu sync.Mutex
guesses int
}
func main() {
buckets := make([]bucket, 100)
// 每分钟发送报告
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 duration=%s",
guess, 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, 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)
}
}
上线后,用户开始反馈有些请求特别慢。
看日志发现,大部分请求都是微秒级的,但偶尔会有超过 100 毫秒的:
2025/09/19 16:52:02 HTTP request: endpoint=/guess-number guess=69 duration=625ns
2025/09/19 16:52:02 HTTP request: endpoint=/guess-number guess=42 duration=1.417µs
2025/09/19 16:52:02 HTTP request: endpoint=/guess-number guess=86 duration=115.186167ms
2025/09/19 16:52:02 HTTP request: endpoint=/guess-number guess=0 duration=127.993375ms
问题来了,能看出哪里有 bug 吗?
用 Flight Recorder 排查
先别急着看答案,我们用 Flight Recorder 来排查。
首先,在 main 函数里配置并启动 recorder:
// 配置Flight Recorder
fr := trace.NewFlightRecorder(trace.FlightRecorderConfig{
MinAge: 200 * time.Millisecond,
MaxBytes: 1 << 20, // 1 MiB
})
fr.Start()
这里 MinAge 设置为 200 毫秒,大概是问题窗口的 2 倍。
MaxBytes 限制缓冲区大小,避免内存爆炸。一般来说,每秒会产生几 MB 的 trace 数据,繁忙的服务可能达到 10MB/s。
接下来写个辅助函数来捕获快照:
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())
})
}
然后在请求处理函数里,当响应时间超过 100 毫秒时触发快照:
if fr.Enabled() && time.Since(start) > 100*time.Millisecond {
go captureSnapshot(fr)
}
重新运行服务,等到触发慢请求,我们就能拿到快照文件了。
分析 trace
拿到 trace 文件后,用 Go 自带的工具分析:
go tool trace snapshot.trace
这个工具会启动一个本地 Web 服务器,然后在浏览器里打开。点击"View trace by proc"可以看到时间线视图。
在这个视图里,我们能看到 goroutine 的执行情况。重点关注右侧那个巨大的空白期——大概 100 毫秒,啥都没干!
放大这个区域后,可以看到很多 goroutine 都在等待一个特定的 goroutine。点击这个 goroutine,查看它的栈信息,发现它在执行sendReport函数。
再仔细看那些"Outgoing flow"事件,它们都指向了sendReport里的Unlock操作。
问题找到了!
看这段代码:
for index := range buckets {
b := &buckets[index]
b.mu.Lock()
defer b.mu.Unlock()
counts[index] = b.guesses
}
我们本想给每个 bucket 加锁,拷贝完值就解锁。但defer的执行时机是函数返回时,不是循环结束时。
所以这些锁一直被持有,直到整个 HTTP 请求完成后才释放。
这就是典型的 defer 误用场景。正确的写法应该是:
for index := range buckets {
b := &buckets[index]
b.mu.Lock()
counts[index] = b.guesses
b.mu.Unlock()
}
总结
Flight Recorder 真的是个好东西。它让我们能在问题发生后,回过头去看发生了什么,而不需要一直开着 trace 收集海量数据。
简单来说,它就像是给你的程序装了个行车记录仪,出了事故可以回放录像。比起传统的 trace 方式,既节省资源,又能精准定位问题。
这个特性在 Go1.25 正式可用了,配合之前几个版本对 tracing 的优化(Go1.21 降低了开销,Go1.22 改进了 trace 格式),整个诊断工具链越来越成熟了。
如果你经常需要排查生产环境的性能问题,强烈建议试试这个新特性。
关注和加煎鱼微信,
一手消息和知识,拉你进技术交流群👇


你好,我是煎鱼,出版过 Go 畅销书《Go 语言编程之旅》,再到获得 GOP(Go 领域最有观点专家)荣誉,点击蓝字查看我的出书之路。
日常分享高质量文章,输出 Go 面试、工作经验、架构设计,加微信拉读者交流群,和大家交流!
原创不易 点赞支持
1111

被折叠的 条评论
为什么被折叠?



