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 里的错误事件,最后核对引用关系是否连续。你想想看,任何一个环节出问题,排查效率都会大打折扣。
3.6 实战:我的一次误判案例
有一次线上告警说订单接口超时率飙升。我调出 Trace 一看,发现某个数据库查询 Span 的 duration 达到了 5 秒。我第一反应是数据库慢查询,赶紧让 DBA 去查。结果 DBA 告诉我那台库根本没这个查询。
再仔细看——那个 Span 的 parentSpanId 传丢了,导致它被错误地挂到另一个无关的根 Span 下。实际慢查询发生在另一个服务,只是引用错乱把它带到数据库这里了。
从那以后,我在排查时一定先看引用关系:把 trace 的树状图展开,核对每一个 Span 的 parentSpanId 是否连续。引用错了,再准的时间戳也救不了你。