Golang网络请求监控 记录API调用的各项耗时

梦枫小哥_6009

梦枫小哥_6009

2026-06-10

910人浏览

原创

唯一能获取全链路耗时的入口是自定义http.roundtripper包装defaulttransport,在roundtrip入口用time.now()打点,通过httptrace捕获dns、连接、tls、ttfb等阶段,并包装resp.body在close()中记录读取耗时,同时用context透传traceid确保归属清晰。

golang网络请求监控 记录api调用的各项耗时

用 http.RoundTripper 包装 DefaultTransport 是唯一能拿到全链路耗时的入口

标准 http.Client.Do 或业务层打点,只能覆盖「发起请求」到「收到 *http.Response」之间,漏掉 DNS 解析、连接池等待、TLS 握手、响应体流式读取等关键阶段。真正可控、可埋点、可透传 context 的位置,只有 http.RoundTripper.RoundTrip 方法。

别改 http.DefaultTransport 字段——它是全局可变结构体,高并发下不安全;正确做法是嵌套它:

type TimingRoundTripper struct {
    base http.RoundTripper
}
func (t *TimingRoundTripper) RoundTrip(req *http.Request) (*http.Response, error) {
    start := time.Now()
    resp, err := t.base.RoundTrip(req)
    if err != nil {
        recordError(req.URL.Host, time.Since(start).Milliseconds())
        return resp, err
    }
    resp.Body = &timingReadCloser{
        ReadCloser: resp.Body,
        req:        req,
        start:      start,
    }
    return resp, nil
}
  • base 必须是线程安全的 transport(如 http.DefaultTransport),不能是临时 new 出来的
  • 必须在 RoundTrip 入口调用 time.Now(),不能依赖 req.Context().Deadline() 等异步时间源
  • 返回 resp 前必须包装 resp.Body,否则无法捕获 Read()Close() 阶段耗时

包装 resp.Body 才能测准响应体读取耗时

很多人以为拿到 resp 就算请求完成,其实不然。大 JSON、流式接口(如 SSE)、慢网络下,resp.Body.Read() 可能阻塞数秒——这部分耗时必须计入总链路。

直接实现 io.ReadCloser,在 Close() 里补全终点时间:

type timingReadCloser struct {
    io.ReadCloser
    req   *http.Request
    start time.Time
}
func (r *timingReadCloser) Close() error {
    defer r.ReadCloser.Close()
    duration := time.Since(r.start).Milliseconds()
    recordFullDuration(r.req.URL.Host, r.req.Method, duration)
    return nil
}
  • 不要只包装 Read():流式响应可能多次调用 Read(),总耗时不等于各次之和
  • 必须在 Close() 里打终点——这是 Go HTTP 客户端约定的“响应处理真正结束”信号
  • 如果业务提前 io.Copy(ioutil.Discard, resp.Body) 就丢弃 body,Close() 仍会被调用,逻辑依然成立

用 httptrace 获取 DNS、连接、TLS 等细分阶段耗时

httptrace 是 Go 标准库(1.8+)提供的轻量级追踪工具,比自己覆写 net.Resolvercrypto/tls.Conn 更安全、更便携。

Golang Lint
Golang Lint

Golang 项目 lint 最佳实践与 golangci‑lint 配置——运行 linter、编辑 .golangci.yml、使用 nolint指令抑制警告。

下载

RoundTrip 中注入 httptrace.ClientTrace

trace := &httptrace.ClientTrace{
    DNSStart: func(info httptrace.DNSStartInfo) {
        recordStage(r.URL.Host, "dns_start", time.Now())
    },
    DNSDone: func(info httptrace.DNSDoneInfo) {
        recordStage(r.URL.Host, "dns_done", time.Now())
    },
    ConnectStart: func(network, addr string) {
        recordStage(r.URL.Host, "connect_start", time.Now())
    },
    GotFirstResponseByte: func() {
        recordStage(r.URL.Host, "ttfb", time.Now())
    },
}
req = req.WithContext(httptrace.WithClientTrace(req.Context(), trace))
  • DNSStart/DNSDone 能真实反映解析耗时,但注意:若启用了连接复用,后续请求不会触发 DNS 回调
  • GotFirstResponseByte 对应 TTFB(Time to First Byte),是服务端处理能力的关键指标
  • TLSHandshakeStart/TLSHandshakeDone 只在启用 TLS 时触发;未加密 HTTP 下这两个回调不会被调用
  • 所有回调函数运行在 client goroutine 内,无需额外同步

context 透传 traceID,别用全局 map 存耗时数据

如果请求链路含重试、子请求或中间件跳转,仅靠局部变量记录时间会丢失归属。必须把 traceID 挂进 context.Context,再由中心化 tracker 查表补全详情。

客户端示例(发起请求前注入):

ctx := context.WithValue(req.Context(), "trace_id", "abc123")
req = req.WithContext(ctx)

服务端中间件中提取并写入日志或 metrics:

if tid, ok := r.Context().Value("trace_id").(string); ok {
    logFields = append(logFields, "trace_id", tid)
}
  • 绝对不要用 map[string]time.Time 全局缓存 traceID → 时间戳映射——高并发下易冲突、无法清理、GC 压力大
  • 避免往 context 里传指针或可变结构体;只传 string 类型 traceID,再由 tracker 通过 ID 查结构化耗时数据
  • 若用 OpenTelemetry,优先走 otelhttp.Transport,但它默认不暴露原始毫秒值,需加 otelhttp.WithSpanOptions(trace.WithAttributes()) 注入

真正难的不是打点,而是确保每个阶段时间戳归属清晰、不被协程调度干扰、且能在 panic 或超时后仍准确归因——这要求从 RoundTrip 入口开始就绑定 context,并把所有耗时采集收敛到一次 Close() 调用里。

大量免费API接口:立即使用
涵盖生活服务API、金融科技API、企业工商API、等相关的API接口服务。免费API接口可安全、合规地连接上下游,为数据API应用能力赋能!

相关专题

更多
golang如何定义变量
golang如何定义变量

golang定义变量的方法:1、声明变量并赋予初始值“var age int =值”;2、声明变量但不赋初始值“var age int”;3、使用短变量声明“age :=值”等等。本专题为大家提供相关的文章、下载、课程内容,供大家免费下载体验。

2024.02.23

479

5

golang有哪些数据转换方法
golang有哪些数据转换方法

golang数据转换方法:1、类型转换操作符;2、类型断言;3、字符串和数字之间的转换;4、JSON序列化和反序列化;5、使用标准库进行数据转换;6、使用第三方库进行数据转换;7、自定义数据转换函数。本专题为大家提供相关的文章、下载、课程内容,供大家免费下载体验。

2024.02.23

576

5

golang常用库有哪些
golang常用库有哪些

golang常用库有:1、标准库;2、字符串处理库;3、网络库;4、加密库;5、压缩库;6、xml和json解析库;7、日期和时间库;8、数据库操作库;9、文件操作库;10、图像处理库。本专题为大家提供相关的文章、下载、课程内容,供大家免费下载体验。

2024.02.23

956

5

golang和python的区别是什么
golang和python的区别是什么

golang和python的区别是:1、golang是一种编译型语言,而python是一种解释型语言;2、golang天生支持并发编程,而python对并发与并行的支持相对较弱等等。本专题为大家提供相关的文章、下载、课程内容,供大家免费下载体验。

2024.03.05

751

5

golang是免费的吗
golang是免费的吗

golang是免费的。golang是google开发的一种静态强类型、编译型、并发型,并具有垃圾回收功能的开源编程语言,采用bsd开源协议。本专题为大家提供相关的文章、下载、课程内容,供大家免费下载体验。

2024.05.21

1406

6

golang结构体相关大全
golang结构体相关大全

本专题整合了golang结构体相关大全,想了解更多内容,请阅读专题下面的文章。

2025.06.09

3874

18

golang相关判断方法
golang相关判断方法

本专题整合了golang相关判断方法,想了解更详细的相关内容,请阅读下面的文章。

2025.06.10

1714

13

golang数组使用方法
golang数组使用方法

本专题整合了golang数组用法,想了解更多的相关内容,请阅读专题下面的文章。

2025.06.17

3706

17

golang数组大全
golang数组大全

本专题整合了golang数组相关问题解决方法,想了解更多相关内容,请阅读下面的文章。

2025.06.20

4166

22

热门下载

更多
网站特效
/
网站源码
/
网站素材
/
前端模板

精品课程

更多
相关推荐
/
热门推荐
/
最新课程