
生产者的实现逻辑-kafka知识体系2
作者 | 小汪哥写代码
来源 | 小汪哥(ID:xwgcoding)
kafka 是单条发送还是批量发送消息?
kafka 怎么做到单条发送?
kafka 发送消息是顺序的吗?
生产者什么情况下可能会频繁FullGC?
消息发送的逻辑
上帝视角来看消息发送的流程。
生产者的设计
消费发送机制:
1)序列化器:序列化消息对象转成字节数组,然后通过网络传输。
2)分区器:计算消息发往的具体分区;如果显示指定了partition,便不会走分区器。
3)消息缓冲池:客户端的消息缓冲池,默认大小32M,见参数buffer.memory。
4)批量发送:缓冲池中消息会按batch分批次发送,默认批次大小16KB,见参数batch.size。
负载均衡设计:
由于消息topic 由多个partition 组成,且partition 会均衡分布到不同broker 上。因此,为了有效利用broker 集群的性能,提高消息的吞吐量,producer 可以通过随机或者hash 等方式,将消息平均发送到多个partition 上,以实现负载均衡。
分区策略:
轮询策略,默认策略
随机策略,实际表现来看,它要逊于轮询策略
按消息键保序策略,一旦消息被定义了 Key,那么你就可以保证同一个 Key 的所有消息都进入到相同的分区里面,由于每个分区下的消息处理都是有顺序的。
KafkaProducer
源码
KafkaProducer 具有如下特征:
KafkaProducer 是线程安全的,可以被多个线程交叉使用。
KafkaProducer 内部包含一个缓存池,存放待发送消息,即 ProducerRecord 队列,与此同时会开启一个IO线程将 ProducerRecord 对象发送到 Kafka 集群。
KafkaProducer 的消息发送 API send 方法是异步,只负责将待发送消息 ProducerRecord 发送到缓存区中,立即返回,并返回一个结果凭证 Future。
acks 参数的作用
KafkaProducer 提供了一个核心参数 acks 用来定义消息“已提交”的条件(标准),就是 Broker 端向客户端承偌已提交的条件,可选值如下:
0:只要调用 KafkaProducer 的 send 方法返回后即认为成功
all 或 -1:表示消息不仅需要 Leader 节点已存储该消息,并且要求其副本(准确的来说是 ISR 中的节点)全部存储才认为已提交,才向客户端返回提交成功。这是最严格的持久化保障,当然性能也最低。
1:表示消息只需要写入 Leader 节点后就可以向客户端返回提交成功。
retries 参数的作用
kafka 在生产端提供的另外一个核心属性,用来控制消息在发送失败后的重试次数,设置为 0 表示不重试,重试就有可能造成消息在发送端的重复。从消息发送接口来看:
从上面的 API 可以得知,用户在使用 KafkaProducer 发送消息时,首先需要将待发送的消息封装成 ProducerRecord,返回的是一个 Future 对象,典型的 Future 设计模式。
Kafka 消息追加流程
KafkaProducer 的 send 方法,并不会直接向 broker 发送消息,kafka 将消息发送异步化,即分解成两个步骤,send 方法的职责是将消息追加到内存中(分区的缓存队列中),然后会由专门的 Send 线程异步将缓存中的消息批量发送到 Kafka Broker 中。
主要的方法在KafkaProducer#doSend
将消息追加到生产者的发送缓存区,其实现类为:RecordAccumulator。我们先来看一下 Kafka 一条消息写到内存的流程图:
Sender线程
到此为止,我们看到,当我们调用send 方法的时候,其实只是发送到了 生产者客户端的服务内存中。还没有到Broker。Kafka producer 客户端后台会启动一个线程不停的轮询消息批次存放的区域,把消息发送给Broker。
消息批次的内存结构和分配
根据上面的源码我们可以了解到,每一个ProducerBatch 是一块 大小为batch.size 字节大小的内存。而且用到了池化技术。
缓冲池的内存持有类是 BufferPool,我们先来看下 BufferPool 都有哪些成员:
从 BufferPool 的成员可看出,缓冲池实际上由一个个 ByteBuffer 组成的,BufferPool 持有这些内存块,并保存在成员 free 中,free 的总大小由 totalMemory 作限制,而 nonPooledAvailableMemory 则表示还剩下缓冲池还剩下多少内存还未被分配。
当 Batch 的消息发送完毕后,就会将它持有的内存块归还到 free 中,以便后面的 Batch 申请内存块时不再创建新的 ByteBuffer,从 free 中取就可以了,从而避免了内存块被 JVM 回收的问题。
创建内存块的流程如下:
归还内存块的逻辑流程
如果归还的内存块大小等于 batchSize,则将其清空后添加到缓冲池的 free 中,即将其归还给缓冲池,避免了 JVM GC 回收该内存块。如果不等于就直接将内存大小累加到未分配并且空闲的内存大小值中即可,内存就无需归还了,等待 JVM GC 回收掉,最后唤醒正在等待空闲内存的线程。
Java生产者是如何管理TCP连接的
为何采用 TCP?
Apache Kafka 的所有通信都是基于 TCP 的,而不是基于 HTTP 或其他协议。无论是生产者、消费者,还是 Broker 之间的通信都是如此
从社区的角度来看,在开发客户端时,人们能够利用 TCP 本身提供的一些高级功能,比如多路复用请求以及同时轮询多个连接的能力
TCP 的多路复用请求会在一条物理连接上创建若干个虚拟连接,每个虚拟连接负责流转各自对应的数据流。其实严格来说,TCP 并不能多路复用,它只是提供可靠的消息交付语义保证,比如自动重传丢失的报文。
而且目前已知的 HTTP 库在很多编程语言中都略显简陋。
何时创建 TCP 连接?
TCP 连接是在创建 KafkaProducer 实例时建立的 ,在创建 KafkaProducer 实例时,生产者应用会在后台创建并启动一个名为 Sender 的线程,该 Sender 线程开始运行时首先会创建与 Broker 的连接
bootstrap.servers是Producer的核心参数之一,指定了Producer启动时要连接的Broker地址
如果bootstrap.servers指定了1000个Broker,那么Producer启动时会首先创建与这1000个Broker的TCP连接
因此不建议把集群中所有的Broker信息都配置到bootstrap.servers中,通常配置3~4台足够
Producer一旦连接到集群中的任意一台Broker,就能拿到整个集群的Broker信息(metadata request)
TCP 连接还可能在两个地方被创建:一个是在更新元数据后,另一个是在消息发送时。
当Producer更新了集群的元数据后,如果发现与某些Broker当前没有连接,那么Producer会创建一个TCP连接
【场景1】
当Producer尝试向不存在的主题发送消息时,Broker会告诉Producer这个主题不存在,此时Producer会发送metadata request到Kafka集群,去尝试获取最新的元数据信息,与集群中所有的Broker建立TCP连接。
【场景2】
Producer通过metadata.max.age.ms参数定期地去更新元数据信息,默认值300000,即5分钟。
当Producer要发送消息时,Producer发现与目标Broker(依赖负载均衡算法)还没有连接,也会创建一个TCP连接
何时关闭 TCP 连接?
Producer端关闭TCP连接有两种方式:用户主动关闭、Kafka自动关闭。
【用户主动关闭】
广义的主动关闭,包括用户调用kill -9来杀掉Producer,最推荐的方式:producer.close()
【Kafka自动关闭】
Producer端参数connections.max.idle.ms,默认值540000,即9分钟
如果9分钟内没有任何请求经过某个TCP连接,Kafka会主动把TCP连接关闭
connections.max.idle.ms=-1会禁用这种机制,TCP连接将成为永久长连接
Kafka创建的Socket连接都开启了keepalive
关闭TCP连接的发起方是Kafka客户端,属于被动关闭的场景
被动关闭的后果就是会产生大量的CLOSE_WAIT连接
Producer端或Client端没有机会显式地观测到此TCP连接已被中断
总结
现在我们可以回答开头的3个问题了。
1、kafka 是单条发送还是批量发送消息?
正常情况下都是批量发送的。封装成一个ProducerBatch 发送。
2.kafka 怎么做到单条发送?
只能设置单生产者单线程同步调用send 方法。
3.kafka 发送消息是顺序的吗?
不是的,如果需求顺序必须设置key,并且是生产者是单线程的。
4.生产者什么情况下可能会频繁FullGC?
如果你的消息大小比 batchSize 还要大,则不会从 free 中循环获取已分配好的内存块,而是重新创建一个新的 ByteBuffer,并且该 ByteBuffer 不会被归还到缓冲池中(JVM GC 回收),如果此时 nonPooledAvailableMemory 比消息体还要小,还会将 free 中空闲的内存块销毁(JVM GC 回收),以便缓冲池中有足够的内存空间提供给用户申请,这些动作都会导致频繁 GC 的问题出现。
因此,需要根据业务消息的大小,适当调整 batch.size 的大小,避免频繁 GC。
