3. 追踪数据模型:时间戳、标签、日志、引用

一条完整的 Span,说白了就是一条带着「时间戳、标签、日志、引用」四个维度的数据记录。我早期做链路追踪时,总觉得把这些字段填上就行,结果一查问题才发现——字段填错了,数据根本对不上。今天我们就拆开这四样东西,看看它们各自是什么角色,怎么配合。

3.1 时间戳 —— 链路的骨架

时间戳定下了整个调用链的“时轴”。没它,你就不知道谁先谁后。每个 Span 需要记录 开始时间戳 和 结束时间戳,一般用毫秒或微秒精度。

字段 含义 常见格式
startTime Span 开始时刻 Unix 毫秒时间戳
endTime Span 结束时刻 Unix 毫秒时间戳
duration Span 持续时长 毫秒(endTime - startTime)

这里有一个常见坑:时钟偏差。服务 A 在机器 A,服务 B 在机器 B,如果两台机器时间不同步,那 A 记录的 startTime 可能比 B 的 startTime 还晚。我曾经在排查跨机房调用时,明明调用链路是 A→B→C,可时间线却显示 C 在 A 之前开始。后来加了 NTP 强制同步,才把数据捋顺。

小技巧:发送端和接收端尽量使用同一台时间服务器。如果精度要求高,可以考虑记录相对偏移,或者用 Tracer 自动打时间戳。

3.2 标签(Tags)—— 索引和过滤的命根子

标签是键值对,用来描述 Span 的特征。例如 HTTP 请求的方法、状态码、数据库的表名、SQL 语句。标签用于 搜索、过滤、聚合,所以设计时一定要想清楚哪些字段值得打上 Tag。

// 一个典型的 HTTP Span 标签
"http.method": "POST"
"http.status_code": 200
"http.url": "/api/order"
"error": "true"

我个人习惯把业务关键字段也打进去:用户 ID、订单 ID、设备 ID。有一次内存泄漏排查,我按 Tag 筛选出某个订单号对应的所有 Span,发现它在半小时内被调用了 3 万次,从而定位到循环调用 bug。

避坑指南:不要打太多标签!我有一次把整个请求体都塞进去,结果存储爆炸,查询还变慢。建议每个 Span 不要超过 10 个标签,值的长度也要控制。

3.3 日志(Logs)—— 事件的快照

日志不同于业务日志,它记录的是 Span 生命周期内发生的关键事件。比如一条请求什么时候开始处理、失败时的异常栈、发送消息时的消息体摘要。日志里可以挂载自定义字段。

// 一条日志事件
{
  "timestamp": 1627001234567,
  "fields": {
    "event": "cache_miss",
    "key": "user_1234"
  }
}

我还遇见过一种恶心情况:服务代码里抛了异常,但没把它记成日志事件,导致链路里看到 Span 正常结束,客户端却报超时。后来我强制要求每一个 catch 块必须写 span.log("异常"),才算彻底解决。

3.4 引用(References)—— 串联一切

引用定义了 Span 之间的关系。最常见的是 ChildOf 和 FollowsFrom。ChildOf 表示父 Span 等待子 Span 完成(同步调用),FollowsFrom 表示父 Span 不等待(异步消息)。

引用类型 说明 典型场景
ChildOf 子 Span 是父 Span 的一部分 RPC 同步调用
FollowsFrom 子 Span 由父 Span 触发,但独立执行 消息队列、异步任务
参考引用 仅关联,不构成父子关系 跨服务关联查询

引用是通过 TraceId + SpanId + parentSpanId 实现。每个 Span 记录自己的 parentSpanId,回溯形成树状结构。我调试过一个 bug:因为下游服务没传 parentSpanId,结果整条链路断成了几条孤立的 trace,根本看不出调用顺序。

核心原则:引用关系决定了链路图的形状。传错 parentSpanId = 拼错拼图。

3.5 数据模型综合视图

把这四个维度合起来,一条 Span 的数据结构大概长这样:

{
  "traceId": "abc123",
  "spanId": "span-001",
  "parentSpanId": "span-000",
  "operationName": "mysql.query",
  "startTime": 1627001000000,
  "endTime": 1627001000050,
  "tags": {
    "db.instance": "orders",
    "db.type": "mysql"
  },
  "logs": [
    { "timestamp": 1627001000020, "fields": { "event": "connection_acquired" } }
  ],
  "references": [
    { "refType": "CHILD_OF", "traceId": "abc123", "spanId": "span-000" }
  ]
}

我现在做排查,拿到一条异常链路,第一件事就是看时间戳有没有跳变,然后扫 Tags 和 Logs 里的错误事件,最后核对引用关系是否连续。你想想看,任何一个环节出问题,排查效率都会大打折扣。

Span 数据模型结构 Span 时间戳 标签 (Tags) 日志 (Logs) 引用 关键要点 • 时间戳决定调用顺序,时钟偏差是常见陷阱 • 标签用于索引和筛选,太多标签会拖垮性能 • 日志记录关键事件,异常一定要显式打点

3.6 实战:我的一次误判案例

有一次线上告警说订单接口超时率飙升。我调出 Trace 一看,发现某个数据库查询 Span 的 duration 达到了 5 秒。我第一反应是数据库慢查询,赶紧让 DBA 去查。结果 DBA 告诉我那台库根本没这个查询。

再仔细看——那个 Span 的 parentSpanId 传丢了,导致它被错误地挂到另一个无关的根 Span 下。实际慢查询发生在另一个服务,只是引用错乱把它带到数据库这里了。

从那以后,我在排查时一定先看引用关系:把 trace 的树状图展开,核对每一个 Span 的 parentSpanId 是否连续。引用错了,再准的时间戳也救不了你。