1. Kafka发送消息大致流程
- KafkaProducer 首先将待发送的消息封装成 ProducerRecord。
- 紧接着对 ProducerRecord 进行序列化。
- 基于某种分区算法,计算把消息发送到哪个 TopicPartition,此时需要获取集群的元数据信息,以便或知应该将消息发送到哪个 Broker 节点。
- 需要先把消息缓存到 RecordAccumulator 中,然后再把消息分批发送出去。
- 最后由 Sender 子线程,通过 NIO 机制将批量消息发送到 Broker 节点。
Kafka 为什么要设计一个 RecordAccumulator 缓冲池?
原因就是,如果每一条消息都触发一次发送请求,由于网络请求需要消耗一定的资源,每次只发送条消息的吞吐量未免太低。所以,Kafka 就设计了一个 RecordAccumulator 缓冲池。由 main 线程负责把消息发送到缓冲池后就立即返回;当消息积累到 Produceratch 大小或者 linger.ms 时长时,会通过 NIO机制将消息批量的发送到 Broker 端。
2. RecordAccumulator
Kafka 为了提高 Producer 客户端发送消息的吞吐量,选择先将消息暂时缓存起来,等到满足一定的 条件再进行批量发送。这样做可以减少网络的请求次数,从而提高发送消息的性能。而负责将消息缓存起 来的 Java 类就是 RecordAccumulator.java 。
当消息序列化之后,经过分区器计算出应该将消息发送到哪个TopicPartition。针对每条消息,会按照 TopicPartition 维度把他们放在不同的 Deque< ProducerBatch >队列里面。只要消息的 TopicPartition相同,就会被缓存在相同的 Deque< ProducerBatch >里面。
- 每个 ProducerBatch 的大小由参数 batch.size 控制(默认 16384,16KB),而 RecordAccumulator 的大小由参数 buffer.memory 控制(默认 33554432,32MB)。
- 如果申请的内存大小是 ProducerBatch 大小(16KB),并且已分配内存不为空,则直接取出一个 ByteBuffer 返回。
- 如果释放的内存是一个 ProducerBatch 的大小是 16KB,就直接将内存添加到 Deque< ByteBuffer >free 队列即可,这样就可以避免生产者客户端将一个 ProducerBatch 发送出去之后需要等待 JVM 的垃圾回收。
- 但是如果申请的 ProducerBatch > 16KB,就需要通过 JVM 的垃圾回收才能回收内存
3. 消息大小 <= 16KB 的发送场景
3.1 申请内存
我们假设发送的消息大小是 5KB,因此这里申请的 ProducerBatch 是 16KB,会从空闲的池化内存Deque< ByteBuffer > free 的队首获取一块 ByteBuffer 使用。
然后生产者就会将消息发送到不同分区的 ProducerBatch 中,这样当一个 ProducerBatch 被写满消息或者 linger.ms 时间触达的时候。就会由 Sender 子线程负责把 ProducerBatch 发送到 Broker 端。
3.2 释放内存
当把消息批量发送到 Broker 后,就会释放 ProducerBatch 占用的空间,会把 ProducerBatch 占用的内存放到缓冲池 Deque< BvteBuffer > free 的队尾,并调用 BvteBuffer.clear() 清空数据以便下次重复使用。
4. 消息大小>16KB 的发送场景
4.1 申请内存
我们假设发送的消息大小是 24KB,因此这里申请的 ProducerBatch 大小就是 24KB,此时就只能从非池化内存 nonPooledAvailableMemory 中申请内存。如果整个内存池的可用空间比要申请的内存大(this.nonPooledAvailableMemory + freeListSize >= size),就可以直接从 nonPooledAvailableMemory中申请内存。并从 nonPooledAvailableMemory 去掉申请的那一块内存。
当申请到 24KB 的 ProducerBatch 之后,生产者就可以把大小等于 24KB 的消息发送到该ProducerBatch。随后就会将消息发送到 Broker 端。
4.2 释放内存
当把消息批量发送到 Broker 后,就会释放 ProducerBatch 占用的空间,并非仅仅是在非池化内存(nonPooledAvailableMemory)中加上刚刚释放的 ProducerBatch=24KB 内存,该过程需要经过JVM的GC,才能释放内存
5.池化内存和非池化内存相互转换
5.1,池化内存转化为非池化内存
假如,我们现在需要发送的消息的大小是 32KB,因此我们要创建一个32KB 的 ProducerBatch。因为 ProducerBatch 的大小超过了 16KB,所以只能从非池化内存 nonPooledAvailableMemory 中申请内存。但不巧的是,非池化内存中的可用内存已不足 32KB,或者没有剩余内存了。
这个时候就会尝试从池化内存 Deque< ByteBuffer >free 中,将一部分 ByteBuffer 转换到nonPooledAvailableMemory 中。释放第一个 ByteBuffer=16KB,不够再释放第二个 ByteBuffer,一直到可用的内存达到 32KB。当 nonPooledAvailableMemory 有足够的内存时,就可以创建大小是 32KB 的ProducerBatch 了.
5.2,非池化内存转化为池化内存
虽然我们现在需要发送的消息的大小是 5KB,需要创建大小是 16KB 的 ProducerBatch。但此时Deque< ByteBufer >free 中已经没有足够的内存,这时就会从 nonPooledAvailableMemory 中划走一部分内存到池化内存中,Deque< ByteBuffer > free 会将申请到的内存放到 free 队列的头部,然后会从空闲的池化内存 Deque< ByteBuffer >free 的队首获取一块 ByteBuffer,用于创建 ProducerBatch。
然后生产者就会将消息发送到不同分区的 ProducerBatch 中,这样当一个 ProducerBatch 被写满消息或者 linger.ms 时间触达的时候。就会由 Sender 子线程负责把,ProducerBatch 发送到 Broker 端。
当把消息批量发送到 Broker 后,就会释放 ProducerBatch 占用的空间,会把 ProducerBatch 占用的内存放到缓冲池 Deque< ByteBufer >free 的队尾,并调用 ByteBufer.clear() 清空数据以便下次重复使用。因此,池化内存的空间不但会增加,而且该过程也不会触发 JVM 的 GC。
6. 总结
kafka通过实现了一个内存池来累积将要发送的消息(增加吞吐量),同时使用bytebuffer来存储消息(通过bytebuffer可以避免JVM垃圾回收带来的影响),最后还使用了一块非池化空间(旨在应对消息大于 16KB 的场景)。