【2】生产者

174 阅读10分钟

整体架构

image.png

整个生产者客户端由两个线程协调运行,这两个线程分别为主线程和Sender线程(发送线程)。在主线程中由KafkaProducer创建消息,然后通过可能的拦截器、序列化器和分区器的作用之后缓存到消息累加器(RecordAccumulator,也称为消息收集器)中。Sender 线程负责从RecordAccumulator中获取消息并将其发送到Kafka中。

  • Interceptor: 拦截器,对发送的消息做一些定制化操作

  • Serializer:序列化,网络间数据的传输是通过字节码进行传输,则这里对数据进行序列化处理

  • Partitioner: 分区器,计算消息发到到哪个分区

  • RecordAccumulator:消息收集器,主要用来缓存消息以便 Sender 线程可以批量发送,进而减少网络传输的资源消耗以提升性能,维护一个双向队列,队列中的内容就是ProducerBatch,即 Deque<ProducerBatch>

  • ProducerBatch:指一个消息批次,是由一个或多个ProducerRecord组成,目的是减少网络请求的次数以提升整体的吞吐量。ProducerBatch的大小和batch.size参数也有着密切的关系,当一条消息(ProducerRecord)流入RecordAccumulator时,会先寻找与消息分区所对应的双端队列(如果没有则新建),再从这个双端队列的尾部获取一个 ProducerBatch(如果没有则新建),查看 ProducerBatch 中是否还可以写入这个ProducerRecord,如果可以则写入,如果不可以则需要创建一个新的ProducerBatch。在新建ProducerBatch时评估这条消息的大小是否超过batch.size参数的大小,如果不超过,那么就以 batch.size 参数的大小来创建ProducerBatch,这样在使用完这段内存区域之后,可以通过BufferPool 的管理来进行复用;如果超过,那么就以评估的大小来创建ProducerBatch,这段内存区域不会被复用。

  • Sender:从 RecordAccumulator 中获取缓存的消息之后,会进一步将原本<分区,Deque<ProducerBatch>>的保存形式转变成<Node,List<ProducerBatch>的形式,其中Node表示Kafka集群的broker节点。对于网络连接来说,生产者客户端是与具体的broker节点建立的连接,也就是向具体的 broker节点发送消息,而并不关心消息属于哪一个分区

  • 创建Request:Sender 还会进一步封装,将<Node,List<ProducerBatch>>的形式封装为<Node,Request>的形式

  • InFlightRequests:保存对象的具体形式为 Map<NodeId,Deque<Request>>,它的主要作用是缓存了已经发出去但还没有收到响应的请求(NodeId 是一个String 类型,表示节点的 id 编号)。与此同时,InFlightRequests还提供了许多管理类的方法,并且通过配置参数还可以限制每个连接(也就是客户端与Node之间的连接)最多缓存的请求数。这个配置参数为max.in.flight.requests.per.connection,默认值为 5

  • 元数据:是指Kafka集群的元数据,这些元数据具体记录了集群中有哪些主题,这些主题有哪些分区,每个分区的leader副本分配在哪个节点上,follower副本分配在哪些节点上,哪些副本在AR、ISR等集合中,集群中有哪些节点,控制器节点又是哪一个等信息

功能实现

实现生产者流程

image.png

配置参数初始化

在创建生产者实例之前需要进行必要参数的配置,例如:

  • bootstrap.servers:该参数用来指定生产者客户端连接Kafka集群所需的broker地址清单
  • key.serializer或value.serializer:指定key或value序列化器 等等
代码示例:
public static Properties initConfig(){
    Properties props = new Properties();
    // 配置连接集群地址
    props.put(ProducerConfig.BOOTSTRAP_SERVERS_CONFIG, brokerList);
    // 配置生产消息topic序列号类型
    props.put(ProducerConfig.KEY_SERIALIZER_CLASS_CONFIG, StringSerializer.class.getName());
    // 配置生产消息序列号类型
    props.put(ProducerConfig.VALUE_SERIALIZER_CLASS_CONFIG, StringSerializer.class.getName());
    // 配置客户端ID
    props.put(ProducerConfig.CLIENT_ID_CONFIG, "producer.client.id.demo");
    // 配置重试次数
    props.put(ProducerConfig.RETRIES_CONFIG, 10);
    // 配置自定义分区器
    props.put(ProducerConfig.PARTITIONER_CLASS_CONFIG, CustomPartitioner.class.getName());
    // 配置自定义拦截器
    props.put(ProducerConfig.INTERCEPTOR_CLASSES_CONFIG, CustomProducerInterceptor.class.getName());
    return props;
}

备注:生产者客户端配置参数有如下

属性描述默认值
acksacks=1:生产者发送消息之后,只要分区的leader副本成功写入消息,那么它就会收到来自服务端的成功响应。 1是消息可靠性和吞吐量之间的折中方案。acks=0:生产者发送消息之后不需要等待任何服务端的响应,在其他配置环境相同的情况下,acks 设置为 0可以达到最大的吞吐量。acks=-1或acks=all:生产者在消息发送之后,需要等待ISR中的所有副本都成功写入消息之后才能够收到来自服务端的成功响应。在其他配置环境相同的情况下,acks 设置为-1(all)可以达到最强的可靠性。
max.request.size这个参数用来限制生产者客户端能发送的消息的最大值1048576B,即1MB
retries用来配置生产者重试的次数0
retry.backoff.ms它用来设定两次重试之间的时间间隔,避免无效的频繁重试100
compression.type这个参数用来指定消息的压缩方式,默认值为“none”,即默认情况下,消息不会被压缩。该参数还可以配置为“gzip”“snappy”和“lz4”。对消息进行压缩可以极大地减少网络传输量、降低网络I/O,从而提高整体的性能。消息压缩是一种使用时间换空间的优化方式,如果对时延有一定的要求,则不推荐对消息进行压缩none
connections.max.idle.ms这个参数用来指定在多久之后关闭限制的连接540000(ms),即9分钟
linger.ms这个参数用来指定生产者发送 ProducerBatch 之前等待更多消息(ProducerRecord)加入ProducerBatch 的时间,默认值为 0。生产者客户端会在 ProducerBatch 被填满或等待时间超过linger.ms 值时发送出去。增大这个参数的值会增加消息的延迟,但是同时能提升一定的吞吐量。这个linger.ms参数与TCP协议中的Nagle算法有异曲同工之妙。0
receive.buffer.bytes这个参数用来设置Socket接收消息缓冲区(SO_RECBUF)的大小,默认值为32768(B),即32KB。如果设置为-1,则使用操作系统的默认值。如果Producer与Kafka处于不同的机房,则可以适地调大这个参数值32768(B),即32KB
send.buffer.bytes这个参数用来设置Socket发送消息缓冲区(SO_SNDBUF)的大小,默认值为131072(B),即128KB。与receive.buffer.bytes参数一样,如果设置为-1,则使用操作系统的默认值。131072(B),即128KB
request.timeout.ms这个参数用来配置Producer等待请求响应的最长时间,默认值为30000(ms)。请求超时之后可以选择进行重试。注意这个参数需要比broker端参数replica.lag.time.max.ms的值要大,这样可以减少因客户端重试而引起的消息重复的概率。30000(ms)
bootstrap.servers指定kafka集群所需的broker地址清单
key.serializer消息中key对应的序列化类,需要实现org.apache.kafka.common.serialization.Serializer接口
value.serializer消息中value对应的序列化类,需要实现 org.apache.kafka.common.serialization.Serializer接口
buffer.memory生产者客户端中用于缓存消息的缓冲区大小33554432(32M)
batch.size用于指定ProducerBatch 可以复用内存区域的大小16384(16KB)
client.id用于设定KafkaProducer 对应的客户端id6000
max.block.ms用来控制KafkaProducer中send()方法和paritionsFor()方法的阻塞时间。当生产者的发送缓冲区已经满,或者没有可用的元数据时,这些方法就会被阻塞
partitioner.class用来指定分区器,需要实现org.apache.kafka.clients.producer.Partitioner接口
enable.idempotence是否开启幂等功能false
interceptor.classes用于设定生产者拦截器,需要实现org.apache.kafka.clients.producer.ProducerInterceptor接口
max.in.flight.requests.per.connection限制每个链接(也就是客户端与Node之间的连接)最多缓存的请求数5
metadata.max.age.ms如果在这个时间内元数据没有更新的话会被强制更新300000(5分钟)
transactional.id设置事务id,必须唯一

自定义序列化

生产者需要用序列化器(Serializer)把对象转换成字节数组才能通过网络发送给Kafka.

  • 自带序列化:org.apache.kafka.common.serialization.StringSerializer

  • 自定义序列化:可以通过实现org.apache.kafka.common.serialization.Serializer接口进行自定义。建议使用json来进行序列化

    代码示例:
      public class CustomSerializer implements Serializer {
      // 用来配置当前类
      public void configure(Map configs, boolean isKey) {
    
      }
    
      // 执行序列化操作
      public byte[] serialize(String topic, Object data) {
          return new byte[0];
      }
    
      // 执行序列化操作
      public byte[] serialize(String topic, Headers headers, Object data) {
          return new byte[0];
      }
    
      // 关闭处理
      public void close() {
    
      }
    

自定义分区器

分区器的作用是进行消息的分配分区,如果消息的ProducerRecord中没有指定partition字段,那么就需要依赖分区器,根据key这个字段来计算partition的值。

  • 默认分区器:org.apache.kafka.clients.producer.internals.DefaultPartitioner,分区逻辑,如果 key 不为 null,那么计算得到的分区号会是所有分区中的任意一个;如果 key为null,那么计算得到的分区号仅为可用分区中的任意一个,注意两者之间的差别。

  • 自定义分区器:通过实现 org.apache.kafka.clients.producer.Partitioner接口进行自定义。

      public class CustomPartitioner implements Partitioner {
      // 计算分区号
      public int partition(String topic, Object key, byte[] keyBytes, Object value, byte[] valueBytes, Cluster cluster) {
          return 0;
      }
    
      // 关闭分区器的时候用来回收一些资源
      public void close() {
    
      }
    
      // 用来获取配置信息及初始化数据
      public void configure(Map<String, ?> configs) {
    
      }
      }
    

自定义拦截器

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

拦截器执行顺序:

根据配置的拦截器先后顺序一一执行,排在前面的拦截器失败了,不会影响后续拦截器的执行

例如:

// 配置拦截器
props.put(ProducerConfig.INTERCEPTOR_CLASSES_CONFIG, Custom01.class.getName() + "," + Custom02.class.getName());

从上面例子可以看到,配置了Custom01和Custom02的拦截器,则优先执行Custom01,然后再执行Custom02

自定义拦截器:

public class CustomProducerInterceptor implements ProducerInterceptor {
    // 消息发送时调用,对消息进行定制化操作
    public ProducerRecord onSend(ProducerRecord record) {
        return null;
    }

    // 消息被应道之前或消息发送失败时调用,优先与用于自定义callback函数,运行在IO线程中,尽量避免大量操作
    public void onAcknowledgement(RecordMetadata metadata, Exception exception) {

    }

    // 用于关闭拦截器执行一些资源的清理工作
    public void close() {

    }

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

    }
}

创建KafkaProducer实例

KafkaProducer提供的构造函数有如下,最常用的构建函数为public KafkaProducer(Properties properties)

KafkaProducer 构建函数
public KafkaProducer(Properties properties) 
public KafkaProducer(Map<String, Object> configs, Serializer<K> keySerializer, Serializer<V> valueSerializer)
public KafkaProducer(final Map<String, Object> configs)

例如:

Properties props = initConfig(); //这里initConifg函数为上面配置初始化函数
KafkaProducer<String, String> producer = new KafkaProducer<String, String>(props);

生产ProducerRecord消息

将要发送的消息,封装为一个ProducerRecord对象。ProducerRecord提供的构建方法有如下:

// Creates a record with a specified timestamp to be sent to a specified topic and partition
public ProducerRecord(String topic, Integer partition, Long timestamp, K key, V value, Iterable<Header> headers)

// Creates a record with a specified timestamp to be sent to a specified topic and partition
public ProducerRecord(String topic, Integer partition, Long timestamp, K key, V value)

// Creates a record to be sent to a specified topic and partition
public ProducerRecord(String topic, Integer partition, K key, V value, Iterable<Header> headers)

// Creates a record to be sent to a specified topic and partition
public ProducerRecord(String topic, Integer partition, K key, V value)

// Create a record to be sent to Kafka
public ProducerRecord(String topic, K key, V value)

示例:

ProducerRecord<String, String> record = new ProducerRecord<String, String>("test-topic", "hello, kafka");

消息发送

KafkaProducer提供send方法用于将消息发送到kafka中

消息发送模式:

  • 同步(sync)
  • 异步(async)

KafkaProducer提供消息发送的方法:

// 同步发送消息
public Future<RecordMetadata> send(ProducerRecord<K, V> record)
// 异步发送消息
public Future<RecordMetadata> send(ProducerRecord<K, V> record, Callback callback)

同步发送消息:

KafkaProducer<String, String> producer = new KafkaProducer<String, String>(props);
ProducerRecord<String, String> record = new ProducerRecord<String, String>("test-topic", "hello, kafka");
// 同步发送方式
producer.send(record).get();

异步发送消息:

KafkaProducer<String, String> producer = new KafkaProducer<String, String>(props);
ProducerRecord<String, String> record = new ProducerRecord<String, String>("test-topic", "hello, kafka");
// 异步发送消息
producer.send(record, new Callback() {
    public void onCompletion(RecordMetadata metadata, Exception exception) {
    }
});

关闭 KafkaProducer 实例

KafkaProducer提供close方法,进行资源的释放

KafkaProducer<String, String> producer = new KafkaProducer<String, String>(props);
ProducerRecord<String, String> record = new ProducerRecord<String, String>("test-topic", "hello, kafka");
producer.send(record, new Callback() {
    public void onCompletion(RecordMetadata metadata, Exception exception) {
    }
});
// 关闭
producer.close();