Kafka在发送一条消息时经历了什么?

Kafka在发送一条消息到borker之前,需要经过过滤器,序列化器, 分区器。
架构图
整个生产者客户端由两个线程协调运行,这两个线程分别为主线程Sender线程 。在主线程中由 KafkaProducer创建消息,然后通过可能的拦截器、序列化器和分区器的作用之后缓存到RecordAccumulator中 Sender 线程负责从 RecordAccumulator中获取消息并将其发送到 Kafka broker中去
拦截器的代码:

  public ProducerRecord<K, V> onSend(ProducerRecord<K, V> record) {
      ProducerRecord<K, V> interceptRecord = record;
      for (ProducerInterceptor<K, V> interceptor : this.interceptors) {
          try {
              interceptRecord = interceptor.onSend(interceptRecord);
          } catch (Exception e) {
              // do not propagate interceptor exception, log and continue calling other interceptors
              // be careful not to throw exception from here
              if (record != null)
                  log.warn("Error executing interceptor onSend callback for topic: {}, partition: {}", record.topic(), record.partition(), e);
              else
                  log.warn("Error executing interceptor onSend callback", e);
          }
      }
      return interceptRecord;
  }

RecordAccumulator 主要用来缓存消息 以便 Sender 线程可以批量发送,进而减少网络传输 的资源消耗以提升性能 。 RecordAccumulator 缓存的大 小可以通过生产者客户端参数 buffer .memory 配置,默认值为 33554432B,即 32M。

RecordAccumulator.RecordAppendResult result = accumulator.append(tp, timestamp, serializedKey,
                    serializedValue, headers, interceptCallback, remainingWaitMs);

如果生产者发送 消息的速度超过发 送到服务器的速度,则会导致生产者空间不足,这个时候 KafkaProducer的 send()方法调用要么 被阻塞,要么抛出异常,这个取决于参数max.block.ms 的配置,此参数的默认值为60 秒 。
主线程中发送过来的消息都会被迫加到 RecordAccumulator 的某个双端队列( Deque)中, 在 RecordAccumulator 的内部为每个分区都维护了 一 个双端队列,队列中的内容就是ProducerBatch,即 Deque。消息写入缓存 时,追加到双端队列的尾部: Sender
读取消息时 ,从双端队列的头部读取。注意 ProducerBatch 不是 ProducerRecord, ProducerBatch 中可以包含一至多个 ProducerRecord。 通俗地说, ProducerRecord 是生产者中创建的消息,而 ProducerBatch 是指一个消息批次 , ProducerRecord 会被包含在 ProducerBatch 中,这样可以使 宇 节的使用更加紧凑。与此同时,将较小的 ProducerRecord 拼凑成一个较大 的 ProducerBatch,也 可以减少网络请求的次数以提升整体的吞吐量 。
如果生产者客户端需要向很多分区发送消息, 则可以 将
buffer .memory 参数适当调大以增加整体的吞吐量 。
消息在网络上都是以字节 CByte)的形式传输的,在发送之前需要创建一块内存区域来保 存对应的消息 。在 Kafka 生产者客户端中,通过 java.io.ByteBuffer 实现消息内存的创建和释放。 不过频繁的创建和释放是比较耗费资源的,在 RecordAccumulator的内部还有一个 BufferPool, 它主要用来实现 ByteBuffer 的复用,以实现缓存的高效利用 。不过 BufferPool 只针对特定大小的 ByteBuffer进行管理,而其他大小的 ByteBuffer不会缓存进 Bu他rPool 中,这个特定的大小batch.size 参数来指定,默认值为 16384B,即 16KB。 我们可以适当地调大 batch.size参数 以便多缓存一些消息。
ProducerBatch 的大小和 batch.size 参数也有着密切的关系。当一条消息( ProducerRecord ) 流入 RecordAccumulator 时,会先寻找与消息分区所对应的双端队列再从 这个双端队列的尾部获取一个 ProducerBatch ,查看 ProducerBatch 中是否 还可以写入这个 ProducerRecord,如 果可以则写入,如果不可 以则 需要 创 建一个新 的 ProducerBatch。在新建 ProducerBatch时评估这条消息的大小是否超过 batch.size 参数的大 小,如果不超过,那么就以 batch. size 参数的大小来创建 ProducerBatch,这样在使用完这 段内存区域之后,可以通过 BufferPool 的管理来进行复用;如果超过,那么就以评估的大小来 创建 ProducerBatch, 这段内存区域不会被复用。
Sender 从 RecordAccumulator 中 获取缓存的消息之后,会进一步将原本<分区, Deque>的保存形式转变成<Node, List< ProducerBatch>的形式,其中 Node 表示 Kafka 集群 的 broker 节点 。对于网络连接来说,生产者客户端是与具体 的 broker 节点建立 的连接,也 就是向具体的 broker 节点发送消息,而并不关心消息属于哪一个分区;而对于 KafkaProducer 的应用逻辑而 言 ,我们只 关注向哪个分区中发送哪些消息,所 以在这里需要做一个应用逻辑层 面到网络IO层面的转换。
在转换成<Node, List>的形式之后, Sender 还 会进一步封装成<Node, Request>的形式,这样就可以将 Request 请求发往各个 Node了,
这里 的 Request 是指 Kafka 的 各种协议请求,对于消息发送而言就是指具体的 ProduceRequest请求在从 Sender 线程发往 Kafka 之前还会保存到 InFlightRequests 中, InFlightRequests 保存的具体对象是 Map<Nodeld, Deque>,
它的主要作用是缓存 了已经发出去但还 没有收到响应的请求。与此同时, InFlightRequests 通过配置参数还可 以限制每个连接(也就是 客户端与 Node 之间的连接)最多缓存的请求数。这个配置参数为 max.in.flight.requests .per.connection,默认值为 5,即每个连接最多只能缓存 5 个未响应的请求,超过该数值 之后就不能再向这个连接发送更多的请求了,除非有缓存的请求收到了响应( Response)。通 过比较 Deque的 size 与这个参数的大小来判断对应的 Node 中是否己 经堆积了很多未 响应的消息,如果真是如此,那么说明这个 Node 节点负载较大或网络连接有问题,再继续向 其发送请求会增大请求超时的可能。

原创文章 132 获赞 23 访问量 3万+

猜你喜欢

转载自blog.csdn.net/qq_33797928/article/details/91478092