4.2 链路追踪:TraceID 贯穿
灰度要对照,排障要串链,都靠同一个东西:一条 TraceID 贯穿全部服务。我们基于 OpenTelemetry 做埋点,TraceID 在网关入口生成、随 gRPC metadata 透传、每个服务落到日志与指标标签,最后在 CLS 一条检索串出全链。这一篇把生成规则、透传细节、断链排障写全。TraceID 的规范与 go-micro-cps-guide 的观测章节对照,本篇只讲 17 个服务上的落地细节。
场景:断链是怎么发生的
| 断链场景 | 现象 | 根因 |
|---|---|---|
| 跨进程丢 | 下游日志没有 traceId | metadata 没透传 |
| 异步丢 | 消息队列消费端无 traceId | 生产端没写进消息头 |
| 日志断 | 有 traceId 但日志行没带 | 打日志用了本地 context |
三起都是「某一段没把 ID 传下去」,链路断一段,后面全瞎。1.1 那次 35 分钟排障的根因就是第二起:订单超时发生在异步对账线程里,traceId 没进消息头。
排障慢的另一半原因是日志散在 9 台机器上 grep(1.1 的数字)。TraceID 解决「串起来」,4.4 的 CLS 归集解决「找得到」,两篇合起来才是完整的排障提速。
方案:TraceID 规则定死
| 项 | 规则 |
|---|---|
| 生成位置 | 只在网关入口生成,内部服务只透传 |
| 格式 | 32 位 hex(16 字节随机加 4 字节时间戳) |
| SpanID | 每服务内生成 16 位 hex,随父 span 关系存 |
| 透传 | gRPC metadata 两个 key:trace-id、span-id |
| 落点 | 日志行、指标标签、消息头三处必带 |
时间戳进 traceId 的好处是 CLS 检索时能按 traceId 前 8 位粗筛时间窗,减少检索扫描量。16 字节随机保证 17 个服务并发下不撞号,碰撞概率按生日问题算在 10^15 次以内可忽略。
// 网关入口:生成 traceId 塞进 ctx,随 metadata 下发
traceID := newTraceID() // 16B 随机 + 4B 时间戳,32 hex
md := metadata.NewOutgoingContext(ctx, md, metadata.Pairs(
"trace-id", traceID,
"span-id", newSpanID(),
))
服务侧的 interceptor 统一做两件事:入站把 metadata 解进 ctx,出站从 ctx 取出来写 metadata。服务代码不碰 traceId,全在 govkit 的观测钩子里,1.4 篇定的观测层职责。
// govkit 观测 interceptor:入站解、出站注,服务代码零感知
func TraceUnary() grpc.UnaryServerInterceptor {
return func(ctx context.Context, req any, info *grpc.UnaryServerInfo, h grpc.UnaryHandler) (any, error) {
if id, ok := metadata.FromIncomingContext(ctx).Get("trace-id")[0]; ok && valid32Hex(id) {
ctx = withTrace(ctx, id, newSpanID()) // 续接上游链路
}
log.Bind(ctx, "traceId", TraceOf(ctx)) // 日志行必带
return h(ctx, req)
}
}
出站侧对称:从 ctx 取 traceId 与 spanId 写进 outgoing metadata,消息生产端写进消息头,HTTP 外呼写 X-Trace-Id。三段封装同一个 withTrace/TraceOf,保证出口不遗漏。
透传要覆盖三类出口:gRPC 同步调用走 metadata、消息队列走消息头、HTTP 外呼(第三方网关)走 X-Trace-Id 头。三类出口的透传封装都在 govkit 里,漏掉任何一类就断在那类出口上——我们断链事故有三起里有两起源于第三类。
配置与代码:采样与落点
采样策略:入口请求 100% 采样,内部调用 10% 采样,但命中任一「异常条件」(错误、超时、降级、灰度命中)强制 100%。异常链全量,正常链抽样,存储成本压到 1/8 而不丢排障能力。采样判定在网关入口一次算完,随 traceId 一起透传,下游不重复判。
| 落点 | 内容 | 用途 |
|---|---|---|
| 日志行 | traceId=xxx spanId=yyy |
CLS 全文检索 |
| 指标标签 | 延迟直方图带 trace 维度 | 4.3 看板 |
| 消息头 | 异步链路续接 | 跨进程不断链 |
CLS 里一条 traceId 串 5 个服务:traceId: 9f3a... | select ts, svc, span, err order by ts,35 分钟的排障现在 10 分钟内讲得清楚。异常链 90 天保留是为了复盘灰度事故与压测尖峰,正常链 30 天足够日常排障。
踩坑:四个断链教训
第一,异步丢 ID。消息队列消费端没读消息头的 traceId,对账链路断在异步处。修法是生产者把 traceId 写进消息头、消费者重建 ctx,两端的约定写进 govkit 的 MQ 封装,新接入的异步场景强制走封装。
第二,采样率配置漂移。内部 10% 采样被某服务本地覆盖成 100%,存储翻倍一周才发现。采样配置只能走 etcd 统一下发(1.4 的口径),本地覆盖的入口关掉,改采样要过 4.3 评审。
第三,ID 混用。早期有人拿 requestId(业务单号)当 traceId 塞进日志,CLS 检索把业务号与链路号混在一起,排障时两条线打架。规则:traceId 只认 metadata 里的 32 位 hex,业务单号走 err_code 与 msg 字段,CLS 的检索模板按字段分开。
第四,跨服务版本不一致。2.2 说 proto 只加不改,但观测层也适用:某服务升 OT SDK 后把 span 命名改了,下游看板按旧命名聚合直接断图。span 命名进契约,改名字走 4.3 看板的双周冻结期。
断链排障实操:四步定位
断链不用猜,按四步定位,每步都有固定动作:
| 步 | 动作 | 判据 |
|---|---|---|
| 1 | CLS 搜 traceId 全链 | 看缺哪个服务段 |
| 2 | 缺段服务的入口日志 | 有没有收到该 traceId |
| 3 | 上一跳出口日志 | 透传是否发出 |
| 4 | 出口类型对表 | metadata/消息头/HTTP 头哪类漏了 |
第 3、4 步是快慢的分界:多数断链出在「上一跳漏发」,对出口类型表一查即中。17 个服务里 14 个走 gRPC metadata、3 个纯异步走消息头,排障时先判类型再查代码,比逐行 grep 快一个量级。
这套排障动作的脚本化版本(CLS 检索串自动生成、断链服务高亮)见 rust-logpipe-guide 的归集章节;订单域异步对账的链路重建对照 java-order-guide 的链路设计。
验收:链路通不通一条命令看
CI 里加了一个链路冒烟:网关打一条请求,断言返回体带 traceId,同时断言该 traceId 在 CLS 里能串出全部 17 个服务至少 1 段。任何一段缺失即红灯,合并前必过。这条冒烟把「断链」从生产事故降级成提交前缺陷,1.1 的 35 分钟排障不再复现。
参数速查
| 参数 | 取值 | 说明 |
|---|---|---|
| traceId 长度 | 32 hex | 16B 随机 + 4B 时间戳 |
| 入口采样 | 100% | 网关入口全采 |
| 内部采样 | 10% | 异常链强制全采 |
| 透传 key | trace-id、span-id | gRPC metadata |
| CLS 保留 | 30 天 | 异常链 90 天 |
小结
TraceID 四句话:只在网关生成、metadata 透传加消息头续接、异常链全量采样、ID 与业务号分字段。断链的三起全在「某段没传下去」,所以落点三处(日志、指标、消息头)缺一即断。下一篇 4.3 把 QPS、延迟、错误率三条线接上看板,TraceID 是它们的检索入口。
延伸阅读
- 链路追踪的更多设计与采样策略,见 go-micro-cps-guide。
- 观测数据入云产品的更多场景,见 tencent-cloud-practice。
- 日志与追踪归集实践,见 rust-logpipe-guide。
