分布式追踪在生产环境的落地
Trace 是把复杂系统讲明白的唯一办法。从采样策略到上下文传播,从跨服务到跨队列的 span 串联,本文分享把 trace 系统从「看着热闹」做到「真的有用」的工程经验。
Trace 不是日志的替代,而是另一种叙事
日志是「此刻发生了什么」,指标是「一段时间内发生了什么趋势」,Trace 则是「一个请求从进入到返回的完整旅程」。它存在的意义是:当系统变复杂,单凭指标与日志已经无法定位问题。
很多人以为 Trace 只是一个「自动化日志」,其实它最大的价值在于「路径结构化」。一个请求经过网关 → A → B → C → MQ → B' → 返回。每个节点都有自己的耗时与属性,工程师一眼就能看到「卡在哪一段」。这与日志不同,日志是扁平的、没有父子关系的。
Trace 的核心概念,几个新词
- Span:一个工作单元,相当于一帧操作。Span 有开始时间、结束时间、属性(attribute)和事件(event)。
- Trace:一组共享 trace_id 的 Span 集合,构成「一棵调用树」。
- Context Propagation:把 trace_id / span_id 在服务边界传递。最常见通过 HTTP 头或 gRPC metadata。
- Sampling:在大量请求中只记录一部分,避免存储被打爆。
只要熟悉这四件事,剩下的细节都是工程实现。
采样策略:决定你的 trace 是否值钱
很多团队 trace 系统做不下去,是因为采样策略不合适。最常见的错误是「全采样」。每个请求都记录,存储与查询双双爆掉。但低采样率又会让「偶发问题」找不到样本。
| 策略 | 采样率 | 成本 | 适用 |
|---|---|---|---|
| 全采样 | 100% | 极高 | 不推荐生产 |
| 恒定比例 | 1%–10% | 低 | 流量稳定 |
| 头部采样 | 首 span 决定 | 中 | 采样一致性高 |
| 尾部采样 | 慢请求采 100% | 中 | 保留「异常」 |
| 自适应采样 | 按错误率动态 | 中 | 复杂业务 |
我推荐的是「头部 + 尾部组合」。入口网关按 5% 头部比例采样;下游服务记录时,如果发现错误或慢请求,标记为「keep」并要求全链路保留。这种配合让采样变得可预期。
上下文传播:坑最多的地方
上下文传播是 trace 系统里「细节决定成败」的地方。一次请求跨协议、跨语言、跨队列时,trace 是否串得起来全靠它。
几个高频坑:
- MQ 的 trace 断:消息进 RabbitMQ 或 Kafka,header 没继承。需要在生产端注入 trace_id,消费端解析出来。
- 异步线程丢失上下文:Java 用 ThreadLocal,线程池复用会丢。需要用
InheritableThreadLocal或显式拷贝。 - HTTP 客户端覆盖:某些客户端会清理所有 header,需要显式注入 W3C Trace Context。
// Go 中保留 trace context 跨 goroutine
ctx, span := tracer.Start(ctx, "handleRequest")
defer span.End()
go func(ctx context.Context) {
_, sub := tracer.Start(ctx, "background-task")
defer sub.End()
doWork()
}(trace.ContextWithSpanContext(ctx, span.SpanContext()))
属性与事件:让 Span 真正可解释
没有属性的 Span 只是一个「耗时记录」。一个完整的 Span 应当附带以下信息:
- 业务属性:用户 ID(脱敏后)、订单号、链路类型;
- 技术属性:DB 类型、HTTP 方法、返回码;
- 关键事件:缓存命中、锁等待、外部调用结果。
属性越详细,troubleshooting 时越能定位。但要小心「属性爆炸」:每个 Span 上百个字段会让查询变慢。建议每个 Span 的属性在 5–15 个之内。
Trace 数据的存储:不是「所有后端都行」
Trace 的数据形态与日志、指标都不一样。它是高基数、低延迟、按 trace_id 关联的组合查询。所以不能用 ELK 简单替代。常见选择:
- Jaeger + Cassandra / ES:经典组合,部署稍重。
- Zipkin + MySQL / ES:简单但生态老。
- OpenTelemetry Collector + Tempo / ClickHouse:较新,写入性能好。
- 云厂商方案:如阿里 ARMS / AWS X-Ray,最省心但绑定。
我们最终选用 OTel Collector + ClickHouse。理由:写入 QPS 高、单次 trace 关联查询快、成本可控。
从 Trace 到「真正的可观测」
Trace 是三大支柱中最难落地的一支,因为它的实现成本最高、价值最容易花掉。常见的失败方式是「建设投入很大、查询实际不使用」。要避免这个结果,我的经验是:
- 先选 3 个最有问题的链路,把它们的 trace 「打通」;
- 然后每次故障复盘带上 trace ID;
- 最后把 trace 与告警关联起来——慢 trace 自动告警;
- 推动 trace 上线成为发布门禁的一项。
经过半年迭代,trace 在我们这儿从「只有少数人看」变成了「不查 trace 不算定位完成」。
值得收藏的几条原则
- 设计一个统一
traceId字段用于跨系统联动。 - 采样策略必须写在文档里,所有团队遵守。
- 上下文传播要测试用例覆盖,不能只靠经验。
- 属性定义要有 schema,避免重复定义。
- 把 trace 与 RUM、APM 联动起来。
Trace 不是「加 SDK」就完事的工程。它是一整套团队纪律,但回报也是实打实的——当你能准确说出「这次慢是因为 Redis 偶尔的 fork 阻塞 300ms」,一切就值了。