5.生产者客户端原理分析

整体架构

在上一节中提交了消息在真正发往 Kafka 之前,有可能需要经历拦截器(Interceptor)、序列化器(Serializer)和分区器(Partitioner)等一系列的作用,那么在这之后呢?下面看一下生产者客户端的整体架构:

在这里插入图片描述
生产者客户端由两个线程协调运行,这两个线程分别为主线程和 Sender 线程(发送线程)。在主线程由 KafkaProducer 创建消息,经过拦截器,序列化器,分区器的作用后将消息缓存到消息累加器中(RecordAccumulator,也称之为消息收集器)。Sender 线程负责从消息收集器获取消息并发送往 Kafka。

RecordAccumulator 主要用来缓存消息以便 Sender 线程可以批量发送,进而减少网络传输的资源消耗以提升性能。RecordAccumulator缓存的大小可以通过生产者客户端参数 buffer.memory 配置,默认值为32MB。如果生产者发送消息的速度超过发送到 Kafka 的速度就会导致生产者空间不足,这个时候 KafkaProducer 的 send() 方法要么被阻塞,要么抛出异常,这个参数取决于 max.block.ms的配置,这个参数的默认值为60秒。

主线程中发送过来的消息都会被追加到 RecordAccumulator 的某个双端队列(Deque),在 RecordAccumulator 内部为每个分区维护了一个双端队列,队列的内容就是 ProducerBatch,即Deque。消息写入缓存时,会追加在队列的尾部;消费者读取消息的时候从队列的头部读取。注意 ProducerBatch 不是 ProducerRecord,一个 ProducerBatch 是指一个批次,包含了多个 ProducerRecord,这样可以使字节的使用更加的紧凑。于此同时将较小的 ProducerRecord 拼接成一个较大的 ProducerBatch 也可以减少网络传输次数提升吞吐量。如果生产者客户端要向很多分区发送消息,则可以讲 buffer.memory 参数适当调大以增加吞吐量。

消息在网络上都是以字节(Byte)的形式传输的,在发送之前需要创建一块内存区域保存对应的消息。在Kafka 生产者客户端中,通过 java.io.ByteBuffer 实现消息内存的创建和释放。不过频繁的创建释放太过于消耗资源,所以在 RecordAccumulator 的内部还有一个 BufferPool,它主要用来实现 ByteBuffer 的复用,用以实现缓存的高效利用。不过 BufferPool只针对特定大小的 BufferPool 进行管理,而其他大小的 ByteBuffer 不会缓存进 BufferPool中,这个特定的大小由 batch.size 来指定,默认只为 16KB。

ProducerBatch 的大小和 batch.size 参数也有着密切的关系。当一条消息(ProducerRecord)流入 RecordAccumulator 时,会先寻找与消息分区所对应的双端队列(如果没有则新建),再从这个双端队列的尾部获取一个 ProducerBatch(如果没有则新建),查看 ProducerBatch 中是否还可以写入这个 ProducerRecord,如果可以则写入,如果不可以则需要创建一个新的 ProducerBatch。在新建 ProducerBatch 时评估这条消息的大小是否超过 batch.size 参数的大小,如果不超过,那么就以 batch.size 参数的大小来创建 ProducerBatch,这样在使用完这段内存区域之后,可以通过 BufferPool 的管理来进行复用;如果超过,那么就以评估的大小来创建 ProducerBatch,这段内存区域不会被复用。

Sender 线程从 RecordAccumulator 中获取缓存的消息后,会将原本的<分区,Deque< ProducerBatch >>的保存形式转换成<Node,List< ProducerBatch >> 的形式,其中Node代表的是 broker。对于网络连接来说,生产者客户端是与具体的 broker 进行连接一个 broker 对应一个客户端连接,而不是关心是那个分区;而对于 KafkaProducer 的应用逻辑来说是关注往那个分区发送消息,所以这里要做一层逻辑层面到网络I/O层面的转换。转换成<Node,List>的形式之后,Sender 还会进一步分装成<Node,Request>的形式,这样就可以将Request请求发往各个 broker,这里的 Request 是指Kafka的各种协议请求,对与消息发送而言就是指ProducerRequest

请求在从 Sender 线程发往 Kafka 之前还会保存到 InFlightRequests 中,InFlightRequests 保存对象的具体形式为Map<NodeId,Deque>,它的主要作用是保存已经发送但是还没有收到响应的请求。(NodeId是String类型,表示的是节点编号)。与此同时,InFlightRequests 还提供了许多管理类的方法,并且通过配置参数还可以限制每个连接(客户端到 broker的连接)可以缓存多少个请求。参数配置为max.in.flight.requests. per. connection,默认的是5个,也就是说一个连接如果缓存了5个未响应的请求,那么就不能在向这个连接发送更多的请求了,除非有缓存的请求收到了响应(Response)。通过比较Deque的size与这个参数大小来判断对应的Node中是否已经堆积了很多未响应的消息,如果真是这样,那么说明这个Node节点负载比较大或者网络连接有问题,继续发送会增大请求超时的可能。

元数据的更新

前面提及的 InflightRequests 还可以获得 leastLoadeNode,就是所有的Node中负载最小的那个。这里的负载最小是通过每个Node在InflightRequests中还未确认的请求数量决定的,未确认的请求越多则认为负载越大。对于下图中的 InFlightRequests来说,Node1的负载最小,也就是说 Node1为当前的 leastLoadedNode。leastLoadedNode 的概念可以用于多个应用场合,比如元数据请求、消费者组播协议的交互。
在这里插入图片描述

什么是元数据?

我们使用如下方法创建了一条消息 ProducerRecord:

ProducerRecord<String, String> record = new ProducerRecord<>(topic, "Hello, Kafka!");

我们只知道主题名称,对于其他信息一无所知。KafkaProducer 需要将消息发送到主题的某个分区对应的leader副本之前,首先需要知道主题的分区数量,然后根据计算或者是指定分区得到分区信息,也就是分区器做的事情。之后 KafkaProducer 根据分区的leader副本得到broker节点的地址、端口等信息才能建立连接,最终将消息发送到Kafka,这一个过程中所需要的信息都属于元数据。

在第3节中说到 bootstrap.servers 参数配置只需要配置部分的 broker 节点即可,客户端会根据配置的 broker 节点找到集群中的其他节点地址,这一过程也属于元数据的相关操作。于此同时,分区数量及 leader 副本的分布都会动态的变化,客户端也需要动态的捕捉这些变化。

元数据是指 Kafka 集群的元数据,这些元数据具体记录了集群中有那些主题,这些主题有那些分区,,每个分区的leader副本分配在那个节点,follower副本分配在那些节点上,那些副本在AR、ISR等集合中,集群有那些节点,控制器节点又是那一个等信息。

当客户端中没有需要使用的元数据时,比如没有指定的主题信息,或超过 metadata.max.age.ms 时间没有更新元数据都会引起元数据的更新操作。客户端参数 metadata.max.age.ms 默认时间为300000也就是5分钟。元数据的更新操作是在客户端内部进行的,对客户端外部的使用者不可见。当需要元数据的时候,会先选出 leastLoadedNode,然后向这个Node发送MetadataRequest请求来获取具体的元数据信息。这个更新操作是由 Sender 线程发起的,在创建完 MetaDataRequest 之后同样会存入 InFlightRequests,之后的步骤和发送消息类似。元数据虽然由 Sender 线程负责更新,但是主线程也需要读取这些信息,这里的数据同步通过 synchronized 和 final 关键字来保障。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值