源码分析 Kafka 消息发送流程(文末附流程图)

  • 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

public Future send(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

return doSend(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())

throw new KafkaException(“Producer closed while send in progress”, e);

throw e;

}

long remainingWaitMs = 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) {

throw new SerializationException("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) {

throw new SerializationException("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

int partition = partition(record, serializedKey, serializedValue, cluster);

tp = new TopicPartition(record.topic(), partition);

Step4:根据分区负载算法计算本次消息发送该发往的分区。其默认实现类为 DefaultPartitioner,路由算法如下:

  • 如果指定了 key ,则使用 key 的 hashcode 与分区数取模。

  • 如果未指定 key,则轮询所有的分区。

KafkaProducer#doSend

setReadOnly(record.headers());

Header[] headers = record.headers().toArray();

Step5:如果是消息头信息(RecordHeaders),则设置为只读。

KafkaProducer#doSend

int serializedSize = AbstractRecords.estimateSizeInBytesUpperBound(apiVersions.maxUsableProduceMagic(),

compressionType, serializedKey, serializedValue, headers);

ensureValidRecordSize(serializedSize);

Step5:根据使用的版本号,按照消息协议来计算消息的长度,并是否超过指定长度,如果超过则抛出异常。

KafkaProducer#doSend

long timestamp = record.timestamp() == null ? time.milliseconds() : record.timestamp();

log.trace(“Sending record {} with callback {} to topic {} partition {}”, record, callback, record.topic(), partition);

Callback interceptCallback = new InterceptorCallback<>(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();

}

return result.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);

return new FutureFailure(e);

} catch (InterruptedException e) {

this.errors.record();

this.interceptors.onSendError(record, tp, e);

throw new InterruptException(e);

} catch (BufferExhaustedException e) {

this.errors.record();

this.metrics.sensor(“buffer-exhausted-records”).record();

this.interceptors.onSendError(record, tp, e);

throw e;

} catch (KafkaException e) {

this.errors.record();

this.interceptors.onSendError(record, tp, e);

throw e;

} 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);

throw e;

}

Step9:针对各种异常,进行相关信息的收集。

接下来将重点介绍如何将消息追加到生产者的发送缓存区,其实现类为:RecordAccumulator。

2.2 RecordAccumulator append 方法详解

RecordAccumulator#append

public RecordAppendResult append(TopicPartition tp,

long timestamp,

byte[] key,

byte[] value,

Header[] headers,

Callback callback,

long maxTimeToBlock) throws InterruptedException {

在介绍该方法之前,我们首先来看一下该方法的参数。

  • 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)

throw new KafkaException(“Producer closed while send in progress”);

RecordAppendResult appendResult = tryAppend(timestamp, key, value, headers, callback, dq);

if (appendResult != null)

return appendResult;

}

Step1:尝试根据 topic与分区在 kafka 中获取一个双端队列,如果不存在,则创建一个,然后调用 tryAppend 方法将消息追加到缓存中。Kafka 会为每一个 topic 的每一个分区创建一个消息缓存区,消息先追加到缓存中,然后消息发送 API 立即返回,然后由单独的线程 Sender 将缓存区中的消息定时发送到 broker 。这里的缓存区的实现使用的是 ArrayQeque。然后调用 tryAppend 方法尝试将消息追加到其缓存区,如果追加成功,则返回结果。

在讲解下一个流程之前,我们先来看一下 Kafka 双端队列的存储结构:

在这里插入图片描述

RecordAccumulator#append

int size = 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);

自我介绍一下,小编13年上海交大毕业,曾经在小公司待过,也去过华为、OPPO等大厂,18年进入阿里一直到现在。

深知大多数Java工程师,想要提升技能,往往是自己摸索成长或者是报班学习,但对于培训机构动则几千的学费,着实压力不小。自己不成体系的自学效果低效又漫长,而且极易碰到天花板技术停滞不前!

因此收集整理了一份《2024年Java开发全套学习资料》,初衷也很简单,就是希望能够帮助到想自学提升又不知道该从何学起的朋友,同时减轻大家的负担。img

既有适合小白学习的零基础资料,也有适合3年以上经验的小伙伴深入学习提升的进阶课程,基本涵盖了95%以上Java开发知识点,真正体系化!

由于文件比较大,这里只是将部分目录截图出来,每个节点里面都包含大厂面经、学习笔记、源码讲义、实战项目、讲解视频,并且会持续更新!

如果你觉得这些内容对你有帮助,可以扫码获取!!(备注Java获取)

img

面试准备+复习分享:

为了应付面试也刷了很多的面试题与资料,现在就分享给有需要的读者朋友,资料我只截取出来一部分哦

秋招|美团java一面二面HR面面经,分享攒攒人品

《互联网大厂面试真题解析、进阶开发核心学习笔记、全套讲解视频、实战项目源码讲义》点击传送门即可获取!
既有适合小白学习的零基础资料,也有适合3年以上经验的小伙伴深入学习提升的进阶课程,基本涵盖了95%以上Java开发知识点,真正体系化!*

由于文件比较大,这里只是将部分目录截图出来,每个节点里面都包含大厂面经、学习笔记、源码讲义、实战项目、讲解视频,并且会持续更新!

如果你觉得这些内容对你有帮助,可以扫码获取!!(备注Java获取)

img

面试准备+复习分享:

为了应付面试也刷了很多的面试题与资料,现在就分享给有需要的读者朋友,资料我只截取出来一部分哦

[外链图片转存中…(img-B0sB1Bii-1713750505470)]

《互联网大厂面试真题解析、进阶开发核心学习笔记、全套讲解视频、实战项目源码讲义》点击传送门即可获取!

  • 3
    点赞
  • 8
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值