三、Kafka生产者2---发送消息主线程业务逻辑-KafkaProducer#send

概述

本文主要是分享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。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值