【SkyWalking从入门到精通】第32篇:上下文传播协议——分布式链路是如何穿针引线的

发布时间:2026/7/12 18:36:52
【SkyWalking从入门到精通】第32篇:上下文传播协议——分布式链路是如何穿针引线的 上一篇【第31篇】SkyWalking数据传输协议解析——探针与OAP的通信密码下一篇【第33篇】SkyWalking v3协议详解——从编码到Baggage的全新升级1. 引言千里姻缘一线牵想象一个场景用户在APP上点击下单请求到达网关gateway-service网关调用订单服务order-service订单服务调用库存服务inventory-service库存服务调用支付服务payment-service支付服务调用银行接口。这中间经过了5个服务。在SkyWalking的UI中这些调用被串成一条完整的Trace像一条漂亮的珍珠项链。问题是——这些珍珠Span是在不同JVM进程中独立产生的它们是怎么知道彼此的关系的答案就是上下文传播协议。它是分布式追踪的红娘让素未谋面的Span能够认亲。2. 跨进程传播的原理2.1 问题两个JVM之间怎么打电话------------------------------------------------------------------ | 跨进程传播的核心问题 | ------------------------------------------------------------------ | | | JVM-A (gateway-service) JVM-B (order-service) | | ┌──────────────────────┐ ┌──────────────────────┐ │ | │ Trace ID: abc123 │ │ Trace ID: ??? │ │ | │ Segment: seg-A-1 │ │ Segment: seg-B-1 │ │ | │ Span-0: Entry │ │ Span-0: Entry │ │ | │ Span-1: Exit → B ────┼──RPC Call──→│ 我是谁 │ │ | │ │ │ 我从哪里来 │ │ | └──────────────────────┘ └──────────────────────┘ │ | | | 需要传递的信息: | | 1. Trace ID: 让下游知道自己是哪条Trace的一部分 | | 2. Parent Segment ID: 让下游知道上游是谁 | | 3. Parent Span ID: 让下游知道是上游的哪个Span发起的调用 | | 4. Parent Service: 上游服务的名称 | | 5. Parent Endpoint: 上游的端点名称 | ------------------------------------------------------------------2.2 Header注入与提取Inject ExtractSkyWalking使用依赖注入的经典模式——Inject/Extract------------------------------------------------------------------ | Inject/Extract 工作流程 | ------------------------------------------------------------------ | | | 上游服务 (调用方) 下游服务 (被调用方) | | ┌────────────────────┐ ┌────────────────────┐ | | │ │ │ │ | | │ ① 构造 │ │ ④ 提取 │ | | │ ContextCarrier │ │ ContextCarrier │ | | │ ┌──────────────┐ │ │ ┌──────────────┐ │ | | │ │ traceId:xxx │ │ │ │ traceId:xxx │ │ | | │ │ segmentId:yy │ │ │ │ segmentId:yy │ │ | | │ │ spanId:3 │ │ │ │ spanId:3 │ │ | | │ │ service:A │ │ │ │ service:A │ │ | | │ └──────────────┘ │ │ └──────────────┘ │ | | │ │ │ │ | | │ ② Inject │ │ ⑤ 创建EntrySpan │ | | │ → 编码为SW8 │ │ 关联上游Span │ | | │ → 写入Header │ │ │ | | │ │ │ │ ▲ │ | | └────────┼───────────┘ └────────┼───────────┘ | | │ │ | | │ ③ HTTP/RPC Request │ | | │ Headers: │ | | │ sw8: 1-xxx-yyy-3-A-... │ | | └───────────────────────────────────┘ | ------------------------------------------------------------------3. ContextCarrier的数据字段详解3.1 完整字段定义publicclassContextCarrierimplementsSerializable{// 核心追踪字段 /** Trace ID — 全局唯一标识一条完整的分布式调用链 */privateStringtraceId;/** Trace Segment ID — 上游JVM进程的Segment ID */privateStringtraceSegmentId;/** Span ID — 上游JVM进程中发起本次调用的那个Span的ID */privateintspanId;/** Parent Service — 上游服务的名称 */privateStringparentService;/** Parent Service Instance — 上游服务实例的名称 */privateStringparentServiceInstance;/** Parent Endpoint — 上游端点名称如 /api/order/create */privateStringparentEndpoint;/** Address Used At Client — 客户端发起调用时使用的地址 */privateStringaddressUsedAtClient;// 扩展字段 /** 采样标记 — 1表示采样0表示不采样 */privateintsampled1;/** Parent Endpoint Name (v3新增用于更精确的端点匹配) */privateStringparentEndpointName;}3.2 各字段的作用示意图------------------------------------------------------------------ | ContextCarrier各字段在追踪中的作用 | ------------------------------------------------------------------ | | | ┌──────────────────────────────────────────────────────────────┐│ | │ gateway-service ││ | │ TraceSegment[traceSegmentId: seg-gw-1] ││ | │ ├── EntrySpan[0]: /api/order/create ││ | │ └── ExitSpan[1]: POST order-service/create ││ | │ ↓ ││ | │ 构造ContextCarrier: ││ | │ traceId abc.1625.1625000000001 ← 哪条Trace ││ | │ traceSegmentId seg-gw-1 ← 上游Segment ID ││ | │ spanId 1 ← 上游Span ID ││ | │ parentService gateway-service ← 我是谁 ││ | │ parentServiceInstance gateway-pod-a ← 我在哪个实例 ││ | │ parentEndpoint /api/order/create ← 我的入口端点 ││ | │ addressUsedAtClient order-service:8080 ← 我要调谁 ││ | └──────────────────────────┬───────────────────────────────────┘│ | │ │ | HTTP Header: sw8... │ | │ │ | ▼ │ | ┌──────────────────────────────────────────────────────────────┐│ | │ order-service ││ | │ TraceSegment[traceSegmentId: seg-os-1] ││ | │ ├── EntrySpan[0]: POST /create ││ | │ │ refs[0]: ││ | │ │ traceId → 同一条Trace ││ | │ │ parentTraceSegmentId → seg-gw-1 ││ | │ │ parentSpanId → 1 ││ | │ ├── ExitSpan[1]: MySQL INSERT orders ││ | │ └── ExitSpan[2]: POST inventory-service/deduct ││ | └──────────────────────────────────────────────────────────────┘│ ------------------------------------------------------------------4. HTTP场景的传播流程4.1 完整的代码流程// 步骤1Agent拦截出站HTTP请求 publicclassHttpClientExecuteInterceptor{OverridepublicvoidbeforeMethod(EnhancedInstanceobjInst,Methodmethod,Object[]allArguments,Class?[]argumentsTypes){HttpRequestBaserequest(HttpRequestBase)allArguments[0];// ① 获取当前Trace上下文ContextManagercontextManagerContextManager.get();// ② 创建ExitSpan标记这是一个出站调用AbstractSpanexitSpancontextManager.createExitSpan(request.getURI().getPath(),request.getURI().getHost():request.getURI().getPort());// ③ 构造ContextCarrierContextCarriercarriernewContextCarrier();contextManager.inject(carrier);// ④ 编码为SW8 HeaderStringsw8Headercarrier.serialize();// ⑤ 注入到HTTP请求头request.setHeader(sw8,sw8Header);}}// 步骤2Agent拦截入站HTTP请求 publicclassTomcatInvokeInterceptor{OverridepublicvoidbeforeMethod(EnhancedInstanceobjInst,Methodmethod,Object[]allArguments,Class?[]argumentsTypes){HttpServletRequestrequest(HttpServletRequest)allArguments[0];// ⑥ 提取sw8 HeaderStringsw8Headerrequest.getHeader(sw8);if(sw8Header!null){// ⑦ 反序列化为ContextCarrierContextCarriercarrierContextCarrier.deserialize(sw8Header);// ⑧ 创建EntrySpan关联上游ContextContextManagercontextManagerContextManager.get();AbstractSpanentrySpancontextManager.createEntrySpan(request.getRequestURI(),carrier// ← 传入上游上下文);}else{// 不是分布式调用创建根EntrySpancontextManager.createEntrySpan(request.getRequestURI(),null);}}}4.2 HTTP Header完整示例# 一个完整的分布式调用HTTP请求头 POST /api/order/create HTTP/1.1 Host: order-service:8080 Content-Type: application/json # SkyWalking追踪相关Header sw8: MS1iYXNlNjQoYWJjMTIzKQ.MS1iYXNlNjQoc2VnLWd3LTEp.NC4x.MS1iYXNlNjQoZ2F0ZXdheS1zZXJ2aWNlKQ.MS1iYXNlNjQoZ2F0ZXdheS1wb2QtYSk.MS1iYXNlNjQoL2FwaS9vcmRlci9jcmVhdGUp.MS1iYXNlNjQob3JkZXItc2VydmljZTo4MDgwKQ # sw8-correlation (Baggage透传) sw8-correlation: base64(userId),base64(10086),base64(tenantId),base64(acme-corp)5. 消息队列场景的传播流程5.1 Kafka场景消息队列的传播和HTTP有本质不同——HTTP是同步的请求-响应MQ是异步的生产-消费------------------------------------------------------------------ | Kafka消息的上下文传播 | ------------------------------------------------------------------ | | | 生产者端: | | ┌─────────────────────────────────────────────────┐ │ | │ ① 构造ContextCarrier并注入到Kafka Header │ │ | │ │ │ | │ ProducerRecordString, Order record │ │ | │ new ProducerRecord(order-topic, order); │ │ | │ │ │ | │ // Agent自动添加Header字节码增强 │ │ | │ record.headers() │ │ | │ .add(sw8, carrier.serialize().getBytes()); │ │ | └──────────────────┬──────────────────────────────┘ │ | │ │ | ▼ │ | ┌─────────────────────────────────────────────────┐ │ | │ Kafka Broker │ │ | │ 消息持久化到磁盘Header随消息一起存储 │ │ | └──────────────────┬──────────────────────────────┘ │ | │ │ | ▼ │ | 消费者端: | | ┌─────────────────────────────────────────────────┐ │ | │ ② 从Kafka Header提取SW8 │ │ | │ │ │ | │ ConsumerRecordString, Order record ...; │ │ | │ │ │ | │ // Agent自动提取Header │ │ | │ Headers headers record.headers(); │ │ | │ Header sw8Header headers.lastHeader(sw8); │ │ | │ String sw8 new String(sw8Header.value()); │ │ | │ ContextCarrier carrier deserialize(sw8); │ │ | │ │ │ | │ // 创建EntrySpan关联生产者 │ │ | │ contextManager.createEntrySpan( │ │ | │ Kafka/order-topic/Consumer, │ │ | │ carrier │ │ | │ ); │ │ | └─────────────────────────────────────────────────┘ │ ------------------------------------------------------------------5.2 消息队列传播的特殊性------------------------------------------------------------------ ------------------------------------------------------------------ | 同步RPC vs 异步消息队列的传播差异 | ------------------------------------------------------------------ | | | HTTP/RPC 同步调用: | | ┌──────────────────────────────────────────────────┐ │ | │ Service-A ──调用──→ Service-B │ │ | │ ←响应── │ │ | │ │ │ | │ A的ExitSpan和B的EntrySpan在时间上是重叠的 │ │ | │ Trace是一棵树从根到叶 │ │ | └──────────────────────────────────────────────────┘ │ | | | MQ 异步消息: | | ┌──────────────────────────────────────────────────┐ │ | │ Service-A ──发送──→ [Queue] ──消费──→ Service-B │ │ | │ │ │ | │ A的ExitSpan生产消息 │ │ | │ B的EntrySpan消费消息 │ │ | │ 这两个Span在时间上可能有延迟秒/分/时 │ │ | │ Trace中A的Span和B的Span是兄弟关系 │ │ | │ 而不是父子关系 │ │ | └──────────────────────────────────────────────────┘ │ ------------------------------------------------------------------6. ContextSnapshot线程间传播6.1 线程池场景的传播问题// 问题场景异步线程池GetMapping(/api/order/async)publicvoidasyncOrder(RequestBodyOrderDTOorder){// 主线程有Trace上下文// Trace ID: abc123// Active Span: EntrySpan /api/order/asyncexecutorService.submit(()-{// ⚠️ 新线程Trace上下文丢失了// 如果没有特殊处理这里的操作不会被追踪orderService.processAsync(order);});}6.2 ContextSnapshot的解决方案// SkyWalking通过ContextSnapshot实现线程间传播publicclassTracingContext{// ① 在主线程中捕获当前上下文快照publicContextSnapshotcapture(){ContextSnapshotsnapshotnewContextSnapshot();snapshot.setTraceId(this.getTraceId());snapshot.setTraceSegmentId(this.getTraceSegmentId());snapshot.setSpanId(this.activeSpan().getSpanId());snapshot.setParentService(this.getParentService());// ... 其他字段returnsnapshot;}// ② 在新线程中恢复上下文publicvoidcontinued(ContextSnapshotsnapshot){// 重新建立Trace上下文this.traceIdsnapshot.getTraceId();this.traceSegmentIdsnapshot.getTraceSegmentId();// 创建LocalSpan引用父SpanAbstractSpanlocalSpancreateLocalSpan(asyncTask);// 引用关系类型CrossThreadlocalSpan.ref(snapshot);}}6.3 线程间传播的可视化------------------------------------------------------------------ | 线程间上下文传播 | ------------------------------------------------------------------ | | | 主线程 (Thread-1) 工作线程 (Thread-2) | | ┌────────────────────────┐ ┌─────────────────────┐ │ | │ │ │ │ │ | │ ① ContextManager │ │ ③ ContextManager │ │ | │ .capture() │ │ .continued(snap) │ │ | │ → ContextSnapshot │──传递快照───→│ │ │ | │ │ │ ④ 创建LocalSpan │ │ | │ ② 提交异步任务 │ │ ref(snap) │ │ | │ executor.submit() │ │ → CrossThread │ │ | │ │ │ │ │ | └────────────────────────┘ └─────────────────────┘ │ | | | Trace中的效果: | | ┌──────────────────────────────────────────────────┐ │ | │ ★ EntrySpan /api/order/async (Thread-1) │ │ | │ ├── ExitSpan MySQL insert order │ │ | │ └── LocalSpan asyncTask (Thread-2) │ │ | │ refType: CrossThread │ │ | │ └── ExitSpan Redis SET │ │ | └──────────────────────────────────────────────────┘ │ ------------------------------------------------------------------6.4 Agent自动处理的线程池场景// SkyWalking Agent通过字节码增强自动处理的线程创建场景// 1. new Thread(Runnable).start()// 2. ExecutorService.submit(Runnable/Callable)// 3. Async (Spring异步方法)// 4. CompletableFuture.supplyAsync()// 5. ForkJoinPool// 6. ScheduledExecutorService// 所有这些场景Agent都会自动在Runnable/Callable被提交时捕获上下文// 在执行时恢复上下文。开发零感知7. 传播协议的健壮性保障7.1 异常情况的处理// 场景1: 下游没有Agent或Agent未启用// → SW8 Header被忽略EntrySpan创建为Root Span// → Trace就此断开但不影响上游的正常追踪// 场景2: Header丢失网关/代理层截断了自定义Header// → 下游创建为独立的Root Span// → UI上表现为两条独立的Trace// 场景3: Header被篡改// → Base64解码失败// → 按Header丢失处理// 场景4: Trace数据大小超限// 可通过以下配置设置最大Trace数据大小:// agent.config:// agent.span_limit_per_segment500 # 每个Segment最多500个Span// agent.segment_limit_size1M # Segment数据最大1MB7.2 传播安全的边界控制# 忽略不需要追踪的URL# 如健康检查、静态资源等plugin.tomcat.trace_ignore_url_pattern/health,/metrics,/actuator/**# 或者通过环境变量# export SW_TRACE_IGNORE_PATH/actuator/**,/health,/metrics/**8. 总结SkyWalking的上下文传播协议是分布式追踪能够穿针引线的核心魔法Inject/Extract模式上游注入下游提取经典而优雅SW8 Header编码紧凑的Base64编码承载7个关键传播字段三大传播场景HTTP同步、MQ异步、线程间传播覆盖所有主流场景零配置自动化Agent字节码增强自动完成传播逻辑开发者零感知健壮性设计各类异常情况有合理的降级策略不影响业务理解上下文传播就等于理解了分布式追踪的灵魂。它让几十个独立的JVM进程不再各自为政而是协同讲述同一个请求的故事。下一篇文章我们聚焦SkyWalking v3协议的新特性特别是**Baggage透传业务信息**的实现。上一篇【第31篇】SkyWalking数据传输协议解析——探针与OAP的通信密码下一篇【第33篇】SkyWalking v3协议详解——从编码到Baggage的全新升级