gin-contrib/zipkin 必须作为第一个中间件注册,否则因上下文注入失败导致 trace 断裂;需手动传播 b3 header、panic 时显式标记 error、全局复用 reporter 并在退出前 close。

直接用 gin-contrib/zipkin 是最省事的路径,但必须注意它和 zipkin-go 原生库不是一回事——前者是专为 Gin 封装的中间件,后者是底层 tracer 实现;混用或错放注册顺序,trace 就会断在第一个 handler 里。
为什么 gin-contrib/zipkin 必须最早注册
这个中间件依赖 r.Request.Context() 注入 span 上下文,而 Gin 的 engine.Use() 执行顺序决定了中间件链的起始点。如果把它放在 router.GET() 之后、或夹在 logger 和 auth 中间,那请求进来的第一段 path(比如 /api/v1/users)就捕获不到,后续所有子 span 都变成孤立 trace。
- 正确写法:
engine.Use(zipkin.Middleware(...))必须是engine.Use()调用中的第一个 - 错误写法:先
engine.Use(logger()),再engine.Use(zipkin.Middleware(...))—— 此时logger()已经消费了原始 request,span 拿不到完整 path - 别指望靠
c.Next()补救:中间件链是线性执行的,顺序错了,上下文就漏了
跨服务调用时 B3 header 必须手动传播
gin-contrib/zipkin 只负责 HTTP 入口的 span 创建和上报,不自动处理 outbound 请求的 header 注入。你用 http.Client 调下游服务时,如果没显式把当前 span 的 context 写进 X-B3-TraceId 等头,链路就在本机结束。
- 发起请求前,从 Gin Context 提取 span:
span := zipkin.SpanFromContext(c.Request.Context()) - 写入 headers:
span.Context().WriteHTTPHeaders(&headers) - 别用
context.WithValue(c.Request.Context(), key, span):这只能在当前 goroutine 生效,http.Client.Do()启的新协程拿不到 - 更稳的做法:封装一个带 tracer 的 client,每次
Do()前统一注入
panic 场景下 span 不会自动标 error
Gin 默认 recover 中间件会吞掉 panic,但 gin-contrib/zipkin 不监听这个事件,所以即使 handler 崩了,span 还是 status=200,看不出异常。
- 必须自己加 recover + span 标记:
defer func() { if r := recover(); r != nil { span.Tag("error", "true"); span.Tag("error.object", fmt.Sprintf("%v", r)); span.Finish(); } }() - 注意
span.Finish()要在 recover 里调,否则 defer 已随 panic 中断,不会执行 - 别只打日志:光记录 panic 不等于链路中标记失败,Zipkin UI 里看不到 error tag 就没法按错误率聚合
全局 reporter 复用与进程退出清理
每次新建 http.NewReporter("http://zipkin:9411/api/v2/spans") 都会启动独立 goroutine 和连接池,跑几天就内存泄漏。这不是功能问题,是资源管理疏忽。
- reporter 必须定义为包级变量,初始化一次,全局复用
- tracer 也一样:
tracer := zipkin.NewTracer(reporter, zipkin.WithSampler(zipkin.CountSampler(100))),采样别用AlwaysSample,线上压测都扛不住 - 进程退出前(如收到 SIGTERM)必须调
reporter.Close(),否则最后一批 span 丢在缓冲区里不上报 - 没 Close 的典型现象:服务刚重启时 Zipkin UI 显示 trace 数突降,过几分钟才恢复——其实是上一周期的 span 滞后送达
最容易被忽略的是跨 goroutine 场景:比如你在 handler 里启了个 go sendNotification(ctx),却忘了把 span.Context() 传进去,那个 goroutine 的操作就彻底脱离链路。Zipkin 不会报错,只会安静地少一条 span。











