
【生产者源码分析系列第八篇】图解 Kafka 源码之 Sender 线程架构设计
阅读本文大约需要 20 分钟。
大家好,我是 华仔, 又跟大家见面了。
上篇主要带大家深度剖析了「号称承载 Kafka 客户端消息快递仓库 RecordAccmulator 的架构设计」,消息被暂存到累加器中,今天主要聊聊「发送网络 I/O 的 Sender 线程的架构设计」,深度剖析下消息是如何被发送出去的。
这篇文章干货很多,希望你可以耐心读完。
01 总的概述
通过「场景驱动」的方式,来看看消息是如何从客户端发送出去的。
在上篇中,我们知道了消息被暂存到 Deque<ProducerBatch> 的 batches 中,等「批次已满」或者「有新批次被创建」后,唤醒 Sender 子线程将消息批量发送给 Kafka Broker 端。
接下来我们就来看看,「Sender 线程的架构实现以及发送处理流程」,为了方便大家理解,所有的源码只保留骨干。
02 Sender 线程架构设计
在《图解Kafka生产者初始化核心流程》这篇中我们知道 KafkaProducer 会启动一个后台守护进程,其线程名称:kafka-producer-network-thread + "|" + clientId。
在 KafkaProducer.java 类有常量定义:NETWORK_THREAD_PREFIX,并启动 守护线程 KafkaThread 即 ioThread,如果不主动关闭 Sender 线程会一直执行下去。
github 源码地址如下:
从上面得出 Sender 类是一个线程类, 我们来看看 Sender 线程的重要字段和方法,并讲解其是如何发送消息和处理消息响应的。
02.1 关键字段
从该类属性字段来看比较多,这里说几个关键字段:
- client:KafkaClient 类型,是一个接口类,Sender 线程主要用它来实现真正的网络I/O,即 NetworkClient。该字段主要为 Sender 线程提供了网络连接管理和网络读写操作能力。
- accumulator:RecordAccumulator类型,上一篇的内容 图解 Kafka 源码之快递仓库 RecordAccumulator 架构设计,Sender 线程用它获取待发送的 node 节点及批次消息等能力。
- metadata:ProducerMetadata类型,生产者元数据。因为发送消息时要知道分区 Leader 在哪些节点,以及节点的地址、主题分区的情况等。
- guaranteeMessageOrder:是否保证消息在 broker 端的顺序性,参数:max.in.flight.requests.per.connection。
- maxRequestSize:单个请求发送消息最大字节数,默认为1M,它限制了生产者在单个请求发送的记录数,以避免发送大量请求。
- acks:生产者的消息发送确认机制。有3个可选值:0,1,-1/all。
- retries:生产者发送失败后的重试次数。默认是0次。
- running:Sender线程是否还在运行中。
- forceClose:是否强制关闭,此时会忽略正在发送中的消息。
- requestTimeoutMs:生产者发送请求后等待服务端响应的最大时间。如果超时了且配置了重试次数,会再次发送请求,待重试次数用完后在这个时间范围内返回响应则认为请求最终失败,默认 30 秒。
- retryBackoffMs:生产者在发送请求失败后可能会重新发送失败的请求,其目的就是防止重发过快造成服务端压力过大。默认100 ms。
- apiVersions:ApicVersions类对象,保存了每个node所支持的api版本。
- inFlightBatches:正在执行发送相关的消息批次集合, key为分区,value是 list<|ProducerBatch>。
02.2 关键方法
Sender 类的方法也不少,这里针对关键方法逐一讲解下,原文完整版在星球里,感兴趣的可以扫文末二维码加入。
02.2.1 run()
Sender 线程实现了 Runnable 接口,会不断的调用 runOnce(),这是一个典型的循环事件机制。
当 Sender 线程启动后会直接运行 run() 方法,该方法在 4种情况下会一直循环调用去发送消息到 Broker。
02.2.2 runOnce()
我们来看看这个执行业务处理的方法,关于事务的部分后续专门文章讲解。
该方法比较简单,主要做了3件事情:
- 获取当前时间的时间戳。
- 调用 sendProducerData 发送消息,但并非真正的发送,而是把消息缓存在 NetworkClient 的 Send 字段里。下一篇会讲解 NetworkClient。
- 读取 NetworkClient 的 send 字段消息实现真正的网络发送。
02.2.3 sendProducerData()
该方法主要是获取已经准备好的节点上的批次数据并过滤过期的批次集合,最后暂存消息。
该方法主要做了12件事情,逐一说明下:
- 首先获取元数据,这里主要是根据元数据的更新机制来保证数据的准确性。
- 获取已经准备好的节点。accumulator#reay() 方法会通过发送记录对应的节点和元数据进行比较,返回结果中包括两个重要的集合:「准备好发送的节点集合 readyNodes」、「找不到 Leader 分区对应节点的主题 unKnownLeaderTopic」。
- 如果主题 Leader 分区对应的节点不存在,则强制更新元数据。
- 循环 readyNodes 并检查客户端与要发送节点的网络是否已经建立好了。在 NetworkClient 中维护了客户端与所有节点的连接,这样就可以通过连接的状态判断是否连接正常。
- 获取上面返回的已经准备好的节点上要发送的 ProducerBatch 集合。accumulator#drain() 方法就是将 「TopicPartition」-> 「ProducerBatch 集合」的映射关系转换成 「Node 节点」->「ProducerBatch 集合」的映射关系,如下图所示,这样的话按照节点方式只需要2次就完成,大大减少网络的开销。
- 将从消息累加器中读取的数据集,放入正在执行发送相关的消息批次集合中。
- 要保证消息的顺序性,即参数 max.in.flight.requests.per.connection=1,会添加到 muted 集合,保证只有一个 batch 在发送。
- 从正在执行发送数据集合 inflightBatches 中获取过期集合。
- 从 accumulator 累加器的 batches 中获取过期集合。
- 从 inflightBatches 与 batches 中查找已过期的消息批次(ProducerBatch),判断批次是否过期是根据系统当前时间与 ProducerBatch 创建时间之差是否超过120s,过期时间可以通过参数 delivery.timeout.ms 设置。
- 处理已过期的消息批次,通知该批消息发送失败并返回给客户端。
- 发送消息暂存到 NetworkClient send 字段里。
从上面源码可以看出,SendProducerData 方法中调用到了 Sender 线程类中多个方法,这里就不一一讲解了,感兴趣的请移步到星球中查看完整内容。
03 Sender 发送流程分析
通过前两部分的源码解读和剖析,我们可以得出 Sender 线程的处理流程可以分为两大部分:「发送请求」、「接收响应结果」。
03.1 发送请求
从 runOnce 方法可以得出发送请求也分两步:「消息预发送」、「真正的网络发送」。
03.2 接收响应结果
等 Sender 线程收到 Broker 端的响应结果后,会根据响应结果分情况进行处理。
03.3 时序图
原文完整版在星球里面,如果感兴趣可以扫文末二维码加入。
04 总结
这里,我们一起来总结一下这篇文章的重点。
1、开篇总述消息被暂存到 Deque<ProducerBatch> 的 batches 中,等「批次已满」或者「有新批次被创建」后,唤醒 Sender 子线程将消息批量发送给 Kafka Broker 端,从而引出「Sender 线程」。
2、带你深度剖析了「Sender 线程」 的发送消息以及响应处理的相关方法。
3、最后带你串联了整个消息发送的流程,让你有个更好的整体认知。
文章转载自公众号:华仔聊技术
