可观测性:分布式链路追踪
分布式链路追踪(Distributed Tracing)记录一次请求或业务动作在多个服务中的执行路径。它把各进程中的局部操作关联起来,用于定位延迟来源、错误传播路径和服务依赖关系。
日志擅长解释离散事件,指标擅长展示整体趋势;Trace 则提供一次请求的因果上下文和时间结构。
核心概念
Trace
Trace 表示一个端到端操作,由共享同一 trace_id 的多个 Span 组成。例如,“用户提交订单”可以是一条 Trace。
Trace 经常以父子树展示,但现实中的异步批处理、扇入和扇出可能需要 Span Link 表达多对多关系,因此数据模型不一定只是简单的树。
Span
Span 表示一个工作单元,例如:
- 接收一次 HTTP 请求;
- 发起一次 RPC 调用;
- 执行一次数据库查询;
- 发布或消费一条消息;
- 执行“库存预占”这样的业务步骤。
一个 Span 通常包含名称、开始时间、持续时间、状态、属性、事件、父 Span ID 和 Link。异常堆栈适合记录为 Span Event 或关联日志,而不是塞进高基数的指标标签。
Span Context
Span Context 是需要跨进程传播的最小身份信息,核心包括:
trace_id:标识整条 Trace;span_id:标识当前 Span;- Trace Flags:包括采样提示;
- Trace State:携带厂商扩展状态。
Resource
Resource 描述产生 Span 的实体,例如 service.name、service.version、容器、Pod、区域和环境。没有稳定的资源属性,同名 Span 很难被正确归属。
Baggage
Baggage 是随上下文传播的业务键值对,但不会自动成为 Span 属性。它会跨越服务边界并增加请求体积,因此不应放入密码、Token、个人信息或无界大字段。
一条 Trace 如何形成
假设网关调用订单服务,订单服务再访问库存和数据库:
Trace 4bf92f3577b34da6a3ce929d0e0e4736
└─ gateway: POST /orders
└─ order-service: create-order
├─ inventory-service: reserve-stock
└─ database: INSERT orders实际自动埋点可能同时产生客户端 Span 和服务端 Span,所以界面中会看到更细的层级。概念流程如下:
- 入口收到请求,没有合法的上游上下文时创建根 Span。
- 调用下游前创建子 Span,并把当前 Span Context 注入载体。
- 下游提取上下文,以调用方 Span 为父级创建新 Span。
- Span 完成后异步导出,不应让追踪后端故障阻塞业务请求。
- 后端按
trace_id聚合 Span,并展示瀑布图或依赖关系。
W3C Trace Context
HTTP 场景应优先使用 W3C Trace Context 标准。traceparent 的格式为:
traceparent: version-trace_id-parent_id-trace_flags示例:
traceparent: 00-4bf92f3577b34da6a3ce929d0e0e4736-00f067aa0ba902b7-01其中 00 是版本,32 个十六进制字符是 Trace ID,16 个十六进制字符是调用方当前操作的 ID,01 表示 sampled 标志已设置。tracestate 是可选的厂商扩展字段。
注意安全边界:来自公网的 Trace Context 是不可信输入。入口应校验格式、限制 Baggage,并避免让外部调用者任意操纵采样成本或把恶意字段传播到内部系统。
同步与异步传播
| 场景 | 载体 | 关系 |
|---|---|---|
| HTTP | traceparent、tracestate Header | 通常为父子关系 |
| gRPC | Metadata | 通常为父子关系 |
| 消息队列 | Message Headers / Properties | 可能为父子关系或 Span Link |
| 进程内异步任务 | 语言运行时 Context | 需要跨线程或协程正确传递 |
消息消费的生命周期可能远长于生产请求。对于批量消费、多生产者合并或需要独立保留期的流程,创建新 Trace 并用 Span Link 指向生产端,通常比构造一棵巨大且长期不结束的父子树更清晰。
埋点策略
自动埋点
自动埋点通过 Agent、SDK 集成或框架插件捕获 HTTP、gRPC、数据库和消息队列调用。它适合快速获得技术调用图,并统一传播上下文。
手动埋点
手动埋点表达业务边界,例如“计算优惠”“库存预占”“风控检查”。Span 名称应稳定、低基数:
推荐:POST /orders
推荐:order.create
避免:POST /orders/ord_8f72d1
避免:为用户 10392 创建订单订单号、用户类型等动态值应放入属性,并经过敏感性与基数评估。
避免重复和过度埋点
- 不要在已经自动埋点的 HTTP 调用外再创建语义相同的手动 Span。
- 不要给每个普通函数都创建 Span;优先选择远程调用、显著耗时和业务边界。
- 统一 Semantic Conventions,避免
http.method、method、request_method并存。 - 记录错误状态时同时保留异常类型和必要事件,但避免重复打印多份堆栈。
采样:成本与诊断价值的平衡
全量追踪会产生显著的网络、处理和存储成本,因此高流量系统通常需要采样。
头部采样
头部采样在请求开始或 Trace 尚未完整时作决定。
优点:
- 实现简单、资源开销低;
- 可根据 Trace ID 做一致概率采样,避免只保留半条 Trace。
局限:
- 作决定时还不知道请求最终是否失败或变慢;
- 可能错过低频但重要的异常。
尾部采样
尾部采样在 Collector 收集到一条 Trace 的全部或大部分 Span 后再决定,能够优先保留:
- 包含错误的 Trace;
- 总延迟超过阈值的 Trace;
- 新版本、特定租户或低流量关键接口的 Trace。
它需要暂存和按 Trace ID 汇聚数据,状态更多、延迟和资源成本更高。负载均衡必须确保同一 Trace 的 Span 能到达同一个采样决策点。
设计采样策略
- 低流量关键交易可以全量保留,高流量健康请求按比例采样。
- 错误和高延迟 Trace 提高保留率,但仍要设置全局容量上限。
- 记录实际采样率,避免从样本数量直接推断总请求量。
- 指标适合统计总体错误率和延迟分布,Trace 样本适合解释原因;不要互相替代。
采集与存储架构
图示参考:OpenTelemetry Collector、Dapper 论文 与各 Trace Backend 官方文档。
OpenTelemetry 负责产生、传播、处理和导出遥测数据,不是 Trace 存储后端。Collector 能让应用与具体后端解耦,并集中完成批处理、重试、脱敏和采样。
| 后端 | 主要特点 | 适用考虑 |
|---|---|---|
| Jaeger | 专注分布式追踪,提供查询和可视化 | 通用微服务追踪、OpenTelemetry 生态 |
| Zipkin | 架构简单,项目历史成熟 | 学习、已有 Zipkin/Brave 体系 |
| Grafana Tempo | 与 Grafana、Loki、Prometheus 关联紧密,使用对象存储 | 已有 Grafana 技术栈、大规模 Trace |
| Apache SkyWalking | 同时提供探针、拓扑、指标、日志和追踪能力 | 希望使用一体化 APM 平台 |
选型时应验证接收协议、属性检索、保留策略、多租户、对象存储或数据库成本,以及从指标和日志跳转到 Trace 的体验。
如何阅读一张 Trace 瀑布图
不要只寻找“最长的横条”,应按下面顺序判断:
- 根 Span 总耗时:是否与用户观测到的延迟一致?
- 关键路径:哪些串行 Span 真正决定端到端耗时?
- 空白时间:可能来自排队、线程池等待、锁竞争或缺失埋点。
- 重复调用:是否存在意外重试、N+1 查询或循环 RPC?
- 错误传播:错误从哪个 Span 开始,哪些上游只是被动失败?
- 服务端与客户端耗时差:差值可能来自网络、代理、连接池和排队。
- 属性差异:慢请求是否集中在特定版本、区域、接口或依赖。
一条 Trace 只能证明这次请求发生了什么。判断是否为系统性问题,还要回到指标查看频率和影响范围。
常见故障与排查
Trace 断裂
现象:下游出现新的 Trace ID。
检查:
- 客户端是否注入、服务端是否提取 W3C 上下文;
- 代理是否删除了 Header;
- 消息属性是否被序列化和消费框架保留;
- 跨线程、协程时是否丢失语言 Context。
Trace 只有一半
可能是部分服务未埋点、采样决定不一致、应用退出前未刷新、Exporter 被限流,或 Collector 队列溢出。应同时监控 Collector 的接收、拒绝、排队和导出失败指标。
Trace 数量突然下降
不要直接推断业务流量下降。先对照独立的请求指标,再检查采样配置、SDK、Collector 和后端写入限额。
查询成本过高
限制无界属性,把动态 ID 放在属性而不是 Span 名称中;根据查询需求设置保留周期和索引字段,并用指标先缩小时间、服务和版本范围。
实践练习
可以使用 OpenTelemetry Demo 观察一个多服务系统:
- 发起一次正常请求,找到根 Span 和关键路径。
- 根据
trace_id跳转到对应日志。 - 注入一个服务延迟,比较客户端 Span 与服务端 Span 的耗时。
- 注入错误,检查 Span 状态、事件和日志是否一致。
- 修改采样策略,观察错误 Trace 的保留率以及 Collector 自身指标。
学习自测
trace_id与span_id分别解决什么问题?- 为什么 Trace 通常表现为树,但异步系统中还需要 Span Link?
- 头部采样为什么可能错过错误请求,尾部采样又为什么更昂贵?
- 客户端 Span 比服务端 Span 多出 500 ms,可能有哪些原因?
- Trace 数量下降时,为什么要先对照独立请求指标?
参考资料
- W3C Recommendation:Trace Context
- OpenTelemetry:Traces
- OpenTelemetry:Context Propagation
- OpenTelemetry:Sampling
- OpenTelemetry:Collector
- Benjamin H. Sigelman 等:Dapper, a Large-Scale Distributed Systems Tracing Infrastructure
- OpenZipkin:Architecture
- Grafana Tempo:Documentation
- Apache SkyWalking:Documentation