skywalking全链路追踪如何追踪通过kafka异步通信的服务链
下面我把答案拆成 4 部分,先说高层原理(SkyWalking 如何跨消息中间件追踪),再说两种落地方式(自动 vs 手工),接着给出Spring Boot + Kafka 的具体代码示例(生产者把 TraceContext 写到消息 header,消费者从 header 恢复上下文并继续 Span),最后给出验证 & 常见问题排查清单与最佳实践。这样你既知道“它是怎么工作的”,也能立刻在代码里落地和排查。
1) 高层原理:SkyWalking 怎么追踪异步消息(Producer → Kafka → Consumer)
-
Trace 上下文传播:分布式 Trace 在服务间通过携带 TraceContext(TraceId + Span 信息)在请求里传递。对于 HTTP 是 header,消息中间件则通过消息 header(Kafka 自 0.11 起支持 headers)传递这段上下文。
-
Agent 自动增强:SkyWalking Java agent 通过字节码增强拦截 Kafka 客户端(Producer/Consumer)的 API,在发送时自动注入 tracing header(如
sw8),并在消费端自动抽取并创建对应的 Entry span,从而把 producer 的 trace 与 consumer 的 span 串到同一个 Trace 中。 -
消息端点建模:SkyWalking 会把 Producer->Broker 的行为建模为一个 ExitSpan(出站到 MQ),在 Consumer 端把 Broker->Consumer 的行为建模为 EntrySpan(入站)。通过 header 中的引用关系(TraceContext),这两段能被关联成一条完整调用链。
结论:如果你使用的是被 SkyWalking agent 支持的 Kafka 客户端版本,并正确启动 agent,理论上不需要改动业务代码(自动打通)。但在某些场景(很老的客户端、不走标准客户端、使用特殊框架层或需要自定义逻辑)下,你需要手工把上下文写入/抽取到消息 header。
2) 两种落地方式与适用场景
A. 自动方式(推荐先尝试)
-
条件:
-
你的应用以标准 Kafka client(或 Spring-Kafka)为依赖;
-
SkyWalking Java agent 已正确挂载(
-javaagent:/path/skywalking-agent.jar)且 Kafka 插件在 agent 中启用; -
Kafka 客户端版本在 agent 支持列表内(参考 SkyWalking 文档/agent 日志)。
-
-
优点:无需代码改动,agent 完成 inject/extract,SkyWalking UI 会显示消息链路(Producer span → Broker → Consumer span)。
-
如何确认:
-
观察 SkyWalking agent 启动日志,看 plugin/kafka 是否加载成功;
-
在 SkyWalking UI 能看到 Messaging 类型的 span(topic 名称、partition 等信息);
-
在 Kafka 消息中检查是否有
sw8(或 agent 所用的 header 名)头(用 kafka-console-consumer 查看 headers)。
-
B. 手工方式(当自动不适用或你需要自定义)
-
做法:在 Producer 端显式把 trace 上下文编码写到
ProducerRecord.headers();在 Consumer 端从ConsumerRecord.headers()解析并把上下文注入到 SkyWalking 的 Context 中,然后再创建 span/继续链路。 -
适用场景:
-
使用不被 agent 支持的 Kafka 客户端/框架(或 agent 插件在你的环境里被禁用);
-
你需要自定义 header 名、加密 header、或在消息体里嵌入 context(旧版 Kafka);
-
使用非 Java 的消费者或跨语言需要统一 header 格式时(需要由应用层保证传播)。
-
3) Spring Boot + Kafka 的具体实现示例(代码)
下面给出可直接运行的示例片段,包含两部分:
-
Producer:把 TraceContext 写到 Kafka header(示例展示如何把任意 trace header bytes 放入 header;如何获取 SkyWalking 的 ContextCarrier/注入点以“理论上”使用 SkyWalking API我会标注 — 若 agent 自动可省略)。
-
Consumer:从 Kafka header 中抽取 trace 信息并恢复上下文,然后在消费处理逻辑内创建/继续 span。
说明:SkyWalking 的 SDK/agent API(
ContextCarrier/ContextManager/CarrierItem等)在不同版本上方法名有细微差别 —— 我会给出最常见的实现思路与示例代码,并在关键处标注“这里调用 SkyWalking 的 inject/extract API”。如果你用的是 agent 的自动模式,可以跳过注入/抽取代码,仅关注 header 的读写验证。
3.1 Producer(Spring Kafka)—— 在消息 header 写入 trace 上下文
// pom.xml 需要 spring-kafka 依赖 // 简化的 ProducerService @Service public class KafkaProducerService { @Autowired private KafkaTemplate<String, String> kafkaTemplate; public void sendWithTraceContext(String topic, String key, String value) { ProducerRecord<String, String> record = new ProducerRecord<>(topic, key, value); // ---- 1) 如果你想手工注入 SkyWalking 的 tracing header ---- // 用 SkyWalking 的 ContextCarrier 注入 (伪代码示例 — 替换为你 agent 版本的真实 API) // ContextCarrier carrier = new ContextCarrier(); // ContextManager.inject(carrier); // for (CarrierItem item = carrier.items(); item.hasNext(); item = item.next()) { // record.headers().add(item.getHeadKey(), item.getHeadValue().getBytes(StandardCharsets.UTF_8)); // } // ---- 或者,若 agent 自动注入,则无需手工注入 —— agent 在 Kafka client 执行 send 时会自动注入 header ---- // 也可以手动放入一个自定义 header(用于检查或应用级追踪) String appTraceId = getCurrentTraceIdOrNull(); if (appTraceId != null) { record.headers().add("x-app-trace-id", appTraceId.getBytes(StandardCharsets.UTF_8)); } kafkaTemplate.send(record); } private String getCurrentTraceIdOrNull() { // 如果你有 SkyWalking 提供的读取接口可以读到当前 traceId,则返回 // 举例(伪): return ContextManager.getGlobalTraceId(); // 或者从 MDC 里读取 return org.slf4j.MDC.get("SW_CTX_TRACE_ID"); } }
要点:
-
ProducerRecord.headers().add(...)写入的 header 会随消息一起发送给 broker,并可被 consumer 读取。Kafka header 存为byte[]。 -
如果你用 SkyWalking agent 并且 agent 的 Kafka 插件生效,通常不需要上面的 ContextCarrier 手工注入,agent 会拦截 Kafka client 的
send()自动注入sw8header。但在某些自定义客户端或框架情况下,你可能需要手工注入。
3.2 Consumer(Spring Kafka)—— 从 header 恢复 TraceContext 并继续 Trace
@Component public class KafkaConsumer { // 使用 @KafkaListener consumes messages @KafkaListener(topics = "topic-test", groupId = "consumer-group-1") public void listen(ConsumerRecord<String, String> record) { // 1) 读取 header(示例读取 SkyWalking 的 sw8 或自定义 header) Header sw8Header = record.headers().lastHeader("sw8"); if (sw8Header != null) { byte[] headerBytes = sw8Header.value(); String headerValue = new String(headerBytes, StandardCharsets.UTF_8); System.out.println("sw8 header: " + headerValue); // ---- 2) 手工使用 SkyWalking SDK 从 header 抽取 context 并继续链路(伪代码) ---- // ContextCarrier carrier = new ContextCarrier(); // // 将 headerValue 逐项设回 CarrierItem // CarrierItem item = carrier.items(); // while (item.hasNext()) { // item.setHeadValue(extractValueFor(item.getHeadKey(), headerValue)); // item = item.next(); // } // ContextManager.extract(carrier); // // // 之后创建一个 EntrySpan 并在消费逻辑中使用它 // AbstractSpan span = ContextManager.createEntrySpan("Kafka/Consume/topic-test", null); // span.setComponent(ComponentsDefine.KAFKA); // try { // // 真实业务处理 // handleMessage(record.value()); // } finally { // ContextManager.stopSpan(); // } // 如果 agent 自动插桩并自动抽取,则你不需要以上手工 extract/createSpan 的代码。 } else { // 没有 sw8 header,可能 agent 未注入或 header 被清掉 System.out.println("No sw8 header present"); } // 其他业务处理 handleMessage(record.value()); } private void handleMessage(String payload) { // 你的消费逻辑 } }
要点:
-
用
record.headers().lastHeader("sw8")可以拿到 agent 注入的 header(如果存在)。用kafka-console-consumer也能看到 headers(--property print.headers=true)。 -
若 agent 没做自动抽取,你必须把 header 值还原到 SkyWalking 可理解的
ContextCarrier里并调用ContextManager.extract(carrier),随后创建 EntrySpan 并在处理完成后ContextManager.stopSpan()。具体 API 以你使用的 SkyWalking 版本为准(参考你 agent 的 javadocs 或 examples)。
4) 验证、排查清单(如果链路没被追踪到如何检查)
-
确认 agent 是否加载
-
应用启动参数是否包含
-javaagent:/path/skywalking-agent.jar? -
agent 日志(agent/logs/)是否有
Agent service started或 plugin kafka 加载日志。
-
-
检查 agent 是否支持你用的 Kafka 客户端版本
-
SkyWalking release notes / docs 列出支持的 client 版本。
-
-
验证消息 header 是否存在
-
使用
kafka-console-consumer --bootstrap-server ... --topic topic-test --from-beginning --property print.headers=true看消息 header;或在 consumer 应用里打印record.headers()。 -
若
sw8(或你自己注入的 header)不存在,说明上游 producer 没注入或中间代理删除 header。
-
-
检查网关/中间代理是否会移除 header
-
如果消息在发送/接收中经过某些中间层(例如跨语言桥接),确认它们不会丢 header。
-
-
确认 consumer 启动的进程是否有 agent
-
有时 producer 的 agent 正常而 consumer 的 agent 未挂载,consumer 端就没办法上报 span(尽管 header 在消息里)。
-
-
检查异步/线程传播
-
如果 consumer 在异步线程池中处理,确保在创建 Span 前已经完成 context 的恢复(ContextManager.extract)并将 context 传递到线程中。
-
-
查看 SkyWalking UI
-
查找 trace,观察是否出现 Messaging / MQ 相关的 span,或是否出现孤立 segment(producer segment 存在,consumer segment 不存在)。
-
5) 常见问题 & 最佳实践
-
Kafka header 不可用时:早期 Kafka 版本 (<0.11) 不支持 header。如果必须兼容,需把 trace context 编码到消息体(不推荐)或升级 Kafka 客户端/服务端。
-
跨语言场景:使用统一的 header 字段(如
sw8或 W3Ctraceparent),保证不同语言的 agent 或 SDK 都能解析。SkyWalking 提供跨语言通信的桥接能力(查看官网 docs)。 -
不要把大型 context 写入 header:header 应尽量小(TraceContext 本身很小,base64 编码即可)。
-
保证消费者处理前先恢复 context:否则处理发生的所有 Span 都会是独立 Trace。
-
代理/中间件注意保留 headers:有些消息代理转换/桥接会丢 header(例如某些桥接到旧队列的 adapter),需要确认或做额外传递逻辑。
-
使用 agent 自动化优先:如果 agent 能正常工作,自动注入/抽取更可靠、代码改动最小。手工方式仅用于特殊场景或调试。
6) 小结(要点回顾)
-
SkyWalking 可以跟踪 Producer → Kafka → Consumer 的链路,前提是 TraceContext 被正确注入到 Kafka message header(或 agent 自动完成注入)。
-
优先使用 SkyWalking Agent 的自动 Kafka 插件(通常无需代码改动)。
-
若自动不适用,手工在 Producer 把 Context 写到 header,并在 Consumer 在处理前从 header 恢复 Context(用 SkyWalking 的 ContextCarrier/ContextManager 或兼容的接口)。
-
验证方法:检查 agent 日志、查看 Kafka message headers、在 Consumer 打印/抽取 header,并在 SkyWalking UI 确认 Trace 是否完整。

浙公网安备 33010602011771号