暂无图片
暂无图片
暂无图片
暂无图片
暂无图片

从序列化器,分区器,拦截器,消息累加器到发送,kafka生产者的工作原理原来是这样子

刘狗是狗 2020-02-22
330

前提了解:

整个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();
}
}
}



文中第27行指定了拦截器。


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

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


序列化器:



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

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

DoubleSerializer ,ByteArraySerializer ,ByteBufferSerializer,

BytesSerializer IntegerSerializer , LongSerializerStringSerializer


            kafka生产者指定序列化器:

        

 properties.put(ProducerConfig.KEY_SERIALIZER_CLASS_CONFIG, StringSerializer.class.getName());//KEY序列化方式
 properties.put(ProducerConfig.VALUE_SERIALIZER_CLASS_CONFIG, StringSerializer.class.getName());//value序列化方式

            

               当然,如果这几种序列化器都无法满足应用需求,我们可以选择如 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操作。


        这里有篇关于源码解读的文章:

        https://www.cnblogs.com/dingaimin/p/11523787.html

文章转载自刘狗是狗,如果涉嫌侵权,请发送邮件至:contact@modb.pro进行举报,并提供相关证据,一经查实,墨天轮将立刻删除相关内容。

评论