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。

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 关键字来保障。

发布了76 篇原创文章 · 获赞 1 · 访问量 5103

猜你喜欢

转载自blog.csdn.net/qq_38083545/article/details/93067377
今日推荐