上一篇【第29篇】Google Dapper论文精读——分布式追踪的
下一篇【第31篇】SkyWalking数据传输协议解析——探针与OAP的通信密码(未完待续)
1. 引言:为什么需要三层?
上篇文章我们精读了Dapper论文,它定义了Trace和Span两层模型。Zipkin和Jaeger都沿用了这个模型。但SkyWalking偏不——它加了一个中间层:TraceSegment,搞出了一个三层模型。
为什么要"多此一举"?是SkyWalking的设计者喜欢叠床架屋,还是真有什么场景二层模型搞不定?
这篇文章带你理解这个"多出来的一层"到底解决了什么问题。
2. 三层追踪模型概览
2.1 Trace → TraceSegment → Span
+------------------------------------------------------------------+ | SkyWalking 三层追踪模型 | +------------------------------------------------------------------+ | | | Trace (一次完整请求) | | ┌──────────────────────────────────────────────────────────┐ | | │ Trace ID: 7b2a8f1d9e3c.1625.1625000000001 │ | | │ │ | | │ TraceSegment (进程内的一次执行) │ | | │ ┌─────────────────────────────────────────────────┐ │ | | │ │ Segment ID: 7b2a8f1d9e3c.1625.1625000000002 │ │ | | │ │ Service: gateway-service │ │ | | │ │ │ │ | | │ │ EntrySpan: /api/order/create (3.2s) │ │ | | │ │ ├── LocalSpan: validateOrder (5ms) │ │ | | │ │ ├── ExitSpan: POST order-service/create (200ms)│ │ | | │ │ └── ExitSpan: Kafka produce (8ms) │ │ | | │ └─────────────────────────────────────────────────┘ │ | | │ │ | | │ TraceSegment (跨进程,另一个JVM进程) │ | | │ ┌─────────────────────────────────────────────────┐ │ | | │ │ Segment ID: 7b2a8f1d9e3c.1625.1625000000003 │ │ | | │ │ Service: order-service │ │ | | │ │ │ │ | | │ │ EntrySpan: POST /create (1.5s) │ │ | | │ │ ├── ExitSpan: MySQL INSERT orders (120ms) │ │ | | │ │ ├── ExitSpan: Redis SET order:orderId (5ms) │ │ | | │ │ └── LocalSpan: sendNotification (50ms) │ │ | | │ └─────────────────────────────────────────────────┘ │ | | └──────────────────────────────────────────────────────────┘ | +------------------------------------------------------------------+2.2 三层结构精确定义
| 层级 | 英文 | 定义 | 生命周期 |
|---|---|---|---|
| 第一层 | Trace | 分布式系统中完成一个业务请求的完整调用链 | 从用户请求到响应返回 |
| 第二层 | TraceSegment | 在一次Trace中,某个服务(JVM进程)处理的部分 | 一个JVM进程处理一次请求的过程 |
| 第三层 | Span | TraceSegment中的一个具体操作(RPC/DB/Cache/…) | 一个具体操作的起止 |
2.3 为什么需要TraceSegment?
Dapper的二层模型中,所有Span直接挂在Trace下。这在一个关键场景下有缺陷:
问题场景:多个Span属于同一个服务,但它们是哪个服务实例产生的?
+------------------------------------------------------------------+ | Dapper二层模型 vs SkyWalking三层模型 | +------------------------------------------------------------------+ | | | Dapper二层 (Trace → Span 直接父子关系): | | ┌──────────────────────────────────────────────────┐ │ | │ Trace ID: abc123 │ │ | │ │ │ | │ Span-1: [gateway:/api/order] │ │ | │ ├── Span-2: [gateway:validateOrder] │ │ | │ ├── Span-3: [order-service:createOrder] │ │ | │ └── Span-4: [order-service:insertOrder] │ │ | │ │ │ | │ 问题: Span-2和Span-3之间有个跨进程边界 │ │ | │ 但二层模型没有显式标记这个边界! │ │ | │ 你很难回答: "gateway的哪个实例处理的?" │ │ | └──────────────────────────────────────────────────┘ │ | | | SkyWalking三层 (Trace → TraceSegment → Span): | | ┌──────────────────────────────────────────────────┐ │ | │ Trace ID: abc123 │ │ | │ │ │ | │ TraceSegment-1 [gateway-service @ 10.0.1.5:8080] │ │ | │ ├── Span-1: EntrySpan /api/order │ │ | │ └── Span-2: ExitSpan → order-service │ │ | │ ↑ 通过Reference连接到下一个Segment │ │ | │ │ │ | │ TraceSegment-2 [order-service @ 10.0.1.6:8080] │ │ | │ ├── Span-1: EntrySpan /create (对应上游的调用) │ │ | │ └── Span-2: ExitSpan → MySQL │ │ | │ │ │ | │ ✓ 边界清晰!每个Segment代表一个服务实例的一次处理 │ │ | └──────────────────────────────────────────────────┘ │ +------------------------------------------------------------------+TraceSegment的核心价值:
- 明确的服务边界:每个Segment ∈ 一个服务实例
- 独立的数据上报单元:一个Segment作为一个批次发送给OAP
- 天然的服务粒度指标:服务平均响应时间 = Segment的响应时间
- 进程边界标记:Segment之间的Reference标记了跨进程/跨线程关系
3. Span的三种类型及语义
3.1 EntrySpan(入口Span)
定义:标记一个外部请求进入当前服务的入口点。
// SkyWalking Agent自动捕获@RestControllerpublicclassOrderController{@PostMapping("/api/order/create")publicResultcreateOrder(@RequestBodyOrderDTOorder){// Agent在这里创建 EntrySpan// Span类型: EntrySpan// Span名称: POST:/api/order/create// 这是外部(HTTP/RPC)请求进入当前服务的"大门"returnorderService.create(order);}}EntrySpan的特征:
- 永远是一个TraceSegment的第一个Span
- 标记了这个服务对外提供的接口
- 服务级别的响应时间 = EntrySpan的耗时
3.2 LocalSpan(本地Span)
定义:标记当前服务内部的方法调用,不涉及网络通信。
@ServicepublicclassOrderService{@Override@Trace(operationName="validateOrderLogic")// 可选:自定义追踪publicOrdercreate(OrderDTOdto){// Agent在这里创建 LocalSpan// Span类型: LocalSpan// 这是纯本地方法调用,不涉及任何外部依赖validateOrder(dto);// 本地调用calculateDiscount(dto);// 本地调用returnorderRepo.save(dto);// 这里会触发ExitSpan(DB)}privatevoidvalidateOrder(OrderDTOdto){// LocalSpan: validateOrderif(dto.getAmount()<=0){thrownewBusinessException("金额不能为0");}}}LocalSpan的特征:
- 纯本地调用,不涉及网络
- 帮助定位业务逻辑的性能瓶颈
- 可选配置:
plugin.springmvc.collect_http_params=false可以关闭LocalSpan以减少Agent开销
3.3 ExitSpan(出口Span)
定义:标记从当前服务发出的外部调用。
@RepositorypublicclassOrderRepository{@AutowiredprivateJdbcTemplatejdbc;publicOrdersave(Orderorder){// Agent在这里创建 ExitSpan// Span类型: ExitSpan// Component: MySQL/JDBI/PreparedStatement/executeUpdatejdbc.update("INSERT INTO orders(id, user_id, amount) VALUES(?,?,?)",order.getId(),order.getUserId(),order.getAmount());returnorder;}}// 另一个例子:调用外部REST服务@ServicepublicclassNotificationService{@AutowiredprivateRestTemplaterestTemplate;publicvoidsendSMS(Stringphone,Stringmessage){// Agent在这里创建 ExitSpan// Span类型: ExitSpan// Component: SpringRestTemplate// URL: http://sms-gateway/api/sendrestTemplate.postForObject("http://sms-gateway/api/send",newSMSRequest(phone,message),String.class);}}ExitSpan的特征:
- 标记了"离开当前服务"的调用
- 覆盖多种组件:HTTP/RPC/DB/Redis/MQ/…
- 是定位外部依赖问题的关键
3.4 三种Span类型的完整对照
+------------------------------------------------------------------+ +------------------------------------------------------------------+ | Span类型 │ 方向 │ 典型场景 │ Component │ +------------------------------------------------------------------+ | EntrySpan │ 入 │ HTTP/RPC请求入口 │ Tomcat, │ | │ │ MQ消息消费入口 │ SpringMVC, │ | │ │ │ KafkaConsumer│ +------------------------------------------------------------------+ | LocalSpan │ 内部 │ 方法调用 │ 无 │ | │ │ 业务逻辑处理 │ │ | │ │ 本地计算 │ │ +------------------------------------------------------------------+ | ExitSpan │ 出 │ 数据库访问 │ MySQL, │ | │ │ 缓存操作 │ Redis, │ | │ │ HTTP/RPC调用下游 │ HttpClient, │ | │ │ MQ消息发送 │ KafkaProducer│ +------------------------------------------------------------------+3.5 三种Span在代码中的创建方式
// SkyWalking Agent内部创建Span的逻辑(简化版)publicclassTracingContext{// 创建EntrySpan(HTTP请求到达时)publicAbstractSpancreateEntrySpan(StringoperationName){AbstractSpanentrySpan=newEntrySpan(spanIdGenerator.generate(),// spanIdparentSpanId,// 上游的parentSpanIdoperationName,// 如 "/api/order/create"owner// 所属的TraceSegment);entrySpan.setComponent(ComponentsDefine.TOMCAT);entrySpan.setLayer(SpanLayer.HTTP);activeSpanStack.push(entrySpan);returnentrySpan;}// 创建ExitSpan(调用外部时)publicAbstractSpancreateExitSpan(StringoperationName,StringremotePeer){AbstractSpanexitSpan=newExitSpan(spanIdGenerator.generate(),activeSpanStack.peek().getSpanId(),// 父Span = 当前活跃的operationName,// 如 "MySQL/JDBI/PreparedStatement/executeQuery"remotePeer,// 如 "192.168.1.100:3306"owner);exitSpan.setComponent(ComponentsDefine.MYSQL_JDBC_DRIVER);exitSpan.setLayer(SpanLayer.DB);activeSpanStack.push(exitSpan);returnexitSpan;}// 创建LocalSpan(方法调用时)publicAbstractSpancreateLocalSpan(StringoperationName){AbstractSpanlocalSpan=newLocalSpan(spanIdGenerator.generate(),activeSpanStack.peek().getSpanId(),operationName,// 如 "validateOrder"owner);// LocalSpan没有component和remotePeeractiveSpanStack.push(localSpan);returnlocalSpan;}}4. 三种Span在实际Trace中的表现
4.1 一个完整的请求示例
+------------------------------------------------------------------+ | Trace: 用户下单的完整调用链 | +------------------------------------------------------------------+ | | | [gateway-service] TraceSegment-1 (3.2s) | | ┌──────────────────────────────────────────────────────│ │ | │ ★ EntrySpan: POST /api/order/create (3.2s) │ │ | │ │ ← HTTP请求到达Tomcat │ │ | │ │ │ │ | │ ├── ● LocalSpan: validateOrder (5ms) │ │ | │ │ ← 纯本地逻辑,没有网络IO │ │ | │ │ │ │ | │ ├── ▲ ExitSpan: Dubbo/orderService/create (200ms) │ │ | │ │ ← RPC调用下游订单服务 │ │ | │ │ ← 这个ExitSpan会通过Reference连接下一个Segment│ │ | │ │ │ │ | │ └── ▲ ExitSpan: Kafka/order-created (10ms) │ │ | │ ← 异步消息发送 │ │ | └──────────────────────────────────────────────────────│ │ | | | [order-service] TraceSegment-2 (1.5s) | | ┌──────────────────────────────────────────────────────│ │ | │ ★ EntrySpan: Dubbo/create (1.5s) │ │ | │ │ ← RPC请求到达(对应上面的ExitSpan) │ │ | │ │ │ │ | │ ├── ▲ ExitSpan: MySQL/INSERT orders (120ms) │ │ | │ │ ← 数据库写入 │ │ | │ │ │ │ | │ ├── ▲ ExitSpan: Redis/SET key (8ms) │ │ | │ │ ← 缓存写入 │ │ | │ │ │ │ | │ └── ● LocalSpan: sendPushNotification (50ms) │ │ | │ ← 本地推送通知(异步线程) │ │ | └──────────────────────────────────────────────────────│ │ | | | 图例: ★ EntrySpan ▲ ExitSpan ● LocalSpan | +------------------------------------------------------------------+5. TraceSegment的设计意图详解
5.1 数据独立性
每个TraceSegment是独立收集、独立上报的。这解决了什么问题?
+------------------------------------------------------------------+ | TraceSegment的独立上报 | +------------------------------------------------------------------+ | | | 如果没有TraceSegment(二层模型): | | ┌──────────────────────────────────────────────────┐ │ | │ 所有Span混合上报,OAP需要做跨服务重组 │ │ | │ 如果一个服务的Span延迟了,整个Trace无法闭合 │ │ | └──────────────────────────────────────────────────┘ │ | | | 有了TraceSegment(三层模型): | | ┌──────────────────────────────────────────────────┐ │ | │ Segment-1 [gateway → OAP]: 3个Span,独立上报 │ │ | │ Segment-2 [order-svc → OAP]: 4个Span,独立上报 │ │ | │ │ │ | │ OAP端: │ │ | │ 1. 接收各Segment │ │ | │ 2. 根据Reference重组Trace │ │ | │ 3. Segment-2晚到没关系,Trace可部分展示 │ │ | └──────────────────────────────────────────────────┘ │ +------------------------------------------------------------------+5.2 异常隔离
一个服务的崩溃不影响其他服务的数据完整性:
// 场景: B服务OOM,但A服务和C服务的Trace数据完好// 因为每个Segment独立上报!A-Segment→[成功上报到OAP]B-Segment→[OOM,Segment丢失]← 只丢这一段的SpanC-Segment→[成功上报到OAP]// 在SkyWalking UI中:// Trace仍然可展示,只是B服务的Span显示为灰色/虚线5.3 天然的指标粒度
-- 有了TraceSegment,服务级别的指标计算变得简单-- 服务A的平均响应时间 = 所有属于服务A的Segment的延迟均值SELECTAVG(latency)FROMtrace_segmentWHEREservice_id='order-service'ANDtimestamp>NOW()-INTERVAL1HOUR;-- 如果只有Span,你需要规定哪个Span代表"服务响应时间"-- 有了Segment,服务边界天然存在6. 总结
SkyWalking的三层模型(Trace → TraceSegment → Span)不是"叠床架屋",而是在Dapper二层模型基础上的工程性增强:
- TraceSegment明确了服务边界:每个Segment=一个服务实例的一次执行
- 三种Span类型各司其职:
- EntrySpan:标记入口,定义服务响应时间
- LocalSpan:标记内部方法,定位业务逻辑瓶颈
- ExitSpan:标记出口,追踪外部依赖性能
- 独立上报+Reference重组:每个Segment独立上报,OAP端根据Reference重组完整Trace
- 异常隔离:一个服务崩溃不影响其他服务的数据完整性
这个三层模型是SkyWalking区别于Zipkin/Jaeger的核心差异化设计。
下一篇文章,我们将看看这些Trace/Segment/Span在网络上传输时的数据格式——SkyWalking的数据传输协议。
上一篇【第29篇】Google Dapper论文精读——分布式追踪的
下一篇【第31篇】SkyWalking数据传输协议解析——探针与OAP的通信密码(未完待续)