• Go 1.25 的新特性:Flight Recorder
  • 发布于 1个月前
  • 614 热度
    0 评论
  • 那场梦
  • 18 粉丝 66 篇博客
  •   
Go 1.25 引入了一个令人兴奋的新特性:Trace Flight Recorder (飞行记录)。这个工具为 Go 开发者提供了一种更高效、更轻量级的生产环境调试和性能分析方法。本文将深入探讨 Trace Flight Recorder 的工作原理、配置方式、使用示例以及它在实际应用中的价值。

什么是 Tracing 和 Flight Recording?
在深入解 Trace Flight Recorder 之前,我们首先需要理解两个核心概念:
Tracing (跟踪): Tracing 是一种监控和调试技术,通过收集程序执行的详细信息,例如函数调用、goroutine 活动、内存分配等,来帮助开发者识别性能瓶颈和调试复杂问题。传统的 tracing 方式通常会记录程序的整个生命周期,这可能会导致生成巨大的跟踪文件,带来较高的开销。

Flight Recording (飞行记录): 飞行记录是一种更精妙的跟踪方法。它不像传统跟踪那样捕获所有内容,而是在一个循环缓冲区中维护最新的执行数据。这意味着它只保留最近的程序活动,并自动丢弃较旧的信息,以节省空间并显著减少开销。这种方法特别适合在生产环境中持续运行,因为它只会产生一个大小可控的跟踪文件。


Trace Flight Recorder 的配置与使用
Go 1.25 的 trace.FlightRecorderConfig 结构体是配置 Trace Flight Recorder 的关键。它包含两个主要字段:
minAge: 用于指定跟踪事件在被丢弃之前至少保留的时长。例如,设置为 5 秒,则表示缓冲区中的数据至少会保留 5 秒。
maxBytes: 用于定义循环缓冲区的最大大小。例如,设置为 3MB,则表示缓冲区的大小上限为 3MB。
需要注意的是,这两个值是对 Go 运行时的建议,并不保证数据会被精确地保存。

实际代码演示
下面我们将通过一个简单的代码示例来演示如何使用 Trace Flight Recorder:
1. 创建一个简单的 Web 服务器
首先,我们创建一个基本的 HTTP 服务器,并添加一个 /heavy 路由来模拟 CPU 负载。
package main

import (
    "log"
    "net/http"
    "time"
)

// heavyLoad 模拟 CPU 密集型任务
func pow(targetBits int) [32]byte{
 target := big.NewInt(1)
 target.Lsh(target, uint(256-targetBits))

 var hashInt big.Int
 var hash [32]byte
 nonce := 0

 for {
  data := "hello world " + strconv.Itoa(nonce)
  hash = sha256.Sum256([]byte(data))
  hashInt.SetBytes(hash[:])

  if hashInt.Cmp(target) == -1 {
   break
  } else {
   nonce++
  }

  if nonce%100 == 0 {
   runtime.Gosched()
  }
 }
}

func handler(w http.ResponseWriter, r *http.Request) {
    if r.URL.Path == "/heavy" {
        heavyLoad()
    }
    w.Write([]byte("Hello, world!"))
}

func main() {
    http.HandleFunc("/", handler)
    log.Fatal(http.ListenAndServe(":8080", nil))
}
2. 配置并启动 Trace Flight Recorder
接下来,我们在程序启动时配置并启动 Trace Flight Recorder。
package main

import (
    "log"
    "net/http"
    "time"
    "runtime/trace"
    "context"
    "os"
)

var recorder *trace.FlightRecorder

// ... 前面定义的pow函数和handler ...
// 堆代码 duidaima.com
func main() {
    // 配置 Trace Flight Recorder
    cfg := trace.FlightRecorderConfig{
        MinAge:   5 * time.Second, // 至少保留 5 秒
        MaxBytes: 3 * 1024 * 1024, // 最大 3 MB
    }
    recorder = trace.StartFlightRecorder(cfg)
    defer recorder.Stop()
    http.HandleFunc("/", handler)
    log.Fatal(http.ListenAndServe(":8080", nil))
}
3. 实现性能触发器并保存跟踪文件
在实际应用中,我们通常希望在发生异常或性能问题时自动保存跟踪文件。我们可以添加一个性能触发器,当计算出的 hash 值前 6 个字节都是 0,自动保存跟踪快照。
package main

import (
    "log"
    "net/http"
    "time"
    "runtime/trace"
    "os"
)

// ... 前面定义的pow函数 ...

func handlerWithTrigger(w http.ResponseWriter, r *http.Request) {
 hash := pow(rand.Intn(20) + 10) // 随机选择难度在 10 到 30 之间

 if strings.HasPrefix(hash, "000000") {
  // 请求耗时过长,保存跟踪快照
  file, err := os.Create("trace.out")
  if err != nil {
   log.Println("failed to create trace file:", err)
   return
  }
  defer file.Close()

  // 保存最新的跟踪数据到文件
  if _, err := recorder.WriteTo(file); err != nil {
   log.Println("failed to write trace data:", err)
  }
 }
 w.Write([]byte(hash))
}

func main() {
 // 配置 Trace Flight Recorder
 cfg := trace.FlightRecorderConfig{
  MinAge:   5 * time.Second, // 至少保留 5 秒
  MaxBytes: 3 * 1024 * 1024, // 最大 3 MB
 }

 recorder = trace.NewFlightRecorder(cfg)
 if err := recorder.Start(); err != nil {
  log.Fatalf("failed to start FlightRecorder: %v", err)
 }
 defer recorder.Stop()

 http.HandleFunc("/", handlerWithTrigger)
 log.Fatal(http.ListenAndServe(":8080", nil))
}
在上面的例子中,我们使用 WriteTo 函数将循环缓冲区中的最新跟踪数据写入到 trace.out 文件中。

分析跟踪数据
保存跟踪文件后,我们可以使用 go tool trace 命令来分析它:
go tool trace trace.out
这个命令会打开一个浏览器页面,展示丰富的可视化数据(trace event),帮助你理解程序的执行情况。你可以看到:
Goroutine 活动: 了解 goroutine 的创建、执行和阻塞情况。
CPU 使用率: 查看不同逻辑处理器上的 CPU 使用模式。
堆使用模式: 分析内存分配和垃圾回收(GC)的模式。

用例与总结
Trace Flight Recorder 提供了在不影响性能的前提下,对 Go 应用程序进行持续监控和调试的能力。它的主要用例包括:
生产环境调试: 捕获围绕罕见或难以重现错误的上下文,而无需持续的完整跟踪。
性能监控: 分析那些只在特定条件下出现的不可预测的性能问题。

内存受限环境: 由于其低开销和可控的缓冲区大小,它非常适合在资源有限的设备上进行性能分析。


原理
这个新特性/新工具是在 issue#63185[1] 中提出的。“飞行记录”是一种技术,其中将跟踪数据保存在一个概念上的环形缓冲区中,在请求时刷新。这种技术的目的是捕获有趣程序行为的跟踪,即使事先不知道何时会发生。例如,如果网络服务失败健康检查,或者网络服务处理请求的时间异常长。具体来说,网络服务可以在这些条件发生时识别它们,但设置环境的程序员无法预测它们何时会确切发生。在发生有趣的事情后开始跟踪通常也不太有用,因为程序已经执行了有趣的部分。

Java 生态系统已经通过 Java 的飞行记录器拥有这项功能多年了。一旦 JVM 的飞行记录器被启用,JVM 就可以获取代表最后几秒钟时间的跟踪信息。这个跟踪信息可以来自 JMX 中设置的触发器,或者通过传递一个标志给 JVM,在退出时导出跟踪信息。随着  #60773[2]  的实现逐渐接近稳定,Go 1.22 版本中我们能将所有跟踪信息变成一系列自包含的分区。这种实现变更提供了一个机会,可以轻松地添加类似于 Go 执行跟踪器的东西,通过始终保留至少一个可以在任何时间快照的分区。

这还得归功于 Go 1.21 版本中为了使跟踪成本大幅降低所做的努力。因为飞行记录依赖于等待有趣的事情发生,所以跟踪需要启用更长的时间。当跟踪本身并不昂贵时,在例如生产集群的小部分上启用飞行记录就变得更加容易接受。设计核心是在  runtime/trace  包中引入了一个新的 API 以启用飞行记录。这意味着程序可以使用自己的触发器进行仪器化:
package trace

type FlightRecorder struct {
    ...
}
func NewFlightRecorder() *FlightRecorder

func (*FlightRecorder) SetMinAge(d time.Duration)
func (*FlightRecorder) MinAge() time.Duration
// 这个设置优先于SetMinAge
func (*FlightRecorder) SetMaxBytes(bytes uint64)
func (*FlightRecorder) MaxBytes() uint64

func (*FlightRecorder) Start() error
func (*FlightRecorder) Stop() error

func (*FlightRecorder) Enabled() bool

func (*FlightRecorder) WriteTo(w io.Writer) (n int64, err error)
如果你在 Go 代码库中搜索 trace.ok()关键字,会看到很多跟踪的代码:

比如 trace.ProcSteal方法:
func (tl traceLocker) ProcSteal(pp *p, inSyscall bool) {
 // Grab the M ID we stole from.
 mStolenFrom := pp.trace.mSyscallID
 pp.trace.mSyscallID = -1

 if !pp.trace.statusWasTraced(tl.gen) && pp.trace.acquireStatus(tl.gen) {
  tl.writer().writeProcStatus(uint64(pp.id), tracev2.ProcSyscallAbandoned, pp.trace.inSweep).end()
 }


 goStatus := tracev2.GoRunning
 procStatus := tracev2.ProcRunning
 if inSyscall {
  goStatus = tracev2.GoSyscall
  procStatus = tracev2.ProcSyscallAbandoned
 }
 tl.eventWriter(goStatus, procStatus).event(tracev2.EvProcSteal, traceArg(pp.id), pp.trace.nextSeq(tl.gen), traceArg(mStolenFrom))
}
这个方法是 Go 运行时跟踪系统(runtime/trace)的一部分,它负责记录 Go 调度器中一个非常重要的事件:P 偷取(P stealing)。这个 Go 代码片段是 Go 运行时跟踪系统(runtime/trace)的一部分,它负责记录 Go 调度器中一个非常重要的事件:P 偷取(P stealing)。
1.记录被偷取的 M
2.更新被偷取 P 的状态
3.确定偷取者的状态

4.发出主跟踪事件

tl.eventWriter(goStatus, procStatus).event(...): 这是最后一步,也是最重要的。它使用之前确定的状态,记录主事件 EvProcSteal。
该事件携带了关键信息:被偷取 P 的 ID(pp.id)、一个事件序列号,以及被偷取 P 之前的 M 的 ID(mStolenFrom)。
当然全面的设计文档在 https://go.googlesource.com/proposal/+/ac09a140c3d26f8bb62cbad8969c8b154f93ead6/design/60773-execution-tracer-overhaul.md, 这是 Go 1.22 中实现的基础性的工作,到 Go 1.25 中开始展示它的强大的功能。

参考资料
[1] issue#63185: https://github.com/golang/go/issues/63185
[2] #60773: https://github.com/golang/go/issues/60773
用户评论