kafka拦截器 | 深度解析kafka策略

1、背景引入:很多人不懂kafka拦截器,拦截器是什么?

Producer拦截器(interceptor)是在Kafka 0.10版本被引入的,主要用于实现clients端的定制化控制逻辑。

对于producer而言,interceptor使得用户在消息发送前以及producer回调逻辑前有机会对消息做一些定制化需求,比如修改消息等。同时,producer允许用户指定多个interceptor按序作用于同一条消息从而形成一个拦截链(interceptor chain)。

拦截器有什么?

Intercetpor的实现接口是

org.apache.kafka.clients.producer.ProducerInterceptor,其定义的方法包括:

(1)configure(configs)

获取配置信息和初始化数据时调用。

(2)onSend(ProducerRecord):

该方法封装进KafkaProducer.send方法中,即它运行在用户主线程中。Producer确保在消息被序列化以及计算分区前调用该方法。用户可以在该方法中对消息做任何操作,但最好保证不要修改消息所属的topic和分区,否则会影响目标分区的计算

(3)onAcknowledgement(RecordMetadata, Exception):

该方法会在消息被应答或消息发送失败时调用,并且通常都是在producer回调逻辑触发之前。onAcknowledgement运行在producer的IO线程中,因此不要在该方法中放入很重的逻辑,否则会拖慢producer的消息发送效率

(4)close:

关闭interceptor,主要用于执行一些资源清理工作

如前所述,interceptor可能被运行在多个线程中,因此在具体实现时用户需要自行确保线程安全。另外倘若指定了多个interceptor,则producer将按照指定顺序调用它们,并仅仅是捕获每个interceptor可能抛出的异常记录到错误日志中而非在向上传递。这在使用过程中要特别留意。

2、案例演示 1)需求:

实现一个简单的双interceptor组成的拦截链。第一个interceptor会在消息发送前将时间戳信息加到消息value的最前部;第二个interceptor会在消息发送后更新成功发送消息数或失败发送消息数。

2)案例实操

(1)增加时间戳拦截器

import java.util.Map;

import org.apache.kafka.clients.producer.ProducerInterceptor;

import org.apache.kafka.clients.producer.ProducerRecord;

import org.apache.kafka.clients.producer.RecordMetadata;

//自定义类要实现ProducerInterceptor接口,并且实现接口中的方法

public class TimeInterceptor implements ProducerInterceptor<String, String> {

//初始化方法

  @Override

  public void configure(Map<String, ?> configs) {

  }

//发送方法,数据的处理代码逻辑写入此方法中

  @Override

  public ProducerRecord<String, String> onSend(ProducerRecord<String, String> record) {

        // 创建一个新的record,把时间戳写入消息体的最前部

        return new ProducerRecord(record.topic(), record.partition(), record.timestamp(), record.key(),

        System.currentTimeMillis() + "," + record.value().toString());

  }

//该方法会在消息被应答或消息发送失败时调用,并且通常都是在producer回 //调逻辑触发之前

  @Override

  public void onAcknowledgement(RecordMetadata metadata, Exception exception) {

  }

//关闭interceptor,主要用于执行一些资源清理工作

  @Override

  public void close() {

  }

}

(2)统计发送消息成功和发送失败消息数,并在producer关闭时打印这两个计数器

import java.util.Map;

import org.apache.kafka.clients.producer.ProducerInterceptor;

import org.apache.kafka.clients.producer.ProducerRecord;

import org.apache.kafka.clients.producer.RecordMetadata;

public class CounterInterceptor implements ProducerInterceptor<String, String>{

private int errorCounter = 0;

private int successCounter = 0;



  @Override

  public void configure(Map<String, ?> configs) {



  }



  @Override

  public ProducerRecord<String, String> onSend(ProducerRecord<String, String> record) {

         return record;

  }



  @Override

  public void onAcknowledgement(RecordMetadata metadata, Exception exception) {

        // 统计成功和失败的次数

    if (exception == null) {

        successCounter++;

    } else {

        errorCounter++;

    }

  }



  @Override

  public void close() {

    // 保存结果

    System.out.println("Successful sent: " + successCounter);

    System.out.println("Failed sent: " + errorCounter);

  }

}

(3)producer主程序

import java.util.ArrayList;

import java.util.List;

import java.util.Properties;

import org.apache.kafka.clients.producer.KafkaProducer;

import org.apache.kafka.clients.producer.Producer;

import org.apache.kafka.clients.producer.ProducerConfig;

import org.apache.kafka.clients.producer.ProducerRecord;

public class InterceptorProducer {

  public static void main(String[] args) throws Exception {

        // 1 设置配置信息

       Properties props = new Properties();

//设置kafka集群的ip和端口

        props.put("bootstrap.servers", "hadoop102:9092");

//设置kafka接收producer的反馈机制

        props.put("acks", "all");

//设置连接失败重试次数

        props.put("retries", 0);

//设置数据发送的批量大小

        props.put("batch.size", 16384);

//sender线程在检查batch是否ready时候,判断有没有过期的参数,默认

//大小是0ms。

        props.put("linger.ms", 1);

//内存缓冲的大小

        props.put("buffer.memory", 33554432);

//设置key序列化

        props.put("key.serializer", "org.apache.kafka.common.serialization.StringSerializer");

//设置value的序列化

        props.put("value.serializer", "org.apache.kafka.common.serialization.StringSerializer");



        // 2 构建拦截链,拦截器链有顺序

        List<String> interceptors = new ArrayList<>();

//增加时间拦截器

   interceptors.add("com.kafka.interceptor.TimeInterceptor");                   //增加计数器拦截器

interceptors.add("com.kafka.interceptor.CounterInterceptor");

//将拦截器加入到props参数列表中

props.put(ProducerConfig.INTERCEPTOR_CLASSES_CONFIG, interceptors);

        //设置kafka的主题

        String topic = "first";

//创建生产者

        Producer<String, String> producer = new KafkaProducer<>(props);



        // 3 发送消息

        for (int i = 0; i < 10; i++) {



          ProducerRecord<String, String> record = new ProducerRecord<>(topic, "message" + i);

            producer.send(record);

        }

        // 4 一定要关闭producer,这样才会调用interceptor的close方法

        producer.close();

  }

}

3)测试

(1)在kafka上启动消费者,然后运行客户端java程序。

[kafka@hadoop102 kafka]$ bin/kafka-console-consumer.sh \

--zookeeper hadoop102:2181 --from-beginning --topic first

1501904047034,message0

1501904047225,message1

1501904047230,message2

1501904047234,message3

1501904047236,message4

1501904047240,message5

1501904047243,message6

1501904047246,message7

1501904047249,message8

1501904047252,message9

(2)观察java平台控制台输出数据如下:

Successful sent: 10

Failed sent: 0

3、关于高并发下kafka producer send异步发送耗时问题的分析 最近开发网关服务的过程当中,需要用到kafka转发消息与保存日志,在进行压测的过程中由于是多线程并发操作kafka producer 进行异步send,发现send耗时有时会达到几十毫秒的阻塞,很大程度上上影响了并发的性能,而在后续的测试中发现单线程发送反而比多线程发送效率高出几倍。所以就对kafka API send 的源码进行了一下跟踪和分析,在此总结记录一下。

多线程高并发情况下,针对dq的操作会存在比较大的资源竞争,虽然是基于内存的操作,每个线程持有锁的时间极短,但相比单线程情况,高并发情况下线程开辟较多,锁竞争和cpu上下文切换都比较频繁,会造成一定的性能损耗,产生阻塞耗时。

分析到这里你就会发现,其实KafkaProducer这个异步发送是建立在生产者和消费者模式上的,send的真正操作并不是直接异步发送,而是把数据放在一个中间队列中。那么既然有生产者在往内存队列中放入数据,那么必然会有一个专有的线程负责把这些数据真正发送出去。我们通过监控jvm线程信息可以看到,KafkaProducer创建后确实会启动一个守护线程用于消息的发送。

producer.send操作本身其实是个基于内存的存储操作,耗时几乎可以忽略不计,但由于高并发情况下,线程同步会有一定的性能损耗,当然这个损耗在一般的应用场景下几乎是可以忽略不计的,但如果是数据量比较大,高并发的场景下会比较明显。

针对上面的问题分析,这里说下我个人的一些总结:

1、首先避免多线程操作producer发送数据,你可以采用生产者消费者模式把producer.send从你的多线程操作中解耦出来,维护一个你要发送的消息队列,单独开辟一个线程操作;

2、可能有的小伙伴会问,那么多创建几个producer的实例或者维护一个producer池可以吗,我原本也是这个想法,只是在测试中发现效果也不是很理想,我估计是由于创建producer实例过多,导致线程数量也跟着增加,本身的业务线程再加上kafka的线程,线程上下文切换比较频繁,CPU资源压力比较大,效率也不如单线程操作;

3、这个问题其实真是针对API操作来讲的,send操作并不是真正的数据发送,真正的数据发送由守护线程进行;按照kafka本身的设计思想,如果操作本身就成为了你性能的瓶颈,你应该考虑的是集群部署,负载均衡;

4、无锁才是真正的高性能;

评论