news 2026/2/10 18:08:01

Go Flight Recorder 终于来了,线上问题可以 “回放“ 了!

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
Go Flight Recorder 终于来了,线上问题可以 “回放“ 了!

大家好,我是煎鱼。

不知道大家在生产环境排查问题的时候,有没有遇到过这样的窘境:服务突然慢了,等你反应过来想抓个 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 面试、工作经验、架构设计,加微信拉读者交流群,和大家交流!

原创不易 点赞支持

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!