目录
一、前言
本节主要是对kafka发送消息流程的源码分析,如果想了解kafka的基本原理,可以参考另外一篇文章:kafka从入门到不放弃_fish_tao-CSDN博客
下图是kafka发送消息的简单程序
//生产者客户端的基本配置
Properties props = new Properties();
props.put(ProducerConfig.BOOTSTRAP_SERVERS_CONFIG, "localhost:9092");
props.put(ProducerConfig.KEY_SERIALIZER_CLASS_CONFIG, StringSerializer.class.getName());
props.put(ProducerConfig.VALUE_SERIALIZER_CLASS_CONFIG, StringSerializer.class.getName());
props.put(ProducerConfig.MAX_BLOCK_MS_CONFIG, 1000 * 60 * 60);
props.put(ProducerConfig.ACKS_CONFIG, "1");
KafkaProducer<String, String> producer = new KafkaProducer<>(props);
String data = "hello, i am kafka_producer";
ProducerRecord<String, String> record = new ProducerRecord<>(KafkaContants.KAFKA_TOPIC, data);
//发送消息
producer.send(record);
//关闭生产者
producer.close();
ProducerRecord作为send()方法的参数,该类包含以下几个参数,其中topic和value是必传的,其他字段可以自行选择。
private final String topic; //主题
private final Integer partition; //分区数
private final Headers headers; //头部信息
private final K key; //消息的key值
private final V value; //具体消息内容
private final Long timestamp; //发送时间戳
kafka发送过程中,主要有两个线程负责,一个是应用程序的主线程通过调用send()方法,将消息写入到RecordAccumulator中,内部是一个ConcurrentMap,然后由sender线程轮询RecordAccumulator中的消息,如果达到要求,就会将该消息发送给kakfa集群。
在对KafkaProducer实例化的时候会初始化一些参数,比如实例化自定义的拦截器、解析序列化方式、实例化分区器、RecordAccumulator对象,创建Sender线程并且启动。
二、ProducerRecord分析
1、send()的入口,返回值为异步Future,我们可以通过get()方法进行阻塞,直到发送成功后。还有个参数为Callback,如果选择get()阻塞会对我们的应用产生影响,所以我们可以通过自定义Callback,在回调中对发送失败的进行额外处理。
2、发送消息首先执行拦截器,kafka默认没有实现拦截器,需要我们根据业务场景自己定义。
3、doSend()方法包括了将消息发送到 RecordAccumulator中的整个流程(图中的红色序号和下面的序号一一对应):
1)首先判断Sender线程是否在并且运行中,如果非直接抛异常;
2)获取kafka集群元数据,如果本地已经有集群数据,直接从缓存中取,反之,通知Sender线程获取元数据,并且对应用传入的partition进行校验;
3)对传入的key值进行序列化
4)对发送消息的value值进行序列化
5)如果发送消息时指定partition,不会执行分区器,没有指定会通过分区器选择出消息将要发送到哪个partition,kafka2.4版本后采用StickyPartitioning Strategy(粘性分区策略),如果往相同topic写数据,是同一批消息会指定相同的partition。
6)估算发送消息的大小,如果超过maxRequestSize或者totalMemorySize会提示错误,这两个参数可以通过max.request.size和buffer.memory进行配置。
7)实例化回调函数和拦截器
然后执行accumulator.append()方法,将消息写入到内存中,下面具体分析append()方法
1)RecordAccumulator中保存的是一个ConcurrentMap,key为TopicPartition,即topic+partition,value是一个双端队列Deque<ProducerBatch>,首先判断map中是否有值,如果有直接返回value,如果没有初始化一个ArrayDeque(),写入到map中,然后返回;
2)进入tryAppend()方法中,获取链表最后一个node,如果没有则返回null;
3)执行append()方法时,传入的参数abortOnNewBatch为true,所以会执行第三步,new一个RecordAppendResult对象直接返回,且设置参数abortForNewBatch为true。
前面讲到在doSend()方法,执行append()方法时,返回的result.abortForNewBatch为true,此时说明还未将消息写入到内存中,进入第二步if循环体中,再次执行append()方法,判断map中是否有值,前面已经实例化一个ArrayDeque,但是该双端列表为null,此次传入的abortOnNewBatch为false,所以会执行下图的(1);
1)为ProducerBatch分配内存空间;
2) 再次调用tryAppend方法写入到内存中,如果写成功了直接返回,如果写失败了会执行(3)创建ProducerBatch然后调用ProducerBatch.tryAppend()方法;
注意:正常流程执行(1)的时候说明前面返回的PrudecerBatch为bull,为啥还要再重复执行(2)呢?因为(1)分配内存不是线程同步,所以有可能有其他的线程跟当前线程相同的TopicPartition,且已经再RecordAccumulator中写入了数据,这时如果在执行(2)的时候有可能追加成功。
ProducerBatch.tryAppend()方法会校验该ProducerBatch中是否已满,如果已满则返回null,重新实例化ProducerBatch,如果没满则追加到ProducerBatch中,最后释放分配的内存成功返回。
doSend()整个流程图如下
三、Sender发送线程
3.1 Sender
Sender是一个实现Runnable的线程,在应用调用send()方法的时会启动。
1)正常情况下 ,会循环执行while里面的runOnce()方法;
2)如果Sender线程关闭,防止内存中的数据丢失,在forceClose(默认为false)为false的情况下,将内存中的数据发送给kafka。只有当客户端KafkaProducer执行close()方法时会更新forceClose为true;
3)将未发送的消息清空,数据丢失;
4)关闭KafkaClient网络通信;
runOnce()方法中实现数据发送功能。前面都是和Kafka事务相关的工作,我们暂且省略,重点时最后sendProducerData()和client.poll()两个方法;
3.2 sendProducerData
sendProducerData()从内存中获取达到发送要求的数据,写入到kafka channel中
1)获取kafka集群元数据,主要是集群节点信息,最终发送是要确定往哪一个节点上写数据;
2)获取内存中哪些数据达到发送要求,返回对应的节点信息,比如topic-test_0,主题为topic-test,分区为0,返回的是该主题分区为0的leader所在节点信息;
3)从内存中获取需要发送的消息数据,创建发送请求体;
1)首先将要发送的节点信息保存到InFlightRequests中,缓存已经发出但未响应的请求,然后将消息写入到KafkaChannel中,注册write监听事件;
3.3 client.poll
执行KafkaClient.poll()方法,通过Kafka封装的Selector,通过遍历pollSelectionKeys,判断是否达到可写状态,然后执行java中nio发送到kafka集群,完成发送操作。
runOnce()流程图如下所示: