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 的具体实现示例(代码)

下面给出可直接运行的示例片段,包含两部分:

  1. Producer:把 TraceContext 写到 Kafka header(示例展示如何把任意 trace header bytes 放入 header;如何获取 SkyWalking 的 ContextCarrier/注入点以“理论上”使用 SkyWalking API我会标注 — 若 agent 自动可省略)。

  2. 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() 自动注入 sw8 header。但在某些自定义客户端或框架情况下,你可能需要手工注入。


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) 验证、排查清单(如果链路没被追踪到如何检查)

  1. 确认 agent 是否加载

    • 应用启动参数是否包含 -javaagent:/path/skywalking-agent.jar

    • agent 日志(agent/logs/)是否有 Agent service started 或 plugin kafka 加载日志。

  2. 检查 agent 是否支持你用的 Kafka 客户端版本

    • SkyWalking release notes / docs 列出支持的 client 版本。

  3. 验证消息 header 是否存在

    • 使用 kafka-console-consumer --bootstrap-server ... --topic topic-test --from-beginning --property print.headers=true 看消息 header;或在 consumer 应用里打印 record.headers()

    • sw8(或你自己注入的 header)不存在,说明上游 producer 没注入或中间代理删除 header。

  4. 检查网关/中间代理是否会移除 header

    • 如果消息在发送/接收中经过某些中间层(例如跨语言桥接),确认它们不会丢 header。

  5. 确认 consumer 启动的进程是否有 agent

    • 有时 producer 的 agent 正常而 consumer 的 agent 未挂载,consumer 端就没办法上报 span(尽管 header 在消息里)。

  6. 检查异步/线程传播

    • 如果 consumer 在异步线程池中处理,确保在创建 Span 前已经完成 context 的恢复(ContextManager.extract)并将 context 传递到线程中。

  7. 查看 SkyWalking UI

    • 查找 trace,观察是否出现 Messaging / MQ 相关的 span,或是否出现孤立 segment(producer segment 存在,consumer segment 不存在)。


5) 常见问题 & 最佳实践

  • Kafka header 不可用时:早期 Kafka 版本 (<0.11) 不支持 header。如果必须兼容,需把 trace context 编码到消息体(不推荐)或升级 Kafka 客户端/服务端。

  • 跨语言场景:使用统一的 header 字段(如 sw8 或 W3C traceparent),保证不同语言的 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 是否完整。

 

posted @ 2025-11-06 14:07  Boblim  阅读(10)  评论(0)    收藏  举报