8. Kafka分布式链路追踪实现设计

前言

本文将从Kafka 提供的扩展机制出发,结合OpentracingJaeger ,实现Kafka的分布式链路追踪。

kafka-clients 版本:3.1.2
spring-kafka 版本:2.8.11

正文

一. Kafka的拦截器和监听器

先看一下Kafka的拦截器和监听器的接口定义。

首先是拦截器,分为生产者拦截器ProducerInterceptor 和消费者拦截器ConsumerInterceptor,接口定义如下。

java 复制代码
public interface ProducerInterceptor<K, V> extends Configurable {

    // 消息被发送前执行
    ProducerRecord<K, V> onSend(ProducerRecord<K, V> record);

    // 消息发送出现异常或者发送后触发回调时执行
    void onAcknowledgement(RecordMetadata metadata, Exception exception);

    void close();

}
java 复制代码
public interface ConsumerInterceptor<K, V> extends Configurable, AutoCloseable {

    // 消息拉取回来前执行
    ConsumerRecords<K, V> onConsume(ConsumerRecords<K, V> records);

    // 提交offset后执行
    void onCommit(Map<TopicPartition, OffsetAndMetadata> offsets);

    void close();

}

再看一下监听器的接口定义,如下所示。

java 复制代码
public interface ProducerListener<K, V> {

    // 消息发送成功后触发回调时执行
    default void onSuccess(ProducerRecord<K, V> producerRecord, RecordMetadata recordMetadata) {
    }

    // 消息发送有异常触发回调时执行
    default void onError(ProducerRecord<K, V> producerRecord, @Nullable RecordMetadata recordMetadata,
                         Exception exception) {
    }

}

那么生产者生产消息和消费者拉取消息这两个动作,结合拦截器和监听器,一个简单的图示图下。

二. Kafka分布式链路追踪Span模型设计

Kafka 作为消息中间件,支持消息的pushpull ,这样子的信息传递方式,有别于使用RestTemplate 请求传递消息,但是本质还是将消息传递到了下游,那么我们稍加抽象,就可以得到如下的Span模型。

其实就是在生产者这里发送消息时创建代表下游的Span ,然后将Span 通过RecordHeader 来传递给到下游,这样子整体的思路就和我们使用RestTemplate 请求下游时,把Span 放到HTTP请求头里传递给下游是一样的。

三. Kafka生产者链路追踪设计和实现

1. Kafka生产者链路追踪设计

初步看好像可以基于生产者拦截器来实现,例如在onSend() 方法中创建出来Span 并加到ProducerRecordHeader 中,然后在onAcknowledgement() 方法中调用Spanfinish() 方法来记录Span,但是实际是不能基于生产者拦截器来实现的,理由如下。

  1. onSend()onAcknowledgement() 方法中,入参都只有发送的消息,而我们记录Span时需要的一些其它信息,很难拿到;
  2. 没有办法把Tracer 和装饰器等对象设置给生产者拦截器,这是因为生产者拦截器的初始化是在KafkaProducer 中读取到拦截器的全限定名然后再通过反射的方式创建出来的,我们很难把Tracer和装饰器等对象设置给生产者拦截器。

鉴于上述的问题,我们就不能基于生产者拦截器来实现Kafka 生产者链路追踪,有一个很好的取代的思路是,我们在KafkaTemplate 之上再包装一层,作为我们自己的增强的KafkaTemplate ,这里命名为HoneyKafkaTemplate ,然后我们再定义自己的拦截器接口HoneyKafkaProducerInterceptor ,当使用HoneyKafkaTemplate 来发送消息时,会先进入到HoneyKafkaProducerInterceptor ,创建Span 和记录Span 的功能都写在HoneyKafkaProducerInterceptor 中,最终发送消息时,还是将这个发送的动作委派给原生的KafkaTemplate,整体的结构如下所示。

2. Kafka生产者链路追踪实现

首先我们实现HoneyKafkaTemplate 来取代原生的KafkaTemplateHoneyKafkaTemplate如下所示。

java 复制代码
public class HoneyKafkaTemplate<K, V> extends KafkaTemplate<K, V> {

    public HoneyKafkaTemplate(ProducerFactory<K, V> producerFactory) {
        super(producerFactory);
    }

    public HoneyKafkaTemplate(ProducerFactory<K, V> producerFactory, Map<String, Object> configOverrides) {
        super(producerFactory, configOverrides);
    }

    public HoneyKafkaTemplate(ProducerFactory<K, V> producerFactory, boolean autoFlush) {
        super(producerFactory, autoFlush);
    }

    public HoneyKafkaTemplate(ProducerFactory<K, V> producerFactory, boolean autoFlush, Map<String, Object> configOverrides) {
        super(producerFactory, autoFlush, configOverrides);
    }

}

然后提供一个自动装配的配置类HoneyKafkaTemplateConfig,如下所示。

java 复制代码
@Configuration
@AutoConfigureBefore(KafkaAutoConfiguration.class)
public class HoneyKafkaTemplateConfig {

    @Bean
    public HoneyKafkaTemplate<?, ?> kafkaTemplate(ProducerFactory<Object, Object> kafkaProducerFactory) {
        return new HoneyKafkaTemplate<>(kafkaProducerFactory);
    }

}

因为KafkaAutoConfiguration 在注册KafkaTemplate 时,添加了@ConditionalOnMissingBean (KafkaTemplate.class ),所以只需要保证我们的HoneyKafkaTemplateConfigKafkaAutoConfiguration 之前被处理,那么我们注册的HoneyKafkaTemplate 就能取代原生的KafkaTemplate来达到以假乱真的效果。

最后要在spring.factories 文件中指定我们的自动装配类以及在pom 文件中添加spring-kafka依赖,如下所示。

yaml 复制代码
org.springframework.boot.autoconfigure.EnableAutoConfiguration=\
 com.honey.tracing.config.HoneyTracingConfig,\
 com.honey.tracing.config.HoneyTracingFilterConfig,\
 com.honey.tracing.config.HoneyRestTemplateTracingConfig,\
 com.honey.tracing.config.HoneyKafkaTemplateConfig
xml 复制代码
<dependency>
    <groupId>org.springframework.kafka</groupId>
    <artifactId>spring-kafka</artifactId>
    <scope>provided</scope>
</dependency>

现在再给出自定义拦截器HoneyKafkaProducerInterceptor的定义,如下所示。

java 复制代码
public interface HoneyKafkaProducerInterceptor<K, V> {

    /**
     * 拦截Kafka生产者的消息发送。
     *
     * @param producerRecord 生产者发送的消息。
     */
    ListenableFuture<SendResult<K, V>> intercept(
            ProducerRecord<K, V> producerRecord,
            HoneyKafkaProducerInterceptorChain<K, V> kafkaProducerInterceptorChain);

}

我们再提供一个实现类,如下所示。

java 复制代码
public class HoneyKafkaProducerTracingInterceptor<K, V> implements HoneyKafkaProducerInterceptor<K, V> {

    private final Tracer tracer;
    private final List<HoneyKafkaTracingProducerDecorator<K, V>> kafkaTracingProducerDecorators;

    public HoneyKafkaProducerTracingInterceptor(Tracer tracer, List<HoneyKafkaTracingProducerDecorator<K, V>> kafkaTracingProducerDecorators) {
        this.tracer = tracer;
        this.kafkaTracingProducerDecorators = kafkaTracingProducerDecorators;
    }

    @Override
    public ListenableFuture<SendResult<K, V>> intercept(ProducerRecord<K, V> producerRecord,
                                                        HoneyKafkaProducerInterceptorChain<K, V> kafkaProducerInterceptorChain) {
        if (tracer.activeSpan() == null) {
            return kafkaProducerInterceptorChain.intercept(producerRecord);
        }

        // 生成Kafka生产者对应的Span
        // 类似于RestTemplate调用前的Span
        Span span = tracer.buildSpan(HONEY_KAFKA_NAME)
                .withTag(Tags.SPAN_KIND.getKey(), Tags.SPAN_KIND_CLIENT)
                .start();

        for (HoneyKafkaTracingProducerDecorator<K, V> kafkaTracingProducerDecorator : kafkaTracingProducerDecorators) {
            try {
                kafkaTracingProducerDecorator.onSend(span, producerRecord);
            } catch (Exception e) {
                // do nothing
            }
        }

        ListenableFuture<SendResult<K, V>> result;
        try (Scope scope = tracer.activateSpan(span)) {
            // 设置Kafka服务端的host
            String hostString = kafkaProducerInterceptorChain.getHoneyKafkaTemplate().getProducerFactory()
                    .getConfigurationProperties().get(BOOTSTRAP_SERVERS_CONFIG).toString();
            span.setTag(FIELD_HOST, hostString.substring(1, hostString.length() - 1));
            // host需要传递给消费者
            span.setBaggageItem(FIELD_HOST, hostString.substring(1, hostString.length() - 1));

            // 把SpanContext注入到ProducerRecord的Headers中
            tracer.inject(span.context(), Format.Builtin.HTTP_HEADERS, new HoneyKafkaCarrier(producerRecord.headers()));

            try {
                result = kafkaProducerInterceptorChain.intercept(producerRecord);
            } catch (Exception e1) {
                for (HoneyKafkaTracingProducerDecorator<K, V> kafkaTracingProducerDecorator : kafkaTracingProducerDecorators) {
                    try {
                        kafkaTracingProducerDecorator.onError(span, producerRecord);
                    } catch (Exception e2) {
                        // do nothing
                    }
                }
                throw e1;
            }

            for (HoneyKafkaTracingProducerDecorator<K, V> kafkaTracingProducerDecorator : kafkaTracingProducerDecorators) {
                try {
                    kafkaTracingProducerDecorator.onSuccess(span, producerRecord);
                } catch (Exception e) {
                    // do nothing
                }
            }
        } finally {
            span.finish();
            tracer.activeSpan().log(RequestStackUtil.assembleRequestStack((JaegerSpan) span));
        }

        return result;
    }

}

和之前实现的RestTemplate 的拦截器逻辑几乎是一模一样,区别就在于一个是Span 放在HTTP 请求头,一个是Span 放在消息的Header 中,以及我们在Inject 时使用了一个自定义的HoneyKafkaCarrier,如下所示。

java 复制代码
public class HoneyKafkaCarrier implements TextMap {

    private final Headers headers;

    public HoneyKafkaCarrier(Headers headers) {
        this.headers = headers;
    }

    @NotNull
    @Override
    public Iterator<Map.Entry<String, String>> iterator() {
        Map<String, String> headerMap = new HashMap<>();
        for (Header header : headers) {
            headerMap.put(header.key(), new String(header.value()));
        }
        return headerMap.entrySet().iterator();
    }

    @Override
    public void put(String key, String value) {
        headers.add(key, value.getBytes());
    }

}

特别注意到HoneyKafkaProducerInterceptorChain ,我们将所有的HoneyKafkaProducerInterceptor创建为了一个连接器链,如下所示。

java 复制代码
public class HoneyKafkaProducerInterceptorChain<K, V> {

    private final Iterator<HoneyKafkaProducerInterceptor<K, V>> kafkaProducerInterceptors;
    private final HoneyKafkaTemplate<K, V> honeyKafkaTemplate;

    public HoneyKafkaProducerInterceptorChain(Iterator<HoneyKafkaProducerInterceptor<K, V>> kafkaProducerInterceptors,
                                              HoneyKafkaTemplate<K, V> honeyKafkaTemplate) {
        this.kafkaProducerInterceptors = kafkaProducerInterceptors;
        this.honeyKafkaTemplate = honeyKafkaTemplate;
    }

    public HoneyKafkaTemplate<K, V> getHoneyKafkaTemplate() {
        return honeyKafkaTemplate;
    }

    public ListenableFuture<SendResult<K, V>> intercept(ProducerRecord<K, V> producerRecord) {
        if (kafkaProducerInterceptors.hasNext()) {
            // 拦截器没执行完则先执行拦截器
            return kafkaProducerInterceptors.next().intercept(producerRecord, this);
        }
        // 拦截器全部执行完后才发送消息
        return honeyKafkaTemplate.actuallySend(producerRecord);
    }

}

目的如下。

  1. 支持后续添加更多的拦截器;
  2. 让连接器链持有HoneyKafkaTemplate 从而所有拦截器都持有了HoneyKafkaTemplate

至于装饰器,我们暂时就定义一个空接口出来,后面按需添加实现类,如下所示。

java 复制代码
/**
 * Kafka生产者链路追踪装饰器。
 */
public interface HoneyKafkaTracingProducerDecorator<K, V> {

    void onSend(Span span, ProducerRecord<K, V> producerRecord);

    void onSuccess(Span span, ProducerRecord<K, V> producerRecord);

    void onError(Span span, ProducerRecord<K, V> producerRecord);

}

现在已经有拦截器链了,接下来要做的事情就是把拦截器链给到我们的HoneyKafkaTemplate,如下所示。

java 复制代码
public class HoneyKafkaTemplate<K, V> extends KafkaTemplate<K, V> {

    private List<HoneyKafkaProducerInterceptor<K, V>> kafkaProducerInterceptors;

    public HoneyKafkaTemplate(ProducerFactory<K, V> producerFactory,
                              List<HoneyKafkaProducerInterceptor<K, V>> kafkaProducerInterceptors) {
        super(producerFactory);
        this.kafkaProducerInterceptors = kafkaProducerInterceptors;
    }

    public HoneyKafkaTemplate(ProducerFactory<K, V> producerFactory, Map<String, Object> configOverrides) {
        super(producerFactory, configOverrides);
    }

    public HoneyKafkaTemplate(ProducerFactory<K, V> producerFactory, boolean autoFlush) {
        super(producerFactory, autoFlush);
    }

    public HoneyKafkaTemplate(ProducerFactory<K, V> producerFactory, boolean autoFlush, Map<String, Object> configOverrides) {
        super(producerFactory, autoFlush, configOverrides);
    }

    public ListenableFuture<SendResult<K, V>> actuallySend(ProducerRecord<K, V> producerRecord) {
        return super.doSend(producerRecord);
    }

    @NotNull
    @Override
    public ListenableFuture<SendResult<K, V>> doSend(@NotNull ProducerRecord<K, V> producerRecord) {
        HoneyKafkaProducerInterceptorChain<K, V> kafkaProducerInterceptorChain
                = new HoneyKafkaProducerInterceptorChain<>(kafkaProducerInterceptors.iterator(), this);
        return kafkaProducerInterceptorChain.intercept(producerRecord);
    }

}

这样在使用HoneyKafkaTemplate 来发送消息时,可以先让拦截器链执行,然后再把消息委派给原生KafkaTemplate来发送。

现在需要修改注册HoneyKafkaTemplate的自动装配类,如下所示。

java 复制代码
@Configuration
@ConditionalOnBean(KafkaTemplate.class)
@AutoConfigureBefore(KafkaAutoConfiguration.class)
public class HoneyKafkaTemplateConfig {

    @Bean
    public HoneyKafkaTemplate<?, ?> kafkaTemplate(ProducerFactory<Object, Object> kafkaProducerFactory,
                                                  List<HoneyKafkaProducerInterceptor<Object, Object>> kafkaProducerInterceptors) {
        return new HoneyKafkaTemplate<>(kafkaProducerFactory, kafkaProducerInterceptors);
    }

}

即在创建HoneyKafkaTemplate 时需要把注册到容器中的HoneyKafkaProducerInterceptor 设置进去。同时还要为我们的拦截器的实现HoneyKafkaProducerTracingInterceptor提供一个自动装配类,如下所示。

java 复制代码
@Configuration
@ConditionalOnBean(KafkaTemplate.class)
@AutoConfigureAfter(HoneyTracingConfig.class)
public class HoneyKafkaTracingConfig {

    @Bean
    public HoneyKafkaProducerInterceptor kafkaProducerTracingInterceptor(
            Tracer tracer, List<HoneyKafkaTracingProducerDecorator<Object, Object>> kafkaTracingProducerDecorators) {
        return new HoneyKafkaProducerTracingInterceptor<>(tracer, kafkaTracingProducerDecorators);
    }

}

对应修改spring.factories文件如下所示。

yaml 复制代码
org.springframework.boot.autoconfigure.EnableAutoConfiguration=\
 com.honey.tracing.config.HoneyTracingConfig,\
 com.honey.tracing.config.HoneyTracingFilterConfig,\
 com.honey.tracing.config.HoneyRestTemplateTracingConfig,\
 com.honey.tracing.config.HoneyKafkaTemplateConfig,\
 com.honey.tracing.config.HoneyKafkaTracingConfig

至此我们的具备分布式链路追踪功能的HoneyKafkaTemplate就能够使用啦,最后给出上述使用到的常量类,如下所示。

java 复制代码
public class CommonConstants {

    public static final double DEFAULT_SAMPLE_RATE = 1.0;

    public static final String HONEY_TRACER_NAME = "HoneyTracer";
    public static final String HONEY_REST_TEMPLATE_NAME = "HoneyRestTemplate";
    public static final String HONEY_KAFKA_NAME = "HoneyKafka";

    public static final String FIELD_HOST = "host";
    public static final String FIELD_API = "api";
    public static final String FIELD_HTTP_CODE = "httpCode";
    public static final String FIELD_SUB_SPAN_ID = "subSpanId";
    public static final String FIELD_SUB_HTTP_CODE = "subHttpCode";
    public static final String FIELD_SUB_TIMESTAMP = "subTimestamp";
    public static final String FIELD_SUB_DURATION = "subDuration";
    public static final String FIELD_SUB_HOST = "subHost";

    public static final String HOST_PATTERN_STR = "(?<=(https://|http://)).*?(?=/)";

    public static final String SLASH = "/";

    public static final String LOG_EVENT_KIND = "logEventKind";
    public static final String LOG_EVENT_KIND_REQUEST_STACK = "requestStack";

}

四. Kafka消费者链路追踪设计和实现

1. Kafka消费者链路追踪设计

同样的,Kafka消费者拦截器也不适合来实现分布式链路追踪,理由和第三节第1小节中基本一致。

通常在Spring 中实现Kafka 消息消费时,我们使用@KafkaListener 注解,被该注解修饰的方法用于消费消息并处理,入参是ConsumerRecord ,所以我们可以选择提供切面来切这些被@KafkaListener 注解修饰的方法并在方法执行前后操作Span ,同时为了和@KafkaListener注解解耦,我们可以专门定义一个注解来作为我们的切点,那么现在的消息消费流程就成下面这样了。

2. Kafka消费者链路追踪实现

首先定义作为切点的注解,如下所示。

java 复制代码
/**
 * Kafka消费者方法使用该注解。
 */
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface HoneyKafkaTracing {

}

对应的切面如下所示。

java 复制代码
@Aspect
public class HoneyKafkaConsumerTracingAspect {

    private final HoneyKafkaConsumerInterceptor kafkaConsumerTracingInterceptor;

    public HoneyKafkaConsumerTracingAspect(HoneyKafkaConsumerInterceptor kafkaConsumerTracingInterceptor) {
        this.kafkaConsumerTracingInterceptor = kafkaConsumerTracingInterceptor;
    }

    @Pointcut("@annotation(com.honey.tracing.kafka.consumer.interceptor.HoneyKafkaTracing)")
    private void kafkaTracing() {

    }

    @Around("kafkaTracing()")
    public Object intercept(ProceedingJoinPoint joinPoint) throws Throwable {
        return kafkaConsumerTracingInterceptor.intercept(joinPoint);
    }

}

在环绕通知中,会调用到我们自定义的Kafka消费者拦截器,接口定义如下。

java 复制代码
public interface HoneyKafkaConsumerInterceptor {

    Object intercept(ProceedingJoinPoint joinPoint) throws Throwable;

}

具体的实现类,如下所示。

java 复制代码
public class HoneyKafkaConsumerTracingInterceptor<K, V> implements HoneyKafkaConsumerInterceptor {

    private final Tracer tracer;
    private final List<HoneyKafkaTracingConsumerDecorator<K, V>> kafkaTracingConsumerDecorators;

    public HoneyKafkaConsumerTracingInterceptor(
            Tracer tracer, List<HoneyKafkaTracingConsumerDecorator<K, V>> kafkaTracingConsumerDecorators) {
        this.tracer = tracer;
        this.kafkaTracingConsumerDecorators = kafkaTracingConsumerDecorators;
    }

    @Override
    public Object intercept(ProceedingJoinPoint joinPoint) throws Throwable {
        ConsumerRecord consumerRecord = null;
        // 找到Kafka消息
        Object[] args = joinPoint.getArgs();
        for (Object arg : args) {
            if (arg instanceof ConsumerRecord) {
                consumerRecord = (ConsumerRecord) arg;
            }
        }

        if (consumerRecord == null) {
            // 没有获取到Kafka消息则不处理链路
            return joinPoint.proceed();
        }

        SpanContext extractSpanContext = tracer.extract(Format.Builtin.HTTP_HEADERS,
                new HoneyKafkaCarrier(consumerRecord.headers()));
        Span span = tracer.buildSpan(HONEY_KAFKA_NAME)
                .asChildOf(extractSpanContext)
                .withTag(Tags.SPAN_KIND.getKey(), Tags.SPAN_KIND_SERVER)
                .start();
        span.setTag(FIELD_HOST, span.getBaggageItem(FIELD_HOST));

        for (HoneyKafkaTracingConsumerDecorator<K, V> kafkaTracingConsumerDecorator : kafkaTracingConsumerDecorators) {
            try {
                kafkaTracingConsumerDecorator.onReceive(span, consumerRecord);
            } catch (Exception e) {
                // do nothing
            }
        }

        Object result;
        try (Scope scope = tracer.activateSpan(span)) {
            try {
                result = joinPoint.proceed();
            } catch (Exception e1) {
                for (HoneyKafkaTracingConsumerDecorator<K, V> kafkaTracingConsumerDecorator : kafkaTracingConsumerDecorators) {
                    try {
                        kafkaTracingConsumerDecorator.onError(span, consumerRecord);
                    } catch (Exception e2) {
                        // do nothing
                    }
                }
                throw e1;
            }

            for (HoneyKafkaTracingConsumerDecorator<K, V> kafkaTracingConsumerDecorator : kafkaTracingConsumerDecorators) {
                try {
                    kafkaTracingConsumerDecorator.onFinished(span, consumerRecord);
                } catch (Exception e) {
                    // do nothing
                }
            }
        } finally {
            span.finish();
        }

        return result;
    }

}

同样的我们也定义了一个装饰器接口,如下所示。

java 复制代码
/**
 * Kafka消费者链路追踪装饰器。
 */
public interface HoneyKafkaTracingConsumerDecorator<K, V> {

    void onReceive(Span span, ConsumerRecord<K, V> consumerRecord);

    void onFinished(Span span, ConsumerRecord<K, V> consumerRecord);

    void onError(Span span, ConsumerRecord<K, V> consumerRecord);

}

然后需要修改一下自动装配类HoneyKafkaTracingConfig 来将我们自定义的Kafka消费者拦截器和切面注册到容器中,修改如下。

java 复制代码
@Configuration
@AutoConfigureAfter(HoneyTracingConfig.class)
public class HoneyKafkaTracingConfig {

    @Bean
    public HoneyKafkaProducerInterceptor kafkaProducerTracingInterceptor(
            Tracer tracer, List<HoneyKafkaTracingProducerDecorator<Object, Object>> kafkaTracingProducerDecorators) {
        return new HoneyKafkaProducerTracingInterceptor<>(tracer, kafkaTracingProducerDecorators);
    }

    @Bean
    public HoneyKafkaConsumerInterceptor honeyKafkaConsumerInterceptor(
            Tracer tracer, List<HoneyKafkaTracingConsumerDecorator<Object, Object>> kafkaTracingProducerDecorators) {
        return new HoneyKafkaConsumerTracingInterceptor(tracer, kafkaTracingProducerDecorators);
    }

    @Bean
    public HoneyKafkaConsumerTracingAspect honeyKafkaConsumerTracingAspect(
            HoneyKafkaConsumerInterceptor kafkaConsumerTracingInterceptor) {
        return new HoneyKafkaConsumerTracingAspect(kafkaConsumerTracingInterceptor);
    }

}

最后,由于使用到了切面,所以需要引入SpringAOP的依赖,如下所示。

xml 复制代码
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-aop</artifactId>
</dependency>

五. 功能验证

现在利用一个简单的Kafka 的例子来验证Kafka分布式链路追踪的功能。

我们让example-service-1 充当生产者,pom 中引入Kafka依赖如下所示。

xml 复制代码
<dependency>
    <groupId>org.springframework.kafka</groupId>
    <artifactId>spring-kafka</artifactId>
</dependency>

然后添加生产者的配置,如下所示。

yaml 复制代码
spring:
  kafka:
    bootstrap-servers: 127.0.0.1:9092
    producer:
      key-serializer: org.apache.kafka.common.serialization.StringSerializer
      value-serializer: org.apache.kafka.common.serialization.StringSerializer

最后增加一个KafkaController 来发送Kafka消息,如下所示。

java 复制代码
@RestController
public class KafkaController {

    private static final String TEST_TOPIC = "testTopic";
    private static final String TEST_MESSAGE = "testMessage";

    @Autowired
    private KafkaTemplate<String, String> kafkaTemplate;

    @GetMapping("/kafka/send")
    public void send(String url) {
        kafkaTemplate.send(TEST_TOPIC, TEST_MESSAGE);
    }

}

我们再让example-service-2 充当消费者,pom 中引入Kafka依赖如下所示。

xml 复制代码
<dependency>
    <groupId>org.springframework.kafka</groupId>
    <artifactId>spring-kafka</artifactId>
</dependency>

然后添加消费者的配置,如下所示。

yaml 复制代码
spring:
  kafka:
    bootstrap-servers: 127.0.0.1:9092
    consumer:
      key-deserializer: org.apache.kafka.common.serialization.StringDeserializer
      value-deserializer: org.apache.kafka.common.serialization.StringDeserializer
      group-id: kafka-tracing

最后增加一个KafkaService来消费消息,如下所示。

java 复制代码
@Service
public class KafkaService {

    private static final String TEST_TOPIC = "testTopic";

    @HoneyKafkaTracing
    @KafkaListener(topics = TEST_TOPIC)
    public void onMessage(ConsumerRecord<String, String> consumerRecord) {
        System.out.println(consumerRecord.value());
    }

}

最终example-service-1打印链路如下所示。

json 复制代码
{
	"traceId": "904360d3ae85faa7ce5d698debda55fd",
	"spanId": "ce5d698debda55fd",
	"parentSpanId": "0000000000000000",
	"timestamp": "1708067308855",
	"duration": "306",
	"httpCode": "200",
	"host": "http://localhost:8080",
	"requestStacks": [{
		"subSpanId": "7cb4062ca0fa7cdd",
		"subHttpCode": "null",
		"subTimestamp": "1708067308876",
		"subDuration": "262",
		"subHost": "127.0.0.1:9092"
	}]
}

example-service-2打印链路如下所示。

json 复制代码
{
	"traceId": "904360d3ae85faa7ce5d698debda55fd",
	"spanId": "7cb4062ca0fa7cdd",
	"parentSpanId": "ce5d698debda55fd",
	"timestamp": "1708067309171",
	"duration": "5",
	"httpCode": "null",
	"host": "127.0.0.1:9092",
	"requestStacks": []
}

可见链路信息通过Kafka消息得到了传递。

总结

Kafka 虽然是通过pushpull 方式来传递消息,但是我们可以将这种方式抽象为生产者直接将消息传递给到了消费者,传递介质就是Record ,同时Record 中预留了Header ,我们就可以将Span 通过Header 来传递,整个模式就和通过RestTemplate请求下游一模一样了。

Kafka 分布式链路追踪的实现,本质是拦截Kafka 的消息发送和接收,虽然Kafka 提供了拦截器,但是由于Kafka 拦截器的加载机制问题,使用起来并不是很方便,所以我们选择自定义拦截器和切面的方式来拦截Kafka 的消息发送和接收,同时为了方便的使用我们自定义的拦截器,我们还自定义了KafkaTemplate 来对原生KafkaTemplate 进行了包装,使用自定义KafkaTemplate 发送消息时,可以先在自定义KafkaTemplate 中先应用自定义拦截器的逻辑,然后真正发送消息的动作就委派给原生KafkaTemplate

本文中,honey-starter-tracing的工程目录结构如下所示。

测试demo的工程目录结构如下所示。

相关推荐
drebander16 分钟前
使用 Java Stream 优雅实现List 转化为Map<key,Map<key,value>>
java·python·list
乌啼霜满天24919 分钟前
Spring 与 Spring MVC 与 Spring Boot三者之间的区别与联系
java·spring boot·spring·mvc
tangliang_cn24 分钟前
java入门 自定义springboot starter
java·开发语言·spring boot
程序猿阿伟25 分钟前
《智能指针频繁创建销毁:程序性能的“隐形杀手”》
java·开发语言·前端
Grey_fantasy35 分钟前
高级编程之结构化代码
java·spring boot·spring cloud
新知图书36 分钟前
Rust编程与项目实战-模块std::thread(之一)
开发语言·后端·rust
弗锐土豆42 分钟前
工业生产安全-安全帽第二篇-用java语言看看opencv实现的目标检测使用过程
java·opencv·安全·检测·面部
Elaine20239142 分钟前
零碎04 MybatisPlus自定义模版生成代码
java·spring·mybatis
盛夏绽放1 小时前
Node.js 和 Socket.IO 实现实时通信
前端·后端·websocket·node.js
小小大侠客1 小时前
IText创建加盖公章的pdf文件并生成压缩文件
java·pdf·itext