概述
本文主要是分享Kafka生产者send()方法主线程的业务逻辑
1.调用ProducerInterceptors的onSend()方法,对将要发送的消息进行拦截和修改
2.初始化AppendCallbacks,ClusterAndWaitTime,将key,value序列化
3.计算当前消息将要发送的分区
4.将消息头Header的是否只读字段设置为只读表示锁定消息内容,计算消息内容判断内容大小是否超过单次请求最大值(max.request.size)和缓存最大值(buffer.memory)
5.将消息追加到累加器(RecordAccumulator)
一、拦截器
主要方法有:
onSend:在消息发送到服务器之前调用,可用统一于拦截,修改消息内容
onAcknowledgement(RecordMetadata metadata, Exception exception):
消息发送结束后回调该方法,这里的“消息发送结束后”可能是发送到服务器之前客户端报错,有可能是消息成功发送到服务器,有可能是服务器api报错
onSendError(ProducerRecord<K, V> record, TopicPartition interceptTopicPartition, Exception exception):
该方法是在客户端发送消息过程中报错,是对onAcknowledgement方法的包装,最终还是会调onAcknowledgement方法
所以通常实现onSend,onAcknowledgement能解决大部分拦截,通知需求
SelfInterceptor implements ProducerInterceptor<String, String>
二、准备阶段
初始化AppendCallbacks,ClusterAndWaitTime,将key,value序列化
AppendCallbacks:RecordAccumulator回调时使用的回调方法包装对象,可用于用户回调,拦截去回调,分区回调
ClusterAndWaitTime:对topic所对应的集群信息,和发送阻塞时间的包装;集群信息后续会使用,阻塞时间用于后续判断是否唤起sender线程将消息真正推送到服务器
序列化部分:Kafka支持用户通过实现[org.apache.kafka.common.serialization.Serializer]接口来自定义消息key,value的序列化器,也可使用Kafka包[org.apache.kafka.common.serialization]提供的默认序列化器
AppendCallbacks<K, V> appendCallbacks = new AppendCallbacks<K, V>(callback, this.interceptors, record);
...
ClusterAndWaitTime clusterAndWaitTime = waitOnMetadata(record.topic(), record.partition(), nowMs, maxBlockTimeMs);
...
byte[] serializedKey = keySerializer.serialize(record.topic(), record.headers(), record.key());
byte[] serializedValue = valueSerializer.serialize(record.topic(), record.headers(), record.value());
三、计算分区
代码位置:org.apache.kafka.clients.producer.KafkaProducer#partition
该方法用于计算当前消息将要发送给topic的哪一个分区,Kafka支持用户通过实现[org.apache.kafka.clients.producer.Partitioner]接口来自定义分区器。用户可根据自己业务需求通过消息的key,value将消息映射到不同分区,例如将key = task的消息固定发送到分区1中,可实现key = task的消息都在同一分区进而存在有序性的场景(消息有序性不能仅仅依靠Kafka分区有序性机制实现,具体业务需具体分析和实现)。如果用户未指定自定义分区器,则Kafka将会使用内置分区器计算分区[org.apache.kafka.clients.producer.internals.BuiltInPartitioner#partitionForKey]
private int partition(ProducerRecord<K, V> record, byte[] serializedKey, byte[] serializedValue, Cluster cluster) {
if (record.partition() != null)
return record.partition();
if (partitioner != null) {
// 自定义分区计算器
int customPartition = partitioner.partition(
record.topic(), record.key(), serializedKey, record.value(), serializedValue, cluster);
if (customPartition < 0) {
throw new IllegalArgumentException(String.format(
"The partitioner generated an invalid partition number: %d. Partition number should always be non-negative.", customPartition));
}
return customPartition;
}
if (serializedKey != null && !partitionerIgnoreKeys) {
// hash the keyBytes to choose a partition
return BuiltInPartitioner.partitionForKey(serializedKey, cluster.partitionsForTopic(record.topic()).size());
} else {
return RecordMetadata.UNKNOWN_PARTITION;
}
}
四、锁定+大小判断
该断代码用于锁定消息,并评估消息大小是否超过单次请求最大值(max.request.size)和缓存最大值(buffer.memory)
setReadOnly(record.headers());
Header[] headers = record.headers().toArray();
int serializedSize = AbstractRecords.estimateSizeInBytesUpperBound(apiVersions.maxUsableProduceMagic(),
compressionType, serializedKey, serializedValue, headers);
ensureValidRecordSize(serializedSize);
五、将消息追加到累加器(RecordAccumulator)
代码位置:org.apache.kafka.clients.producer.internals.RecordAccumulator#append
简单概括:RecordAccumulator对象类保存有如下缓存,append方法依赖该缓存,找到当前消息将要追加到哪个本地缓存中
[org.apache.kafka.clients.producer.internals.RecordAccumulator#topicInfoMap]
<topic, TopicInfo<partition, Deque<ProducerBatch<MemoryRecordsBuilder>>>
append方法大体逻辑是:
1.通过topic拿到对应的分区信息缓存TopicInfo
TopicInfo topicInfo = topicInfoMap.computeIfAbsent(topic, k -> new TopicInfo(logContext, k, batchSize));
2.通过之前算出来的分区在TopicInfo缓存中找到该分区缓存的双端队列Deque
Deque<ProducerBatch> dq = topicInfo.batches.computeIfAbsent(effectivePartition, k -> new ArrayDeque<>());
3.拿到Deque最后一项ProducerBatch
ProducerBatch last = deque.peekLast();
4.将消息追加到ProducerBatch的MemoryRecordsBuilder中
recordsBuilder.append(timestamp, key, value, headers);
append方法最终会返回一个结果包装对象RecordAppendResult[org.apache.kafka.clients.producer.internals.RecordAccumulator.RecordAppendResult]给到append方法调用处,该对象有三个关键属性会影响到调用append方法后续的执行逻辑:
RecordAppendResult.abortForNewBatch:当消息第一次追加遇到Deque队列满了或者MemoryRecordsBuilder缓存满了的情况,就需要重新找一个缓存队列存放消息;重新计算分区并再走一遍1到4步,即重试,但重试时会将append方法入参字段abortOnNewBatch写死为false,防止无限重试;
RecordAppendResult.batchIsFull:追加的Deque或者MemoryRecordsBuilder是否满了,如果满了,在append方法调用后需要唤起sender线程将满了的缓存消息推送到Kafka服务器
RecordAppendResult.newBatchCreated:是否新的Deque被创建并将消息追加到新的Deque中,如果有,则在append方法调用后需要唤起sender线程将满了的缓存消息推送到Kafka服务器
总结append方法:如果第一次将消息放入缓存失败会重新计算分区重试一次,如果第一次准备将消息存入的缓存满了会唤起sender线程,将已缓存的消息发送给Kafka服务器。1到4步的每一步都还有很多细节也很重要,本文暂不分享
总结:本文简单概述了Kafka生产者send方法的主要逻辑,执行拦截器,将消息序列化,计算分区,将消息缓存到本地,如果缓存满了唤起sender线程将消息发送给Kafka。