
上一篇【第30篇】SkyWalking追踪模型深度解析——三层架构的独特设计下一篇【第32篇】上下文传播协议——分布式链路是如何穿针引线的1. 引言数据从Agent到OAP的千里之行前面三篇文章我们拆解了DataCarrier的内存队列实现也搞懂了Trace/Segment/Span的三层模型。但数据终究要离开Agent所在的JVM穿过网络到达OAP Server。这个过程中数据长什么样怎么序列化怎么保证跨语言兼容这就是本文要讲的内容——SkyWalking的数据传输协议。它就像Agent和OAP之间的摩斯密码双方必须严格遵循同一套编码规则数据才能被正确解读。2. 探针上报协议的数据结构2.1 协议架构总览SkyWalking使用protobufProtocol Buffers作为序列化协议通过gRPC进行数据传输。------------------------------------------------------------------ ------------------------------------------------------------------ | Agent → OAP 数据上报协议栈 | ------------------------------------------------------------------ | | | ┌──────────────────────────────────────────────────────────┐ │ | │ 应用层 │ │ | │ SegmentObject SpanObject Log JVMMetric ... │ │ | └──────────────────────────────────────────────────────────┘ │ | │ │ | ┌──────────────────────────────────────────────────────────┐ │ | │ 序列化层 │ │ | │ protobuf v3 (.proto文件编译生成Java类) │ │ | └──────────────────────────────────────────────────────────┘ │ | │ │ | ┌──────────────────────────────────────────────────────────┐ │ | │ 传输层 │ │ | │ gRPC (HTTP/2) → 多路复用 二进制 压缩 │ │ | └──────────────────────────────────────────────────────────┘ │ | │ │ | ┌──────────────────────────────────────────────────────────┐ │ | │ 网络层 │ │ | │ TCP → OAP Server port 11800 │ │ | └──────────────────────────────────────────────────────────┘ │ ------------------------------------------------------------------2.2 SegmentObjectTraceSegment的传输形态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 SpanObjectSpan的传输形态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类型0Entry, 1Exit, 2Local SpanType spanType 7; // Span层级Http/RPC/DB/Cache/MQ/Unknown SpanLayer spanLayer 8; // 组件ID如MySQL7, Redis32, Tomcat1 int32 componentId 9; // 是否为错误 bool isError 10; // 标签键值对如 http.methodPOST 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 什么是ContextCarrierContextCarrier是从一个RPC请求的Header中提取的上下文信息用于在分布式链路中传播追踪信息。它是SkyWalking的快递单号——下游服务通过解析它知道这个请求是从哪里来的属于哪个Trace。3.2 SW8协议格式SkyWalking使用SW8SkyWalking 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(){StringBuildersbnewStringBuilder();// 格式: 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[]partsheaderValue.split(-,8);// parts[0] 1 (采样标记)// parts[1] 编码后的traceId// parts[2] 编码后的traceSegmentId// ...ContextCarriercarriernewContextCarrier();carrier.traceIdBase64.decode(parts[1]);carrier.traceSegmentIdBase64.decode(parts[2]);carrier.spanIdInteger.parseInt(parts[3]);carrier.parentServiceBase64.decode(parts[4]);carrier.parentServiceInstanceBase64.decode(parts[5]);carrier.parentEndpointBase64.decode(parts[6]);carrier.addressUsedAtClientBase64.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协议TransportgRPCgRPCSerializationprotobufprotobufTraceId格式固定长度32字符三段式: PID.Timestamp.SeqSegmentId格式固定长度32字符三段式: TraceId SeqHeader编码SW6 (v2)SW8 (v3)Baggage不支持支持 (sw8-correlation)跨语言支持Java-only完整跨语言向后兼容N/A兼容v25.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等框架中注入Headerv2→v3演进三段式TraceIdBaggage透传向后兼容协议设计是SkyWalking的核心竞争力之一——它保证了在高并发下Agent既能采集完整数据又不会成为系统的性能瓶颈。下一篇文章我们聚焦SW8协议中最精妙的部分——上下文传播看SkyWalking如何通过几个Header把几十个微服务的调用串成一条完整链路。上一篇【第30篇】SkyWalking追踪模型深度解析——三层架构的独特设计下一篇【第32篇】上下文传播协议——分布式链路是如何穿针引线的