上一篇【第30篇】SkyWalking追踪模型深度解析——三层架构的独特设计
下一篇【第32篇】上下文传播协议——分布式链路是如何穿针引线的
1. 引言:数据从Agent到OAP的千里之行
前面三篇文章,我们拆解了DataCarrier的内存队列实现,也搞懂了Trace/Segment/Span的三层模型。但数据终究要离开Agent所在的JVM,穿过网络,到达OAP Server。这个过程中,数据长什么样?怎么序列化?怎么保证跨语言兼容?
这就是本文要讲的内容——SkyWalking的数据传输协议。它就像Agent和OAP之间的"摩斯密码",双方必须严格遵循同一套编码规则,数据才能被正确解读。
2. 探针上报协议的数据结构
2.1 协议架构总览
SkyWalking使用protobuf(Protocol Buffers)作为序列化协议,通过gRPC进行数据传输。
+------------------------------------------------------------------+ +------------------------------------------------------------------+ | Agent → OAP 数据上报协议栈 | +------------------------------------------------------------------+ | | | ┌──────────────────────────────────────────────────────────┐ │ | │ 应用层 │ │ | │ SegmentObject SpanObject Log JVMMetric ... │ │ | └──────────────────────────────────────────────────────────┘ │ | │ │ | ┌──────────────────────────────────────────────────────────┐ │ | │ 序列化层 │ │ | │ protobuf v3 (.proto文件编译生成Java类) │ │ | └──────────────────────────────────────────────────────────┘ │ | │ │ | ┌──────────────────────────────────────────────────────────┐ │ | │ 传输层 │ │ | │ gRPC (HTTP/2) → 多路复用 + 二进制 + 压缩 │ │ | └──────────────────────────────────────────────────────────┘ │ | │ │ | ┌──────────────────────────────────────────────────────────┐ │ | │ 网络层 │ │ | │ TCP → OAP Server port 11800 │ │ | └──────────────────────────────────────────────────────────┘ │ +------------------------------------------------------------------+2.2 SegmentObject(TraceSegment的传输形态)
TraceSegment在Java内存中是对象,在网络上是protobuf消息:
// 核心数据结构定义(从 .proto 文件简化) message SegmentObject { // TraceSegment的唯一ID string traceSegmentId = 1; // 这个Segment中的所有Span repeated SpanObject spans = 2; // 所属服务ID int32 serviceId = 3; // 所属服务实例ID int32 serviceInstanceId = 4; // 是否为请求大小(用于大小限制判断) bool isSizeLimited = 5; }2.3 SpanObject(Span的传输形态)
message SpanObject { // Span唯一ID(在Segment内唯一) int32 spanId = 1; // 父Span ID(-1表示Root Span = EntrySpan) int32 parentSpanId = 2; // 开始时间(epoch毫秒) int64 startTime = 3; // 结束时间(epoch毫秒) int64 endTime = 4; // 操作名称(如 "/api/order/create") string operationName = 5; // 对端地址(如 "192.168.1.100:3306") string peer = 6; // Span类型(0=Entry, 1=Exit, 2=Local) SpanType spanType = 7; // Span层级(Http/RPC/DB/Cache/MQ/Unknown) SpanLayer spanLayer = 8; // 组件ID(如MySQL=7, Redis=32, Tomcat=1) int32 componentId = 9; // 是否为错误 bool isError = 10; // 标签(键值对,如 http.method=POST) repeated KeyStringValuePair tags = 11; // 日志(Span内的注解,如 "参数校验通过") repeated Log logs = 12; // 引用(指向其他Segment的Span,实现跨进程关联) repeated SegmentReference refs = 13; // 对端地址(如 "order-service") string peer = 14; // Span类型 enum SpanType { Entry = 0; Exit = 1; Local = 2; } // Span层级 enum SpanLayer { Unknown = 0; Database = 1; RPCFramework = 2; Http = 3; MQ = 4; Cache = 5; } }2.4 SegmentReference(跨进程关联)
message SegmentReference { // 引用的类型 RefType refType = 1; // 上游Segment的ID string parentTraceSegmentId = 2; // 上游Span的ID int32 parentSpanId = 3; // 上游服务ID int32 parentServiceInstanceId = 4; // 网络地址信息(调用方和被调用方) string networkAddress = 5; string networkAddressId = 6; // 入口信息(入口端点名称) string entryServiceInstanceId = 7; string parentEndpointName = 8; string entryEndpointName = 9; enum RefType { CrossProcess = 0; // 跨进程 CrossThread = 1; // 跨线程 } }2.5 数据结构关系图
+------------------------------------------------------------------+ +------------------------------------------------------------------+ | SegmentObject 数据结构关系 | +------------------------------------------------------------------+ | | | SegmentObject | | ┌──────────────────────────────────────────────────────────┐ │ | │ traceSegmentId: "7b2a.1625.1625000000003" │ │ | │ serviceId: 2 (order-service) │ │ | │ serviceInstanceId: 5 (order-service-7f8d9b-abc12) │ │ | │ │ │ | │ spans (数组): │ │ | │ ┌─────────────────────────────────────────────────┐ │ │ | │ │ SpanObject[0]: EntrySpan │ │ │ | │ │ spanId: 0, parentSpanId: -1 │ │ │ | │ │ spanType: Entry, spanLayer: Http │ │ │ | │ │ operationName: "POST:/order/create" │ │ │ | │ │ startTime: 1625000000100 │ │ │ | │ │ endTime: 1625000003300 │ │ │ | │ │ refs[0]: │ │ │ | │ │ refType: CrossProcess │ │ │ | │ │ parentTraceSegmentId: "7b2a.1625." + "0002" │ │ │ | │ │ parentSpanId: 2 │ │ │ | │ └─────────────────────────────────────────────────┘ │ │ | │ ┌─────────────────────────────────────────────────┐ │ │ | │ │ SpanObject[1]: ExitSpan (MySQL) │ │ │ | │ │ spanId: 1, parentSpanId: 0 │ │ │ | │ │ spanType: Exit, spanLayer: Database │ │ │ | │ │ componentId: 7 (MySQL) │ │ │ | │ │ operationName: "INSERT INTO orders ..." │ │ │ | │ │ peer: "192.168.1.100:3306" │ │ │ | │ └─────────────────────────────────────────────────┘ │ │ | └──────────────────────────────────────────────────────────┘ │ +------------------------------------------------------------------+3. ContextCarrier的SW8编码格式
3.1 什么是ContextCarrier?
ContextCarrier是从一个RPC请求的Header中提取的上下文信息,用于在分布式链路中传播追踪信息。它是SkyWalking的"快递单号"——下游服务通过解析它,知道"这个请求是从哪里来的,属于哪个Trace"。
3.2 SW8协议格式
SkyWalking使用SW8(SkyWalking v8 header protocol)作为跨进程传播的编码格式:
+------------------------------------------------------------------+ +------------------------------------------------------------------+ | SW8 Header 编码格式 | +------------------------------------------------------------------+ | | | Header Key: sw8 | | Header Value: | | 1-TRACE_ID-SEGMENT_ID-3-PARENT_SERVICE-PARENT_INSTANCE- | | PARENT_ENDPOINT-IP_OF_PARENT | | | | 字段说明: | | ┌──────┬──────────────────┬────────────────────────────────────┐│ | │ 索引 │ 字段 │ 示例 ││ | ├──────┼──────────────────┼────────────────────────────────────┤│ | │ 0 │ 采样标记 │ 0=未采样, 1=已采样 ││ | │ 1 │ Trace ID │ 7b2a8f1d9e3c.1625.1625000000001 ││ | │ 2 │ Parent Segment ID │ 7b2a8f1d9e3c.1625.1625000000002 ││ | │ 3 │ Parent Service │ Z2F0ZXdheS1zZXJ2aWNl (Base64编码) ││ | │ 4 │ Parent Instance │ Z2F0ZXdheS1zZXJ2aWNlLWFiYw== ││ | │ 5 │ Parent Endpoint │ L2FwaS9vcmRlci9jcmVhdGU= ││ | │ 6 │ Peer Address │ MTAuMC4xLjU6ODA4MA== ││ | └──────┴──────────────────┴────────────────────────────────────┘│ | | | 解码示例: | | Parent Service (Base64): Z2F0ZXdheS1zZXJ2aWNl | | 解码后: gateway-service | | | | Parent Endpoint (Base64): L2FwaS9vcmRlci9jcmVhdGU= │ | 解码后: /api/order/create | +------------------------------------------------------------------+3.3 SW8编码的代码实现
// ContextCarrier编码(简化版)publicclassContextCarrier{privateStringtraceId;privateStringtraceSegmentId;privateintspanId;privateStringparentService;privateStringparentServiceInstance;privateStringparentEndpoint;privateStringaddressUsedAtClient;// 序列化为SW8 Header值publicStringserialize(){StringBuildersb=newStringBuilder();// 格式: 1-TRACE_ID-SEGMENT_ID-SPAN_ID-SERVICE-INSTANCE-ENDPOINT-ADDRESSsb.append("1");// 采样标记(已采样)sb.append("-");sb.append(Base64.encode(traceId));sb.append("-");sb.append(Base64.encode(traceSegmentId));sb.append("-");sb.append(spanId);sb.append("-");sb.append(Base64.encode(parentService));sb.append("-");sb.append(Base64.encode(parentServiceInstance));sb.append("-");sb.append(Base64.encode(parentEndpoint));sb.append("-");sb.append(Base64.encode(addressUsedAtClient));returnsb.toString();}// 从SW8 Header值反序列化publicstaticContextCarrierdeserialize(StringheaderValue){String[]parts=headerValue.split("-",8);// parts[0] = "1" (采样标记)// parts[1] = 编码后的traceId// parts[2] = 编码后的traceSegmentId// ...ContextCarriercarrier=newContextCarrier();carrier.traceId=Base64.decode(parts[1]);carrier.traceSegmentId=Base64.decode(parts[2]);carrier.spanId=Integer.parseInt(parts[3]);carrier.parentService=Base64.decode(parts[4]);carrier.parentServiceInstance=Base64.decode(parts[5]);carrier.parentEndpoint=Base64.decode(parts[6]);carrier.addressUsedAtClient=Base64.decode(parts[7]);returncarrier;}}4. Header传播机制
4.1 HTTP场景的传播流程
+------------------------------------------------------------------+ | SW8 Header在HTTP调用中的传播 | +------------------------------------------------------------------+ | | | 调用方 (gateway-service) | | ┌──────────────────────────────────────────────────┐ │ | │ 1. 创建ExitSpan,准备调用下游 │ │ | │ 2. 构造ContextCarrier │ │ | │ carrier.setTraceId(currentTraceId) │ │ | │ carrier.setSpanId(exitSpan.getSpanId()) │ │ | │ carrier.setParentService("gateway-service") │ │ | │ ... │ │ | │ 3. 编码为SW8 Header │ │ | │ Header: sw8 = "1-xxx-xxx-0-base64..." │ │ | │ 4. 附加到HTTP请求 │ │ | │ request.setHeader("sw8", carrier.serialize()) │ │ | └──────────────────┬───────────────────────────────┘ │ | │ HTTP Request │ | │ Headers: │ | │ sw8: 1-xxx-xxx-0-... │ | │ sw8-correlation: ... │ | ▼ │ | 被调用方 (order-service) │ | ┌──────────────────────────────────────────────────┐ │ | │ 5. Agent拦截HTTP请求 │ │ | │ 6. 提取sw8 Header │ │ | │ String sw8 = request.getHeader("sw8"); │ │ | │ 7. 反序列化为ContextCarrier │ │ | │ ContextCarrier carrier = deserialize(sw8); │ │ | │ 8. 创建EntrySpan,关联上游 │ │ | │ entrySpan.ref(carrier); │ │ | └──────────────────────────────────────────────────┘ │ +------------------------------------------------------------------+4.2 各种RPC框架的Header注入
SkyWalking Agent通过字节码增强,自动在各种框架中注入SW8 Header:
// SkyWalking Agent自动处理的Header注入场景// 1. HTTP (HttpClient/OkHttp/RestTemplate)// Agent自动在HTTP请求中添加Header: sw8=...// 2. Spring Cloud Feign// RequestInterceptor自动添加// 3. Dubbo// RpcContext.getContext().setAttachment("sw8", ...)// 4. gRPC// Metadata headers中添加 sw8 key// 5. Kafka// ProducerRecord.headers().add("sw8", ...)// 6. RabbitMQ// MessageProperties.headers.put("sw8", ...)5. v2协议到v3协议的演进
5.1 版本演进时间线
+------------------------------------------------------------------+ | SkyWalking协议版本演进 | +------------------------------------------------------------------+ | | | v1 (SkyWalking 3.x) | | ┌────────────────────────────────────────────┐ | | │ HTTP + JSON │ | | │ 简单直观但性能差 │ | | └────────────────────────────────────────────┘ | | │ | | ▼ | | v2 (SkyWalking 5.x-6.x) | | ┌────────────────────────────────────────────┐ | | │ gRPC + protobuf │ | | │ 大幅提升性能 │ | | │ 固定字段数量 │ | | └────────────────────────────────────────────┘ | | │ | | ▼ | | v3 (SkyWalking 7.x+) | | ┌────────────────────────────────────────────┐ | | │ 简化编码格式 + 增强特性 │ | | │ 支持 Baggage (业务信息透传) │ | | │ 跨语言更好的兼容性 │ | | └────────────────────────────────────────────┘ | +------------------------------------------------------------------+5.2 v2 vs v3 协议对比
| 特性 | v2协议 | v3协议 |
|---|---|---|
| Transport | gRPC | gRPC |
| Serialization | protobuf | protobuf |
| TraceId格式 | 固定长度32字符 | 三段式: PID.Timestamp.Seq |
| SegmentId格式 | 固定长度32字符 | 三段式: TraceId + Seq |
| Header编码 | SW6 (v2) | SW8 (v3) |
| Baggage | 不支持 | 支持 (sw8-correlation) |
| 跨语言支持 | Java-only | 完整跨语言 |
| 向后兼容 | N/A | 兼容v2 |
5.3 v3的关键改进
1. TraceId三段式格式
// v2: 固定长度32字符(UUID简化版) traceId: "1a2b3c4d5e6f7g8h9i0j1k2l3m4n5o6p" // v3: 三段式格式,包含更多信息 // 格式: ${service_instance_id}.${thread_id}.${timestamp} traceId: "7b2a8f1d9e3c.1625.1625000000001" // ↑ ↑ ↑ // 进程标识 线程ID 时间戳+自增序列 // 好处: // 1. 从traceId就能知道是哪个进程产生的 // 2. 时间戳内嵌,便于按时间分片存储 // 3. 全局唯一保证(时间戳+自增序列)2. sw8-correlation(透传业务信息)
// HTTP Header中新增的关键字段 sw8-correlation: base64(key1),base64(value1),base64(key2),base64(value2) // 用法:业务方可以把业务信息放到链路上下文中透传 // 如:租户ID、用户ID、订单ID下一篇文章会专门展开Baggage的透传实现。
3. 协议字段的向后兼容
// protobuf v3的字段编号保证了向后兼容 message SpanObject { // 新增字段使用新的编号,不影响旧字段 string operationName = 5; // v2已有 repeated KeyStringValuePair tags = 11; // v2已有 repeated SegmentReference refs = 13; // v2已有 // v3新增字段 SpanType spanType = 15; // v3新增 (v2通过layer区分) string serviceCode = 16; // v3新增 }6. protobuf的优势
为什么SkyWalking选protobuf而不是JSON?
+------------------------------------------------------------------+ | protobuf vs JSON 对比 | +------------------------------------------------------------------+ | | | ┌──────────────┬─────────────────┬───────────────┐ │ | │ 指标 │ JSON │ protobuf │ │ | ├──────────────┼─────────────────┼───────────────┤ │ | │ 序列化后大小 │ 较大(含字段名) │ 极小(仅含编号) │ │ | │ 序列化速度 │ 中等 │ 极快 │ │ | │ 反序列化速度 │ 中等 │ 极快 │ │ | │ 可读性 │ 极好(人类可读) │ 差(二进制) │ │ | │ 类型安全 │ 弱(动态类型) │ 强(编译期校验) │ │ | │ 跨语言支持 │ 极好 │ 极好 │ │ | │ 向后兼容 │ 无保证 │ 字段编号保证 │ │ | └──────────────┴─────────────────┴───────────────┘ │ | | | 示例: 同一条Span数据的大小对比 | | JSON: ~500 bytes | | protobuf: ~80 bytes (压缩6倍) | | protobuf + gzip: ~30 bytes (压缩16倍) | +------------------------------------------------------------------+7. 总结
SkyWalking的数据传输协议是一套精心设计的通信体系:
- protobuf + gRPC:高性能序列化+HTTP/2多路复用
- SegmentObject/SpanObject/Reference:三层数据结构精确映射追踪模型
- SW8编码:紧凑的Header格式,Base64编码传输,字段完整
- 自动注入:Agent字节码增强自动在HTTP/Dubbo/gRPC/Kafka等框架中注入Header
- v2→v3演进:三段式TraceId+Baggage透传+向后兼容
协议设计是SkyWalking的核心竞争力之一——它保证了在高并发下,Agent既能采集完整数据,又不会成为系统的性能瓶颈。
下一篇文章,我们聚焦SW8协议中最精妙的部分——上下文传播,看SkyWalking如何通过几个Header,把几十个微服务的调用串成一条完整链路。
上一篇【第30篇】SkyWalking追踪模型深度解析——三层架构的独特设计
下一篇【第32篇】上下文传播协议——分布式链路是如何穿针引线的