源码分析 Kafka 消息发送流程
Futuresend(ProducerRecord<K, V> record)
Futuresend(ProducerRecord<K, V> record, Callback callback)
从上面的 API 可以得知,用户在使用 KafkaProducer 发送消息时,首先需要将待发送的消息封装成 ProducerRecord,返回的是一个 Future 对象,典型的 Future 设计模式。在发送时也可以指定一个 Callable 接口用来执行消息发送的回调。
我们在学习消息发送流程之前先来看一下用于封装一条消息的 ProducerRecord 的类图,先来认识一下 kafka 是如何对一条消息进行抽象的。
1、ProducerRecord 类图
我们首先来看一下 ProducerRecord 的核心属性,即构成 消息的6大核心要素:
String topic
消息所属的主题。
Integer partition
消息所在主题的队列数,可以人为指定,如果指定了 key 的话,会使用 key 的 hashCode 与队列总数进行取模来选择分区,如果前面两者都未指定,则会轮询主题下的所有分区。
Headers headers
该消息的额外属性对,与消息体分开存储.
K key
消息键,如果指定该值,则会使用该值的 hashcode 与 队列数进行取模来选择分区。
V value
消息体。
Long timestamp 消息时间戳,根据 topic 的配置信息 message.timestamp.type 的值来赋予不同的值。
CreateTime
发送客户端发送消息时的时间戳。
LogAppendTime
消息在 broker 追加时的时间戳。
其中Headers是一系列的 key-value 键值对。
在了解 ProducerRecord 后我们开始来探讨 Kafka 的消息发送流程。
2、Kafka 消息追加流程
KafkaProducer 的 send 方法,并不会直接向 broker 发送消息,kafka 将消息发送异步化,即分解成两个步骤,send 方法的职责是将消息追加到内存中(分区的缓存队列中),然后会由专门的 Send 线程异步将缓存中的消息批量发送到 Kafka Broker 中。
消息追加入口为 KafkaProducer#send
publicFuturesend(ProducerRecord<K, V> record, Callback callback){
// intercept the record, which can be potentially modified; this method does not throw exceptions
ProducerRecord<k, v> interceptedRecord =this.interceptors.onSend(record);// @1
returndoSend(interceptedRecord, callback);// @2
}
代码@1:首先执行消息发送拦截器,拦截器通过 interceptor.classes 指定,类型为 List< String >,每一个元素为拦截器的全类路径限定名。
代码@2:执行 doSend 方法,后续我们需要留意一下 Callback 的调用时机。
接下来我们来看 doSend 方法。
2.1 doSend
KafkaProducer#doSend
ClusterAndWaitTime clusterAndWaitTime;
try{
clusterAndWaitTime = waitOnMetadata(record.topic(), record.partition(), maxBlockTimeMs);
}catch(KafkaException e) {
if(metadata.isClosed())
thrownewKafkaException("Producer closed while send in progress", e);
throwe;
}
longremainingWaitMs = Math.max(0, maxBlockTimeMs - clusterAndWaitTime.waitedOnMetadataMs);
Step1:获取 topic 的分区列表,如果本地没有该topic的分区信息,则需要向远端 broker 获取,该方法会返回拉取元数据所耗费的时间。在消息发送时的最大等待时间时会扣除该部分损耗的时间。
温馨提示:本文不打算对该方法进行深入学习,后续会有专门的文章来分析 Kafka 元数据的同步机制,类似于专门介绍 RocketMQ 的 Nameserver 类似。
KafkaProducer#doSend
byte[] serializedKey;
try{
serializedKey = keySerializer.serialize(record.topic(), record.headers(), record.key());
}catch(ClassCastException cce) {
thrownewSerializationException("Can't convert key of class "+ record.key().getClass().getName() +
" to class "+ producerConfig.getClass(ProducerConfig.KEY_SERIALIZER_CLASS_CONFIG).getName() +
" specified in key.serializer", cce);
}
Step2:序列化 key。注意:序列化方法虽然有传入 topic、Headers 这两个属性,但参与序列化的只是 key 。
KafkaProducer#doSend
byte[] serializedValue;
try{
serializedValue = valueSerializer.serialize(record.topic(), record.headers(), record.value());
}catch(ClassCastException cce) {
thrownewSerializationException("Can't convert value of class "+ record.value().getClass().getName() +
" to class "+ producerConfig.getClass(ProducerConfig.VALUE_SERIALIZER_CLASS_CONFIG).getName() +
" specified in value.serializer", cce);
}
Step3:对消息体内容进行序列化。
KafkaProducer#doSend
intpartition = partition(record, serializedKey, serializedValue, cluster);
tp =newTopicPartition(record.topic(), partition);
Step4:根据分区负载算法计算本次消息发送该发往的分区。其默认实现类为 DefaultPartitioner,路由算法如下:
如果指定了 key ,则使用 key 的 hashcode 与分区数取模。
如果未指定 key,则轮询所有的分区。
KafkaProducer#doSend
setReadOnly(record.headers());
Header[] headers = record.headers().toArray();
Step5:如果是消息头信息(RecordHeaders),则设置为只读。
KafkaProducer#doSend
intserializedSize = AbstractRecords.estimateSizeInBytesUpperBound(apiVersions.maxUsableProduceMagic(),
compressionType, serializedKey, serializedValue, headers);
ensureValidRecordSize(serializedSize);
Step5:根据使用的版本号,按照消息协议来计算消息的长度,并是否超过指定长度,如果超过则抛出异常。
KafkaProducer#doSend
longtimestamp = record.timestamp() ==null? time.milliseconds() : record.timestamp();
log.trace("Sending record {} with callback {} to topic {} partition {}", record, callback, record.topic(), partition);
Callback interceptCallback =newInterceptorCallback<>(callback,this.interceptors, tp);
Step6:先初始化消息时间戳,并对传入的 Callable(回调函数) 加入到拦截器链中。
KafkaProducer#doSend
if(transactionManager !=null&& transactionManager.isTransactional())
transactionManager.maybeAddPartitionToTransaction(tp);
Step7:如果事务处理器不为空,执行事务管理相关的,本节不考虑事务消息相关的实现细节,后续估计会有对应的文章进行解析。
KafkaProducer#doSend
RecordAccumulator.RecordAppendResult result = accumulator.append(tp, timestamp, serializedKey, serializedValue, headers, interceptCallback, remainingWaitMs);
if(result.batchIsFull || result.newBatchCreated) {
log.trace("Waking up the sender since topic {} partition {} is either full or getting a new batch", record.topic(), partition);
this.sender.wakeup();
}
returnresult.future;
Step8:将消息追加到缓存区,这将是本文重点需要探讨的。如果当前缓存区已写满或创建了一个新的缓存区,则唤醒 Sender(消息发送线程),将缓存区中的消息发送到 broker 服务器,最终返回 future。这里是经典的 Future 设计模式,从这里也能得知,doSend 方法执行完成后,此时消息还不一定成功发送到 broker。
KafkaProducer#doSend
}catch(ApiException e) {
log.debug("Exception occurred during message send:", e);
if(callback !=null)
callback.onCompletion(null, e);
this.errors.record();
this.interceptors.onSendError(record, tp, e);
returnnew FutureFailure(e);
}catch(InterruptedException e) {
this.errors.record();
this.interceptors.onSendError(record, tp, e);
thrownew InterruptException(e);
}catch(BufferExhaustedException e) {
this.errors.record();
this.metrics.sensor("buffer-exhausted-records").record();
this.interceptors.onSendError(record, tp, e);
throwe;
}catch(KafkaException e) {
this.errors.record();
this.interceptors.onSendError(record, tp, e);
throwe;
}catch(Exception e) {
// we notify interceptor about all exceptions, since onSend is called before anything else in this method
this.interceptors.onSendError(record, tp, e);
throwe;
}
Step9:针对各种异常,进行相关信息的收集。
接下来将重点介绍如何将消息追加到生产者的发送缓存区,其实现类为:RecordAccumulator。
2.2 RecordAccumulator append 方法详解
RecordAccumulator#append
publicRecordAppendResultappend(TopicPartition tp,
longtimestamp,
byte[] key,
byte[] value,
Header[] headers,
Callback callback,
longmaxTimeToBlock)
throwsInterruptedException{
在介绍该方法之前,我们首先来看一下该方法的参数。
TopicPartition tp
topic 与分区信息,即发送到哪个 topic 的那个分区。
long timestamp
客户端发送时的时间戳。
byte[] key
消息的 key。
byte[] value
消息体。
Header[] headers
消息头,可以理解为额外消息属性。
Callback callback
回调方法。
long maxTimeToBlock
消息追加超时时间。
RecordAccumulator#append
Deque dq = getOrCreateDeque(tp);
synchronized(dq) {
if(closed)
thrownewKafkaException("Producer closed while send in progress");
RecordAppendResult appendResult = tryAppend(timestamp, key, value, headers, callback, dq);
if(appendResult !=null)
returnappendResult;
}
Step1:尝试根据 topic与分区在 kafka 中获取一个双端队列,如果不存在,则创建一个,然后调用 tryAppend 方法将消息追加到缓存中。Kafka 会为每一个 topic 的每一个分区创建一个消息缓存区,消息先追加到缓存中,然后消息发送 API 立即返回,然后由单独的线程 Sender 将缓存区中的消息定时发送到 broker 。这里的缓存区的实现使用的是 ArrayQeque。然后调用 tryAppend 方法尝试将消息追加到其缓存区,如果追加成功,则返回结果。
在讲解下一个流程之前,我们先来看一下 Kafka 双端队列的存储结构:
RecordAccumulator#append
intsize = Math.max(this.batchSize, AbstractRecords.estimateSizeInBytesUpperBound(maxUsableMagic, compression, key, value, headers));
log.trace("Allocating a new {} byte message buffer for topic {} partition {}", size, tp.topic(), tp.partition());
buffer = free.allocate(size, maxTimeToBlock);
Step2:如果第一步未追加成功,说明当前没有可用的 ProducerBatch,则需要创建一个 ProducerBatch,故先从 BufferPool 中申请 batch.size 的内存空间,为创建 ProducerBatch 做准备,如果由于 BufferPool 中未有剩余内存,则最多等待 maxTimeToBlock ,如果在指定时间内未申请到内存,则抛出异常。
RecordAccumulator#append
synchronized(dq) {
// Need to check if producer is closed again after grabbing the dequeue lock.
if(closed)
thrownewKafkaException("Producer closed while send in progress");
// 省略部分代码
MemoryRecordsBuilder recordsBuilder = recordsBuilder(buffer, maxUsableMagic);
ProducerBatch batch =newProducerBatch(tp, recordsBuilder, time.milliseconds());
FutureRecordMetadata future = Utils.notNull(batch.tryAppend(timestamp, key, value, headers, callback, time.milliseconds()));
dq.addLast(batch);
incomplete.add(batch);
// Don't deallocate this buffer in the finally block as it's being used in the record batch
buffer =null;
returnnewRecordAppendResult(future, dq.size() >1|| batch.isFull(),true);
}
Step3:创建一个新的批次 ProducerBatch,并将消息写入到该批次中,并返回追加结果,这里有如下几个关键点:
创建 ProducerBatch ,其内部持有一个 MemoryRecordsBuilder对象,该对象负责将消息写入到内存中,即写入到 ProducerBatch 内部持有的内存,大小等于 batch.size。
将消息追加到 ProducerBatch 中。
将新创建的 ProducerBatch 添加到双端队列的末尾。
将该批次加入到 incomplete 容器中,该容器存放未完成发送到 broker 服务器中的消息批次,当 Sender 线程将消息发送到 broker 服务端后,会将其移除并释放所占内存。
返回追加结果。
纵观 RecordAccumulator append 的流程,基本上就是从双端队列获取一个未填充完毕的 ProducerBatch(消息批次),然后尝试将其写入到该批次中(缓存、内存中),如果追加失败,则尝试创建一个新的 ProducerBatch 然后继续追加。
接下来我们继续探究如何向 ProducerBatch 中写入消息。
2.3 ProducerBatch tryAppend方法详解
ProducerBatch #tryAppend
publicFutureRecordMetadatatryAppend(longtimestamp,byte[] key,byte[] value, Header[] headers, Callback callback,longnow){
if(!recordsBuilder.hasRoomFor(timestamp, key, value, headers)) {// @1
returnnull;
}else{
Long checksum =this.recordsBuilder.append(timestamp, key, value, headers);// @2
this.maxRecordSize = Math.max(this.maxRecordSize, AbstractRecords.estimateSizeInBytesUpperBound(magic(),
recordsBuilder.compressionType(), key, value, headers));// @3
this.lastAppendTime = now;//
FutureRecordMetadata future =newFutureRecordMetadata(this.produceFuture,this.recordCount,
timestamp, checksum,
key ==null? -1: key.length,
value ==null? -1: value.length,
Time.SYSTEM);// @4
// we have to keep every future returned to the users in case the batch needs to be
// split to several new batches and resent.
thunks.add(newThunk(callback, future));// @5
this.recordCount++;
returnfuture;
}
}
代码@1:首先判断 ProducerBatch 是否还能容纳当前消息,如果剩余内存不足,将直接返回 null。如果返回 null ,会尝试再创建一个新的ProducerBatch。
代码@2:通过 MemoryRecordsBuilder 将消息写入按照 Kafka 消息格式写入到内存中,即写入到 在创建 ProducerBatch 时申请的 ByteBuffer 中。本文先不详细介绍 Kafka 各个版本的消息格式,后续会专门写一篇文章介绍 Kafka 各个版本的消息格式。
代码@3:更新 ProducerBatch 的 maxRecordSize、lastAppendTime 属性,分别表示该批次中最大的消息长度与最后一次追加消息的时间。
代码@4:构建 FutureRecordMetadata 对象,这里是典型的 Future模式,里面主要包含了该条消息对应的批次的 produceFuture、消息在该批消息的下标,key 的长度、消息体的长度以及当前的系统时间。
代码@5:将 callback 、本条消息的凭证(Future) 加入到该批次的 thunks 中,该集合存储了 一个批次中所有消息的发送回执。
流程执行到这里,KafkaProducer 的 send 方法就执行完毕了,返回给调用方的就是一个 FutureRecordMetadata 对象。
源码的阅读比较枯燥,接下来用一个流程图简单的阐述一下消息追加的关键要素,重点关注一下各个 Future。
2.4 Kafka 消息追加流程图与总结
上面的消息发送,其实用消息追加来表达更加贴切,因为 Kafka 的 send 方法,并不会直接向 broker 发送消息,而是首先先追加到生产者的内存缓存中,其内存存储结构如下:ConcurrentMap< TopicPartition, Deque< ProducerBatch>> batches,那我们自然而然的可以得知,Kafka 的生产者为会每一个 topic 的每一个 分区单独维护一个队列,即 ArrayDeque,内部存放的元素为 ProducerBatch,即代表一个批次,即 Kafka 消息发送是按批发送的。其缓存结果图如下:
KafkaProducer 的 send 方法最终返回的 FutureRecordMetadata ,是 Future 的子类,即 Future 模式。那 kafka 的消息发送怎么实现异步发送、同步发送的呢?
其实答案也就蕴含在 send 方法的返回值,如果项目方需要使用同步发送的方式,只需要拿到 send 方法的返回结果后,调用其 get() 方法,此时如果消息还未发送到 Broker 上,该方法会被阻塞,等到 broker 返回消息发送结果后该方法会被唤醒并得到消息发送结果。如果需要异步发送,则建议使用 send(ProducerRecord< K, V > record, Callback callback),但不能调用 get 方法即可。Callback 会在收到 broker 的响应结果后被调用,并且支持拦截器。
消息追加流程就介绍到这里了,消息被追加到缓存区后,什么时候会被发送到 broker 端呢?将在下一篇文章中详细介绍。

源码分析 Kafka 消息发送流程的更多相关文章
- 源码分析 Kafka 消息发送流程(文末附流程图)
温馨提示:本文基于 Kafka 2.2.1 版本.本文主要是以源码的手段一步一步探究消息发送流程,如果对源码不感兴趣,可以直接跳到文末查看消息发送流程图与消息发送本地缓存存储结构. 从上文 初识 Ka ...
- 源码分析Kafka 消息拉取流程
目录 1.KafkaConsumer poll 详解 2.Fetcher 类详解 本节重点讨论 Kafka 的消息拉起流程. @(本节目录) 1.KafkaConsumer poll 详解 消息拉起主 ...
- 源码分析RocketMQ消息轨迹
目录 1.发送消息轨迹流程 1.1 DefaultMQProducer构造函数 1.2 SendMessageTraceHookImpl钩子函数 1.3 TraceDispatcher实现原理 2. ...
- Solr4.8.0源码分析(5)之查询流程分析总述
Solr4.8.0源码分析(5)之查询流程分析总述 前面已经写到,solr查询是通过http发送命令,solr servlet接受并进行处理.所以solr的查询流程从SolrDispatchsFilt ...
- HDFS源码分析DataXceiver之整体流程
在<HDFS源码分析之DataXceiverServer>一文中,我们了解到在DataNode中,有一个后台工作的线程DataXceiverServer.它被用于接收来自客户端或其他数据节 ...
- (转)linux内存源码分析 - 内存回收(整体流程)
http://www.cnblogs.com/tolimit/p/5435068.html------------linux内存源码分析 - 内存回收(整体流程) 概述 当linux系统内存压力就大时 ...
- JVM源码分析之JVM启动流程
原创申明:本文由公众号[猿灯塔]原创,转载请说明出处标注 “365篇原创计划”第十四篇. 今天呢!灯塔君跟大家讲: JVM源码分析之JVM启动流程 前言: 执行Java类的main方法,程序就能运 ...
- Yii2 源码分析 入口文件执行流程
Yii2 源码分析 入口文件执行流程 1. 入口文件:web/index.php,第12行.(new yii\web\Application($config)->run()) 入口文件主要做4 ...
- zookeeper源码分析之三客户端发送请求流程
znode 可以被监控,包括这个目录节点中存储的数据的修改,子节点目录的变化等,一旦变化可以通知设置监控的客户端,这个功能是zookeeper对于应用最重要的特性,通过这个特性可以实现的功能包括配置的 ...
随机推荐
- Alpha阶段项目复审(小菜鸡联盟)
Alpha项目复审 小队:小菜鸡联盟 团队名称 项目名称 评价 排名 『S.L.N』 OnTime 优点:团队分工合理明确,每个成员有一定的开发经验,能用到自己较为熟悉的技术进行开发:在开发初期制定了 ...
- Distributional Reinforcement Learning with Quantile Regression
郑重声明:原文参见标题,如有侵权,请联系作者,将会撤销发布! arXiv:1710.10044v1 [cs.AI] 27 Oct 2017 In AAAI Conference on Artifici ...
- 浅析 MVC
MVC(Model–View–Controller) Model:数据模型 负责操作所有数据View:视图 负责所有UI界面Controller:控制器 负责其他 //数据放在m const m = ...
- 开始System.out.println();
第一篇博客 我在逛别人的博客的时候,发现他们有些内容是写他们自己的人生,有些是关于技术方面的个人观点分享探讨,每当看到好的文章的时候,我已经习惯的去点击收藏.有的时候我也在想,我为什么就不能做一个输出 ...
- Trie详解
Trie,又名字典树.单词查找树,可以较高效地实现统计.排序和保存大量的字符串. 顾名思义,Trie是一个树状的结构,按照树型结构来存储字符串,显然是一种以空间换时间的方法.整体上理解和实现都不会很难 ...
- 外链专员怎么做提升自己的seo水平
http://www.wocaoseo.com/thread-281-1-1.html 我是一个外链专员,想提升自身的seo水平该怎么做? 随着SEO的学习,已经有了一段时间,平时也在思考好多事情,现 ...
- 超全Python IDE武器库大总结,优缺点一目了然!
本文介绍了多个 Python IDE,并评价其优缺点.读者可以参考此文列举的 Python IDE 列表,选择适合自己的编辑器. 写 Python 代码最好的方式莫过于使用集成开发环境(IDE)了.它 ...
- Nodejs模块:fs
/** * @description fs模块常用api */ // fs所有的文件操作都是异步IO,如果要以同步的方式去调用,都会加一个在原同步api的基础上加Sync // 同步的方式会在最后传入 ...
- git 如何比较不同分支的差异
前两天,良许在做集成的时候碰到了一件闹心事.事情是这样的,良许的一位同事不小心把一个错误的 dev 分支 merge 到了 master 分支上,导致了良许编译不通过.于是,我们需要将版本回退到 me ...
- Webpack 入门指迷
大概算是一份教程吧, 只不过效果肯定不如视频演示之类的好..Webpack 最近在英文社区上经常看到, 留了心, 但进一步了解是通过下边的视频:视频: How Instagram.com Works, ...