3、追踪数据模型:Span概念、Trace概念、SpanContext、Baggage、事件与日志

好,咱们进入正题。这一章讲的是追踪系统的“骨架”——数据模型。说白了,就是追踪数据长什么样,怎么串起来,怎么传下去。

我在做列车控制系统的全链路追踪时,最头疼的不是怎么埋点,而是数据模型没设计好。模型一乱,后面查问题就像在垃圾堆里找针。所以,这一章我建议你仔细看,尤其是那些看起来不起眼的小字段,关键时刻能救命。

3.1 Span:追踪的最小工作单元

Span,翻译过来叫“跨度”。你可以把它理解成一次操作。比如,一次数据库查询、一次RPC调用、一次消息发送,都是一个Span。

每个Span都记录着:

  • 操作名称:比如“queryTrainSchedule”
  • 开始时间和结束时间:精确到微秒
  • 状态:成功还是失败
  • 标签(Tags):键值对,比如“trainId=G1234”

我个人习惯,在Span的Tags里至少放三个东西:请求的URL、响应的状态码、以及耗时。这样,哪怕不看日志,光看Span就能猜个八九不离十。

核心要点:Span是树形结构。一个Span可以有一个父Span,也可以有多个子Span。父Span结束了,子Span不一定结束。这在异步调用里特别常见。

举个例子,列车控制系统的“进站申请”流程:

Span A: 进站申请处理 (父Span)
    ├── Span B: 验证列车身份 (子Span)
    ├── Span C: 查询轨道占用 (子Span)
    └── Span D: 发送进站指令 (子Span)

你看,Span A是总入口,它等所有子Span都完成后,自己才结束。如果某个子Span超时了,父Span也会跟着超时。嗯,这里要注意,别让一个慢子Span拖垮整个链路。

3.2 Trace:一次完整的请求旅程

Trace,就是一组Span的集合。它们通过同一个Trace ID关联起来,描述了一次完整的请求从开始到结束的全过程。

我遇到过最典型的场景:列车调度系统里,一次“发车指令”下发,会经过前端、网关、调度中心、信号系统、车载设备。如果不用Trace串起来,你根本不知道是哪个环节慢了。

Trace有几个关键属性:

  • Trace ID:全局唯一,贯穿整个请求
  • Span ID:每个Span的唯一标识
  • Parent Span ID:指向父Span,形成树形结构

小技巧:Trace ID最好用UUID或者Snowflake算法生成。我曾经见过有人用时间戳+随机数,结果在高并发下重复了,查问题查了三天。你想想看,重复的Trace ID,等于所有请求混在一起,神仙也分不清。

3.3 SpanContext:跨进程传递的“信使”

SpanContext,说白了就是一组元数据。它负责把Trace ID、Span ID这些信息,从一个服务传到下一个服务。

在列车控制系统里,服务间通信通常用gRPC或者消息队列。SpanContext就藏在请求的Header里,跟着数据包一起走。

它的核心内容:

  • Trace ID:告诉下游“我是谁”
  • Span ID:告诉下游“我从哪来”
  • 采样标记:是否要记录这个Trace

我曾经踩过一个坑:某个服务在接收消息时,忘了把SpanContext从消息头里取出来。结果呢?所有下游服务的Trace都断了,变成了孤立的Span。查问题的时候,只能看到一个个“孤岛”,完全连不起来。

避坑指南:SpanContext的传递,一定要在框架层面做。比如gRPC的拦截器、HTTP的中间件。别指望每个开发人员都手动传,一定会漏。

3.4 Baggage:跨服务传递的业务上下文

Baggage,翻译过来叫“行李”。它和SpanContext类似,但存的是业务数据。比如用户ID、订单号、列车ID。

为什么要用Baggage?因为有些信息,你不想在每个Span的Tags里重复写,但又希望所有下游服务都能看到。

举个例子:

// 在入口服务设置Baggage
Baggage.set("trainId", "G1234");
Baggage.set("driverId", "D5678");

// 下游服务可以直接读取
String trainId = Baggage.get("trainId");

我个人建议,Baggage里只放“全局不变”的信息。比如列车ID、调度批次号。别放那些频繁变化的东西,比如时间戳、随机数。否则,Baggage会变得又大又乱,影响性能。

注意:Baggage是透传给所有下游服务的。如果你不小心放了个敏感信息,比如用户密码,那所有服务都能看到。嗯,这个后果你懂的。

3.5 事件与日志:给Span加点“细节”

Span本身只记录开始和结束,但中间发生了什么?这就需要事件(Event)和日志(Log)来补充。

事件:在Span的生命周期里,记录一些关键节点。比如:

  • “开始查询数据库”
  • “收到数据库响应”
  • “开始发送消息”

日志:更详细的记录,比如错误堆栈、参数值。日志通常会关联到Span的Span ID,方便定位。

我习惯的做法是:

  • 每个Span至少记录2-3个事件:开始、关键步骤、结束
  • 错误发生时,一定要记录日志,并且把错误信息写到Span的Tags里

举个例子:

// 记录事件
span.addEvent("查询轨道占用开始");
// ... 执行查询 ...
span.addEvent("查询轨道占用结束");

// 记录错误日志
if (error != null) {
    span.setStatus(Status.ERROR);
    span.addEvent("查询失败", Attributes.of("error.message", error.getMessage()));
}

你想想看,如果没有事件和日志,你只能看到“这个Span花了500ms”,但不知道这500ms里发生了什么。是网络延迟?还是数据库慢?还是业务逻辑卡住了?有了事件,你就能看到“查询数据库花了400ms,业务处理花了100ms”,问题一目了然。

个人经验:事件不要记太多。我见过有人一个Span记了50个事件,结果查问题的时候,事件列表比代码还长。建议控制在5个以内,只记关键节点。

3.6 数据模型在列车控制系统中的应用

最后,咱们把理论落到实际。在列车控制系统里,数据模型怎么用?

场景 Trace ID Span示例 Baggage示例
进站申请 req-20250321-001 验证身份、查询轨道、发送指令 trainId=G1234, stationId=北京南
发车指令 req-20250321-002 调度决策、信号下发、车载确认 trainId=G5678, driverId=D9012
故障报警 req-20250321-003 采集数据、分析故障、生成告警 deviceId=信号机-03, errorCode=E1001

你看,每个场景都有唯一的Trace ID,Span记录了具体操作,Baggage传递了业务上下文。这样,当问题发生时,你只需要一个Trace ID,就能把整个请求链路拉出来,从入口到出口,一个不落。

好了,这一章就到这里。数据模型是追踪系统的地基,地基打不好,后面盖什么楼都歪。下一章,咱们聊聊怎么把这些数据采集上来,也就是“埋点与采样”。