kafka 序列化器,分区器,拦截器,消息累加器

目录

拦截器

生产者拦截器

自定义生产者拦截器

 序列化器

 反序列化器

分区器

消息累加器


前提了解:

整个kafka生产者客户端由两条线程协调运行。

这两条线程分别为主线程和sender线程(发送线程)

主线程的作用就是:由KafkaProducer创建消息,然后通过可能的拦截器,序列化器,分区器的作用之后缓存到消息累加器

send线程的作用就是:负责将消息累加器中的消息发送到kafka中。

拦截器

          拦截器是在kafka0.10.0.0版本中就已经引入的一个功能,kafka一共有两种拦截器。生产者拦截器和消费者拦截器。

生产者拦截器

        生产者拦截器可以用来在消息发送前做一些准备工作,比如按照某个规则过滤不符合要求的消息,修改消息的内容等,也可以用来在发送回调逻辑前做一些定制化的需求。

       当然生产者拦截器的使用也很方便,主要是自定义实现org.apache.kafka.clients.producer.ProducerInterceptor接口。

       ProducerInterceptor接口中包含3个方法:

//producer会在消息序列化器,分区器之前调用拦截器的onSend()方法来对消息进行定制化操作。
 public ProducerRecord<K, V> onSend(ProducerRecord<K, V> record);
//producer会在消息被应答之前或者发送失败时调用生产者的 onAcknowledgement()方法
 //不过该方法运行在Producerde I/O线程中,所以方法中的实现代码逻辑越简单越好,否则会影响消息的发送速度
 public void onAcknowledgement(RecordMetadata metadata, Exception exception);
//用于在关闭拦截器时执行一些资源的清理工作
 public void close();

自定义生产者拦截器

     

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

import java.util.Map;

/**
 * 生产者拦截器
 */
public class ProducerInterceptorPrefix implements ProducerInterceptor<String, String> {

    private volatile long sendSuccess = 0;
    private volatile long sendFailure = 0;
    //定制化数据
    @Override
    public ProducerRecord<String, String> onSend(ProducerRecord<String, String> producerRecord) {
        final String mpdofoeddValue = "preFix" + producerRecord.value();

        return new ProducerRecord<String, String>(
                producerRecord.topic(),
                producerRecord.partition(),
                producerRecord.timestamp(),
                producerRecord.key(),
                mpdofoeddValue,
                producerRecord.headers());
    }
    //消息被应答前发送失败时会调用
    @Override
    public void onAcknowledgement(RecordMetadata recordMetadata, Exception e) {
            if(e == null ){
                sendSuccess ++;
            }else{
                sendFailure ++;
            }
    }
    @Override
    public void close() {
        final double successRatio = (double)sendSuccess / (sendSuccess + sendFailure);
        System.out.println("发送成功率 = " + String.format("%f",successRatio* 100) + "%");
    }
    @Override
    public void configure(Map<String, ?> map) { }
}

       实现自定义的ProducerInterceptorPrefix之后,需要在producer的配置参数中指定拦截器。示例:

import org.apache.kafka.clients.producer.KafkaProducer;
import org.apache.kafka.clients.producer.ProducerConfig;
import org.apache.kafka.clients.producer.ProducerRecord;
import org.apache.kafka.common.serialization.StringSerializer;

import java.util.Properties;

/**
 * kafka生产
 */
public class KafkaProducerFastStart {

    public static final String brokerList = "127.0.0.1:9092";
    public static final String topic = "topic-demo";

    public static void main(String args[]) {

        Properties properties = new Properties();
        properties.put(ProducerConfig.BOOTSTRAP_SERVERS_CONFIG, brokerList);    //集群地址
        properties.put(ProducerConfig.BATCH_SIZE_CONFIG, 16384); // 16K   batch.size
        properties.put(ProducerConfig.BUFFER_MEMORY_CONFIG, 33554432); // 32M   buffer.memory
        properties.put(ProducerConfig.KEY_SERIALIZER_CLASS_CONFIG, StringSerializer.class.getName());//KEY序列化方式
        properties.put(ProducerConfig.VALUE_SERIALIZER_CLASS_CONFIG, StringSerializer.class.getName());//value序列化方式
        properties.put(ProducerConfig.ACKS_CONFIG, "1");
        properties.put(ProducerConfig.RETRIES_CONFIG, 3);//retries
        properties.put(ProducerConfig.LINGER_MS_CONFIG, 1);//linger.ms
        properties.put(ProducerConfig.INTERCEPTOR_CLASSES_CONFIG, ProducerInterceptorPrefix.class.getName());//  interceptor.classes  拦截器

        //配置生产者客户端参数并创建KafkaProducer实例
        KafkaProducer<String, String> producer = new KafkaProducer<String, String>(properties);
        //构建需要发送得消息
        ProducerRecord<String, String> data = new ProducerRecord<String, String>(topic, "key", "value");
        try {
            producer.send(data);
        } catch (Exception e) {
            e.printStackTrace();
        } finally {
            producer.close();
        }
    }
}

        当然,拦截器不仅仅可以指定一个。还可以指定多个形成拦截链,拦截链会按照interceptor.classes 参数配置的拦截器的顺序来一一执行。每个拦截器之间用逗号隔开。

properties.put(
      ProducerConfig.INTERCEPTOR_CLASSES_CONFIG, 
      ProducerInterceptorPrefix.class.getName() + "," + ProducerInterceptorPrefix2.class.getName());//  interceptor.classes  拦截器

 序列化器

          生产者需要用序列化器将对象转换成字节数组才能通过网络发送给kafka.当然消费者需要用对应的反序列化器将kafka的字节数组转换为相应的对象。

          kafka客户端自带的序列化器有:

DoubleSerializer ,ByteArraySerializer ,ByteBufferSerializer,

BytesSerializer , IntegerSerializer , LongSerializer ,StringSerializer

       

         当然,如果这几种序列化器都无法满足应用需求,我们可以选择如 Avro,JSON,Thrift,Protobuf等,或者使用自定义类型的序列化器来实现。

        图中 Company 为自定义的对象。

       

import org.apache.kafka.common.serialization.Serializer;

import java.io.UnsupportedEncodingException;
import java.nio.ByteBuffer;
import java.util.Map;

/**
生产端序列化器
 */
public class CustomizeSerializer implements Serializer<Company> {
    @Override
    public void configure(Map<String, ?> map, boolean b) { }
    @Override
    public byte[] serialize(String topic, Company company) {

        if (company == null) {
            return null;
        }
        byte[] name, address;
        try {
            if (company.getName() != null) {
                name = company.getName().getBytes("UTF-8");
            } else {
                name = new byte[0];
            }
            if (company.getAddress() != null) {
                address = company.getAddress().getBytes("UTF-8");
            } else {
                address = new byte[0];
            }
            ByteBuffer buffer = ByteBuffer.allocate(4 + 4 + name.length + address.length);
            buffer.putInt(name.length);
            buffer.put(name);
            buffer.putInt(address.length);
            buffer.put(address);
            return buffer.array();
        } catch (UnsupportedEncodingException e) {
            e.printStackTrace();
        }
        return new byte[0];
    }
    @Override
    public void close() {
    }
}

       kafkaProducer配置

    

properties.put(ProducerConfig.VALUE_SERIALIZER_CLASS_CONFIG,CustomizeSerializer.class.getName());

 反序列化器

     

import org.apache.kafka.common.errors.SerializationException;
import org.apache.kafka.common.serialization.Deserializer;

import java.io.UnsupportedEncodingException;
import java.nio.ByteBuffer;
import java.util.Map;

/**
 * 反序列化器
 */
public class CustomizeDeserializer implements Deserializer<Company> {
    @Override
    public void configure(Map<String, ?> map, boolean b) { }

    @Override
    public Company deserialize(String topic, byte[] data) {
        if(data == null ){
            return null;
        }
        if(data.length < 8 ){
            throw new SerializationException("Size of data received  by DemoDeserializer is shorter than expected !");
        }
         ByteBuffer buffer = ByteBuffer.wrap(data);
        int nameLen,addressLen;
        String name, address;

        nameLen = buffer.getInt();
        final byte[] nameBytes = new byte[nameLen];
        buffer.get(nameBytes);

        addressLen = buffer.getInt();
        byte[] addressBytes = new byte[addressLen];
        buffer.get(addressBytes);

        try{
            name = new String(nameBytes,"UTF-8");
            address = new String(addressBytes,"UTF-8");
        }catch (UnsupportedEncodingException e){
            throw new SerializationException("Error occur when deserializing");
        }
        return new Company(name,address);
    }
    @Override
    public void close() { }
}

          消费者指定反序列化器

        


properties.put(ConsumerConfig.VALUE_DESERIALIZER_CLASS_CONFIG,CustomizeDeserializer.class.getName());

分区器

         消息通过send()方法发送broker的过程中,有可能会经过拦截器,序列化器,之后,就会需要确定消息要发往的分区。如果ProducerRecord中指定了partition字段,那么就不需要分区器的作用。因为partition代表的就是索要发往的分区号。

         kafka提供的默认分区器是org.apache.kafka.clients.producer.internals.DefaultPartitioner.它实现了org.apache.kafka.clients.producer.Partitioner接口,这个接口定义了2个方法:

    

//用来计算分区号。
 @params topic 分区
 @params key  消息的key
 @params keyBytes 序列化后的消息key
 @params value 消息值
 @params valueBytes 序列化后的值
 @params cluster 集群的元数据信息
 public int partition(String topic, Object key, byte[] keyBytes, Object value, byte[] valueBytes, Cluster cluster);
 
 public void close();

当然Pratitioner接口还有一个父接口:org.apache.kafka.common.Configurable。该接口中只有一个方法:

//用来获取配置信息及初始化数据
  void configure(Map<String, ?> configs);

接下来我们看下默认分区器中的实现

public int partition(String topic, Object key, byte[] keyBytes, Object value, byte[] valueBytes, Cluster cluster) {
        List<PartitionInfo> partitions = cluster.partitionsForTopic(topic);
        int numPartitions = partitions.size();
        if (keyBytes == null) {
            int nextValue = nextValue(topic);
            List<PartitionInfo> availablePartitions = cluster.availablePartitionsForTopic(topic);
            if (availablePartitions.size() > 0) {
                int part = Utils.toPositive(nextValue) % availablePartitions.size();
                return availablePartitions.get(part).partition();
            } else {
                // no partitions are available, give a non-available partition
                return Utils.toPositive(nextValue) % numPartitions;
            }
        } else {
            // hash the keyBytes to choose a partition
            return Utils.toPositive(Utils.murmur2(keyBytes)) % numPartitions;
        }
    }

可以看到:如果key不为null,那么默认分区器会对key进行哈希(采用MurmurHash2算法)得到的哈希值来计算分区号。

接下来我们可以自定义我们自己的分区器

import org.apache.kafka.clients.producer.Partitioner;
import org.apache.kafka.common.Cluster;
import org.apache.kafka.common.PartitionInfo;
import org.apache.kafka.common.utils.Utils;

import java.util.List;
import java.util.Map;
import java.util.concurrent.atomic.AtomicInteger;

/**
 * 分区器
 */
public class Dempartitioner implements Partitioner {

    private final AtomicInteger counter = new AtomicInteger(0); //原子操作类AtomicInteger
    @Override
    public int partition(String topic, Object key, byte[] keyBytes, Object valye, byte[] valueBytes, Cluster cluster) {
         List<PartitionInfo> partitionInfos = cluster.partitionsForTopic(topic);
         int numPartitions = partitionInfos.size();
        if(null == keyBytes){
            return counter.getAndIncrement() % numPartitions;
        }else{
            return Utils.toPositive(Utils.murmur2(keyBytes)) % numPartitions;
        }
    }
    @Override
    public void close() {}
    @Override
    public void configure(Map<String, ?> map) { }
}

指定分区器的配置

properties.put(ProducerConfig.PARTITIONER_CLASS_CONFIG,Dempartitioner.class.getName());//partitioner.class

消息累加器

    RecordAccumulator也叫消息累加器,主要用来缓存消息以便Sender线程可以批量发送,进而减少网络传输的资源消耗来提升性能。 是在客户端开辟出的一块内存区域。

 this.accumulator = new RecordAccumulator(logContext, 
                                         config.getInt("batch.size"),
                                         this.totalMemorySize,
                                         this.compressionType, 
                                         config.getLong("linger.ms"),
                                         retryBackoffMs, 
                                         this.metrics, 
                                         this.time,
                                         this.apiVersions, 
                                         this.transactionManager);

参数buffer.memory,默认为33554432B,及32MB. 指定RecordAccumulator缓存的大小

如果生产者发送消息的速度超过发送到服务器的速度,则会导致生产者空间不足。这个时候producer的send方法要么被阻塞,要么抛出异常。这个取决于参数max.block.ms的配置,此参数默认值为60000,及60秒。

 RecordAccumulator的内部为每个分区都维护了一个双端队列。队列中的具体内容就是ProducerBatch(消息批次).即Deque<ProducerBatch>.

通俗来讲,ProducerBatch为一个消息批次,可以将较小的ProducerRecord拼凑成一个较大的ProducerBatch.来减少网络请求次数提高吞吐量。

            当然ProducerRecord即为我们的消息。

每次有消息要写入到累加器中的时候,会先去寻找对应的双端队列,从队列的尾部获取一个ProducerBatch, 事先会判断消息的大小,如果大小在被写入的Producerbatch批次范围内,则写入。如果没找到ProducerBatch或者消息大小大于被写入的批次范围无法写入的时候,就会新建一个ProducerBatch,根据参数batch.size来创建batch大小。如果消息大于batch.size的大小,就以评估的大小创建ProducerBatch.

消息写入缓存时,追加到双端队列的尾部Sender线程读取消息时,从双端队列的头部读取。注意ProducerBatch中可以包含很多个ProducerRecord(消息)。

 在RecordAccumulator的内部还有一个BufferPool,主要来实现ButeBuffer的复用。不过BufferPool只针对特定大小的ByteBuffer进行管理。超过大小是不会进入BufferPool的。可以通过参数batch.size来指定。默认为16384B.

         当每条消息流入消息累加器,会先寻找每个分区中的ProducerBatch双端队列。如果找不到,会判断消息的大小,如果小于batch.size。则用batch.size的大小来创建ProducerBatch.

sender线程会从RecordAccumulator中获取缓存的消息之后,进一步将消息封装成<Node,Request>的形式,通过KafkaClient进行IO操作。

猜你喜欢

转载自blog.csdn.net/weixin_40954192/article/details/107313662
今日推荐