登录
推荐 文章 Go 技术 课程 下载 专题 AI
首页 >  Golang >  Go教程

Go runtime/trace 怎样只捕获一段关键请求窗口

来源:17golang原创

时间:2026-11-14 00:13:15 226浏览 收藏

我排查 Go HTTP 服务偶发变慢时,最怕一上来就让进程长时间记录 trace:数据量大,真正的请求反而容易淹没。更稳妥的做法是给诊断请求加一个明确的选择条件,只在它进入处理函数前调用 trace.Start,请求返回后调用 trace.Stop,把这一小段写入独立文件。这样截到的是“这段时间内整个进程的运行事件”,不是只属于某一个 goroutine 的私有录像。

要点速览
  • 能提前命中慢请求时,用 Start/Stop 包住一次受控请求窗口;一个进程同一时刻只能有一个活动 trace。
  • 文件必须先打开,Stop 等待追踪数据写完后再关闭;遇到 Start 失败要保留原请求,不要误判为已采集。
  • 如果问题只能在发生后才发现,Go 1.25 的 Flight Recorder 更适合保存最近一段运行数据。

先选对截取方式:手动窗口还是故障后回看

runtime/trace 的常规 API 很适合测试、基准或能提前圈定的请求:打开追踪,执行目标工作,再停止追踪。官方文档明确说明,Start 会在程序级别启用追踪,重复启用会返回错误;Stop 返回前会等待追踪写入完成。

我通常按下面的约束选方案:

方案适用场景窗口特点主要代价
trace.Start/Stop能在请求进入前命中从触发点到返回点进程级单占用,不能并行采集
Flight Recorder慢请求结束后才知道异常保留最近运行窗口,再快照需要控制内存窗口与快照并发
Go runtime/trace 请求选择、Start Stop 与 Flight Recorder 最近窗口的静态关系说明图
图1:追踪窗口说明图,比较选定请求落盘与最近窗口快照的适用边界。

不要把 trace.Start 当成请求级开关反复套在每个并发请求上。服务里多个请求会同时到达,第二个请求拿不到追踪权时,应该继续正常处理,并把“本次未采集”作为诊断日志,而不是重试到影响业务。

用进程级闸门包住关键请求窗口

下面的包装函数只展示采集边界。生产环境还应把触发条件放在受保护的诊断入口、灰度标记或明确的请求头上,避免普通流量随意打开追踪。

package tracewindow

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

var traceMu sync.Mutex

func captureWindow(name string, work func() error) error {
    traceMu.Lock()
    defer traceMu.Unlock() // 进程级互斥,避免多个请求同时 Start

    fileName := fmt.Sprintf("trace-%s-%d.out", name, time.Now().UnixNano())
    file, err := os.OpenFile(fileName, os.O_CREATE|os.O_WRONLY|os.O_TRUNC, 0o600)
    if err != nil {
        return fmt.Errorf("创建追踪文件失败: %w", err)
    }
    defer file.Close() // trace.Stop 的 defer 先注册,故会先完成写入

    if err := trace.Start(file); err != nil {
        return fmt.Errorf("启动追踪失败: %w", err)
    }
    defer trace.Stop() // Stop 返回前等待追踪数据写完

    return work() // 只把被选中的请求处理函数放进窗口
}

func runSelectedRequest(w io.Writer) error {
    return captureWindow("slow-request", func() error {
        _, err := io.WriteString(w, "handle selected request")
        return err // 业务错误与追踪错误分开处理
    })
}

这里的互斥锁不是为了让业务请求串行,而是只保护诊断窗口:未命中的请求直接走正常处理;命中的请求如果拿不到锁,也应快速放弃采集。示例为了突出文件顺序使用阻塞锁,接入 HTTP 服务时可以改成 TryLock 或带超时的占用策略。

文件落盘与 Stop 顺序要固定

这个顺序有三个关键点:先获得可写文件,再启动 trace;请求结束后先 Stop,最后关闭文件。因为 Stop 可能还要把缓冲内容写入 io.Writer,提前关闭文件会让结果不完整。文件名也应包含一次诊断的唯一标识,避免并发排查时覆盖上一份结果。

func saveTrace(path string, handle func() error) (err error) {
    file, err := os.OpenFile(path, os.O_CREATE|os.O_WRONLY|os.O_TRUNC, 0o600)
    if err != nil {
        return err // 目录或权限不对时,不进入追踪状态
    }
    defer func() {
        if closeErr := file.Close(); err == nil {
            err = closeErr // 只在业务和追踪都成功时回传关闭错误
        }
    }()

    if err = trace.Start(file); err != nil {
        return err
    }
    defer trace.Stop() // 先注册 Stop,保证它早于 file.Close 执行

    return handle() // handle 返回后形成一个明确的请求窗口
}
Go os.OpenFile、trace.Start、请求处理函数、trace.Stop、file.Close 与 go tool trace 的静态关系图
图2:文件落盘结构图,查看 trace.Stop、文件句柄与本地分析入口的关系。

需要注意,trace.Stop 本身没有错误返回值;真正的文件创建、写入或关闭错误要靠文件操作和业务包装记录。采集成功后,可以在诊断机器上执行:

# 使用 Go 工具打开已经落盘的执行追踪
go tool trace trace-slow-request.out

这一步只负责分析文件,不代表文件中只有目标请求。窗口内其他 goroutine 的调度、系统调用、GC 等运行时事件也可能出现;正因为如此,触发条件和窗口长度要尽量精确。

几个容易误判的边界

  • Start 返回错误:优先判断进程里是否已有其他 trace 或诊断组件占用,不要把空文件当成有效结果。
  • 慢请求结束后才发现:常规 Start/Stop 已经错过了前因,Go 1.25 可考虑预先启动 Flight Recorder,在异常点调用 WriteTo 快照。
  • 采集范围过大:锁住的是诊断窗口,不是业务全局;同时要限制触发权限、文件目录和保留数量。
  • 窗口太短:只包住写响应的最后几行,可能看不到等待、锁竞争或系统调用,触发点应放在请求边界之前。

常见问题

trace.Start 能不能在每个 HTTP 请求里直接调用?

不建议。它是进程级追踪,同一时刻只能有一个活动调用;应先筛选请求,再用闸门只采集一个窗口。

为什么 trace.Stop 后还要再关闭文件?

Stop 负责结束追踪并等待写入完成,文件句柄仍由调用方拥有,所以仍需显式关闭它。

必须提前知道哪个请求会慢吗?

使用 Start/Stop 时基本需要。若异常只能事后识别,就用 Flight Recorder 保存最近窗口,再按需写出快照。

实际排查时,我会把“谁能触发、一次窗口多长、文件写到哪里、谁负责清理”写成四项清单。满足这四项,runtime/trace 才会从一份很大的运行日志,变成围绕关键请求的可读证据。

声明:本文转载于:17golang原创 如有侵犯,请联系study_golang@163.com删除
相关阅读
更多>
最新阅读
更多>
课程推荐
更多>