目录
kafka生产者发送消息的主要流程
-
创建ProducerRecord:
- 应用程序首先将待发送的数据封装成一个
ProducerRecord
对象。这个对象包含了消息的Topic、可选的Key和Value,以及时间戳等信息。
- 应用程序首先将待发送的数据封装成一个
-
序列化:
- Kafka是面向字节的系统,因此
ProducerRecord
中的Key和Value需要序列化成字节序列。这通常通过序列化器(Serializer)来完成,例如StringSerializer
或ByteArraySerializer
。
- Kafka是面向字节的系统,因此
-
选择分区:
- Kafka使用分区机制来保证消息的顺序性和扩展性。Producer根据Key(如果提供)和分区器(Partitioner)来决定将消息发送到哪个分区。如果没有Key,则使用轮询或其他自定义分区算法。
-
获取元数据:
- Producer需要知道每个TopicPartition的Leader是谁,这样才能知道消息应该发送到哪个Broker。这通过获取集群的元数据来实现,元数据也包含了分区信息和副本信息。
-
缓存到RecordAccumulator:
- 序列化后的消息被缓存到
RecordAccumulator
中。这是一个内存缓冲区,用于收集将要发送的消息批次。它允许Producer收集多个消息,然后批量发送,以提高效率。
- 序列化后的消息被缓存到
-
等待触发条件:
- 消息在
RecordAccumulator
中等待,直到满足以下条件之一:- 达到
batch.size
指定的批次最大大小。 - 达到
linger.ms
指定的延迟时间,即使批次尚未满。
- 达到
- 消息在
-
分批发送:
- 当触发条件满足时,消息批次将被封闭并准备发送。Producer会根据消息的目标分区,将批次分发到不同的分区队列中。
-
Sender子线程处理:
- Kafka Producer内部有一个或多个Sender线程(取决于
max.in.flight.requests.per.connection
的配置)。这些线程负责实际的网络发送工作。
- Kafka Producer内部有一个或多个Sender线程(取决于
-
使用NIO发送:
- Sender线程使用Java NIO(非阻塞I/O)机制,将消息批次异步发送到对应的Broker。NIO允许在单个线程内处理多个网络连接,从而提高网络操作的效率。
-
回调处理:
- 每条消息发送后,如果提供了回调函数,Sender线程会在消息发送完成后调用它,以便应用程序可以处理发送结果,例如记录日志或更新状态。
-
确认和重试:
- Producer根据
acks
配置等待Broker的确认。如果发送失败,根据retries
和retry.backoff.ms
配置,Producer可能会重试发送消息。
- Producer根据
-
错误处理:
- 如果消息最终无法被发送(例如,因为Broker不可用或网络问题),Producer将调用每条消息的回调函数,并传递一个异常。
RecordAccumulator的
关键特性和工作原理
关键特性
-
内存池管理:
RecordAccumulator
使用内存池来管理内存,这有助于减少因频繁分配和释放内存导致的性能问题。 -
按TopicPartition缓存:消息根据
TopicPartition
进行缓存,相同TopicPartition
的消息被放在同一个Deque<ProducerBatch>
队列中。 -
批量处理:通过将消息批量处理,
RecordAccumulator
减少了网络请求的次数,提高了发送效率。 -
动态内存分配:
RecordAccumulator
可以动态地分配和释放内存,以适应不同大小的消息和不同的发送需求。 -
配置驱动:
RecordAccumulator
的行为可以通过参数如batch.size
和buffer.memory
进行配置。
工作原理
-
消息序列化与分区:消息首先被序列化,然后根据分区算法确定目标
TopicPartition
。 -
缓存分配:
RecordAccumulator
根据batch.size
和buffer.memory
参数分配内存。如果请求的内存大小等于ProducerBatch
的大小(例如16KB),它会尝试从内存池中复用已有的ByteBuffer
。 -
内存池复用:
- 如果可用内存块的大小与请求的大小相匹配,并且内存池中有可用的内存块,就直接复用。
- 如果内存块大小匹配,但在内存池中没有可用的内存块,就需要从JVM堆内存中分配新的内存。
-
内存释放:
- 当
ProducerBatch
被发送后,内存可以被释放回RecordAccumulator
。 - 如果释放的内存块大小等于
poolableSize
,它会被添加到空闲列表中,以便再次使用。 - 如果大小不匹配,内存会被添加到
nonPooledAvailableMemory
中,等待JVM的垃圾回收。
- 当
-
内存分配策略:
- 对于大小等于
poolableSize
的内存块,RecordAccumulator
会尝试重用这些内存块,以减少垃圾回收的需要。 - 对于其他大小的内存块,它们将被返回给JVM,依赖垃圾回收机制来释放。
- 对于大小等于
-
等待和唤醒机制:如果内存不足,发送消息的线程可能会被阻塞,直到有足够的内存可用。一旦内存被释放,等待的线程将被唤醒。
-
内存管理优化:通过这种方式,
RecordAccumulator
减少了对JVM垃圾回收的依赖,从而提高了性能。
通过这种设计,RecordAccumulator
不仅提高了消息发送的吞吐量和效率,还通过减少垃圾回收的需求来优化内存使用,这对于高性能的Kafka Producer至关重要。
RecordAccumulator内存申请流程
如果此时生产者生成了一条消息,并且计算出了应该将消息发送到哪个 TopicPartition。接下来我们看看 RecordAccumulator.append() 方法的执行流程
消息大小<=16KB
在消息大小小于或等于16KB的场景中,Kafka Producer使用RecordAccumulator
进行内存管理和消息发送的流程如下:
申请内存
-
预估消息大小:首先,Producer预估消息M的大小,包括序列化后的Key、Value和Headers的大小。
-
确定批量大小:如果预估的消息大小小于或等于
batch.size
(默认为16KB),则使用batch.size
作为批量大小创建ProducerBatch
。 -
从内存池获取ByteBuffer:
- 如果内存池(
Deque<ByteBuffer> free
)中有等于batch.size
大小的空闲ByteBuffer
,则直接从队首获取一个ByteBuffer
对象使用。 - 这是通过
if (size == poolableSize && !this.free.isEmpty()) return this.free.pollFirst();
实现的。
- 如果内存池(
-
创建ProducerBatch:使用获取到的
ByteBuffer
创建一个ProducerBatch
,并将消息追加到这个批次中。 -
等待批量发送条件:当
ProducerBatch
被填满(达到batch.size
)或等待时间达到linger.ms
设置的值时,Sender线程会将这个批次发送到Broker。
释放内存
-
发送后释放:Sender线程在将
ProducerBatch
成功发送到Broker后,会释放该批次占用的内存。 -
清空ByteBuffer:通过调用
ByteBuffer.clear()
清空ByteBuffer
中的数据,准备重复使用。 -
归还到内存池:
- 如果释放的
ByteBuffer
大小等于poolableSize
(默认16KB),则将其归还到内存池的队尾,以便后续的ProducerBatch
使用。 - 这是通过
buffer.clear(); this.free.add(buffer);
实现的。
- 如果释放的
-
唤醒等待线程:如果有其他Producer线程因为内存不足而处于等待状态,此时会唤醒它们,以便它们可以继续执行消息发送操作。
-
依赖JVM GC:如果释放的
ByteBuffer
大小不等于poolableSize
,则将其归还到nonPooledAvailableMemory
中,等待JVM的垃圾回收器进行回收。
消息大小>16KB
在消息大小大于16KB的发送场景中,Kafka Producer的RecordAccumulator
如何申请和释放内存的流程如下:
申请内存
-
预估消息大小:首先,Producer预估消息M的大小,包括序列化后的Key、Value和Headers的大小。
-
确定批量大小:如果预估的消息大小大于
batch.size
(默认为16KB),则使用消息预估的大小作为批量大小创建ProducerBatch
。 -
从非池化内存申请内存:
- 由于消息大小超过了默认的池化大小,不能从
Deque<ByteBuffer> free
中申请内存,而需要从非池化内存nonPooledAvailableMemory
中申请。 - 首先计算当前可用的总内存,包括
nonPooledAvailableMemory
和内存池中可回收的内存大小(freeListSize
)。
- 由于消息大小超过了默认的池化大小,不能从
-
检查可用内存:
- 如果当前空闲的总内存大于或等于需要申请的内存大小,则可以继续申请内存。
-
调整可用内存值:
- 从
nonPooledAvailableMemory
中减去申请的内存大小,更新可用内存的值。
- 从
-
分配内存:
- 执行
allocate
方法从JVM堆内存中分配所需大小的内存给ProducerBatch
。
- 执行
-
发送消息:
- 将大于16KB的消息放入新创建的
ProducerBatch
中,并等待发送条件满足后,由Sender线程发送到Broker端。
- 将大于16KB的消息放入新创建的
释放内存
-
发送后释放:Sender线程在将
ProducerBatch
成功发送到Broker后,会释放该批次占用的内存。 -
归还内存到非池化内存:
- 由于释放的
ByteBuffer
大小不等于poolableSize
(默认16KB),不能直接放入内存池复用,而是将其大小加回到nonPooledAvailableMemory
中。
- 由于释放的
-
依赖JVM GC:
- 释放的内存需要依赖JVM的垃圾回收器进行回收,以便内存最终被释放。
-
唤醒等待线程:
- 如果有其他Producer线程因为内存不足而处于等待状态,此时会唤醒它们,以便它们可以继续执行消息发送操作。
-
内存管理:
- 通过
deallocate
方法管理内存的归还和线程的唤醒,确保内存得到合理利用和回收。
- 通过
池化内存和非池化内存的转化
1 池化内存转换成非池化内存
-
内存申请需求:当需要发送大于默认
batch.size
(如16KB)的消息时,比如32KB,Producer需要从非池化内存nonPooledAvailableMemory
中申请内存。 -
检查非池化内存:如果
nonPooledAvailableMemory
中的可用内存不足以满足申请需求,Producer会尝试从池化内存Deque<ByteBuffer> free
中释放一些ByteBuffer
来补充nonPooledAvailableMemory
。 -
释放池化内存:Producer会从
free
队列中逐个释放ByteBuffer
(每个默认16KB),直到nonPooledAvailableMemory
中的内存足够申请新的ProducerBatch
。 -
内存转换:此过程涉及到将池化内存中的
ByteBuffer
释放并转移到非池化内存中,以满足大消息的发送需求。 -
内存归还:发送完成后,归还的内存需要通过JVM GC来释放。
2 非池化内存转换成池化内存
-
内存申请:当发送的消息较小,需要的
ProducerBatch
小于或等于默认batch.size
时,如果池化内存free
中没有足够的ByteBuffer
,Producer会从nonPooledAvailableMemory
中划分一部分内存到池化内存。 -
内存划分:将非池化内存中的内存划分出来,并将其放入
free
队列的头部。 -
创建ProducerBatch:使用新划分的内存创建
ProducerBatch
,用于存放小消息。 -
内存释放与复用:消息发送后,释放的内存直接放入
free
队尾,通过ByteBuffer.clear()
清空数据,实现内存的复用,不触发JVM GC。
3 内存不足发生阻塞的场景
-
内存申请阻塞:如果内存不足,申请内存的线程可能会阻塞,直到有足够的内存可用。
-
唤醒等待线程:一旦内存被释放并满足申请需求,等待的线程将被唤醒。
4 是否需要修改batch.size
的默认值
-
参数调优:
batch.size
的值可以根据实际场景进行调整,以优化吞吐量和内存利用率。 -
性能测试:调整
batch.size
需要基于充分的性能测试,以确定最优值。 -
内存利用率:增大
batch.size
可能会降低内存利用率,特别是当大容量的ProducerBatch
只包含小消息时。 -
参数权衡:需要在内存利用率和吞吐量之间找到平衡点,合理设置
batch.size
。