目录
1.Kafka生产者端出现延迟的原因
Kafka生产者端出现延迟的原因可以归结为多个方面
-
元数据获取延迟:生产者在发送消息前需要确定目标分区的Leader副本。这需要通过获取主题的元数据来实现。如果元数据获取过程耗时过长,尤其是在
max.block.ms
参数设定的时间内无法完成,生产者将无法发送消息,从而导致延迟。 -
网络请求开销:每次发送消息,生产者都需要与Broker建立网络连接。如果生产者频繁发送单条消息,这将导致大量的网络开销,降低消息发送的效率。
-
RecordBatch 累积机制:为了减少网络请求,生产者会将多条消息累积到一个RecordBatch中。这个过程由
batch.size
参数控制。如果RecordBatch设置得过小,即使消息生成速率不高,也可能导致频繁的网络请求,从而增加延迟。 -
Linger时间设置:
linger.ms
参数决定了生产者在发送消息前等待更多消息的时间。如果设置为0,生产者将尽可能快地发送消息,但这可能导致RecordBatch未填满就发送,降低了网络效率。如果设置得过高,生产者会等待更长时间以填满RecordBatch,这可能会增加延迟,但可以提高吞吐量。 -
压缩策略:启用压缩可以减少发送到Broker的数据量,降低网络带宽的使用。然而,压缩和解压缩过程会增加CPU负载,可能会对消息发送的延迟产生影响。
-
生产者配置:其他生产者配置,如
buffer.memory
(控制生产者可以缓冲的消息的最大字节数)和acks
(控制消息确认的级别),也会影响消息的发送和确认机制,进而影响延迟。 -
Broker负载:Broker的负载情况也会影响消息的发送。如果Broker正在处理大量请求,它可能无法及时响应生产者的请求,导致延迟。
-
主题分区和副本策略:主题的分区数和副本因子也会影响延迟。更多的分区可以提供更高的并行处理能力,但同时也增加了协调副本之间的开销。副本因子的增加可以提高数据的可靠性,但也会增加写入的延迟。
2.消息分区策略
在Kafka v2.3版本以前,RoundRobin分区策略是一种简单的数据分发机制,它在没有key的情况下将消息均匀地分配到各个分区。
RoundRobin分区策略
- 均匀分配:RoundRobin策略通过循环遍历分区列表,将连续的消息发送到不同的分区,实现负载均衡。
- 批次增多:由于RoundRobin策略在每个分区中轮流发送消息,当
linger.ms
参数设置为正值时,可能会导致每个批次中的消息数量减少,甚至出现单个消息的批次。
Network Thread的任务
- 检查Broker状态:Network Thread不断检查Broker节点状态,以获取健康的TopicPartition列表。
- 转换RecordBatch:将RecordBatch从
Map<TopicPartition, Deque<RecordBatch>>
转换为Map<brokerID, List<RecordBatch>>
,以便按Broker分组发送。 - 发送和清理:将分组后的RecordBatch发送到对应的Broker,并在传输完成后清理Map数据。
linger.ms的影响
- 当
linger.ms
大于0时,Network Thread会等待,直到RecordBatch填满或超时,而不是立即发送,这有助于增加每个批次的消息数量,减少网络请求。
max.request.size的作用
max.request.size
限制了生产者单个发送请求的最大字节数,有助于控制请求的大小,避免过大的请求对网络和Broker造成压力。
为什么建议一个发送请求包含多条记录
- 减少数据包数量:包含多条记录的请求减少了数据包的数量,降低了网络开销。
- 减少元数据解析工作量:每个数据包都需要解析报头等元数据,多条记录的请求减少了这种解析的工作量。
- 提高Broker存储效率:在Broker端,
xx.index
文件会记录消息的偏移量和位置,包含多条记录的批次可以在xx.index
中记录更大范围的消息,提高索引效率。 - 加速消费者端数据搜索:消费者在搜索数据时,可以更快地定位到消息,因为
xx.index
中记录了更多消息的位置信息。
在Apache Kafka 2.4版本中引入的Sticky分区策略,旨在改善当消息的key为null时的分区效率和降低延迟。以下是对Sticky分区策略的详细解释和分析:
Sticky分区策略
- 目的:Sticky分区策略通过将连续的消息尽可能发送到同一个分区,减少批次的数量,从而提高批处理效果和降低延迟。
- 工作原理:当key为null时,Sticky分区策略会选择一个分区发送所有记录,直到该分区的RecordBatch已满。然后,它会随机选择一个新的分区,并“粘附”到这个新分区上,直到下一个RecordBatch也满了。
开启Sticky分区策略
- 在Kafka 2.4及以后版本,默认已经开启了Sticky分区策略。如果需要显式设置,可以在生产者配置中添加:
props.put(ProducerConfig.PARTITIONER_CLASS_CONFIG, StickyAssignor.class.getName());
Partitioner接口的变更
- Kafka 2.4在Partitioner接口中增加了
onNewBatch
方法,这个方法在创建新批次之前被调用,是实现Sticky分区策略的关键。
DefaultPartitioner的实现
DefaultPartitioner
实现了onNewBatch
方法,使用stickyPartitionCache
来管理粘性分区的逻辑。
KafkaProducer中的处理流程
- 尝试追加消息:首先尝试向之前的分区追加消息。
- 追加失败处理:如果追加失败(因为RecordBatch已满),则需要选择新的分区,并新建一个RecordBatch。
- 触发新分区:调用
onNewBatch()
方法,使用RoundRobin策略选择新的分区。 - 获取新分区:通过分区器获取新的分区值。
- 封装TopicPartition:使用新分区值封装成TopicPartition对象。
- 再次追加消息:向新分区的RecordBatch中追加之前失败的消息。
代码示例
- 以下是KafkaProducer中处理Sticky分区的伪代码示例:
// 尝试向以前的分区写入消息 RecordAccumulator.RecordAppendResult result = accumulator.append(tp, timestamp, serializedKey, serializedValue, headers, interceptCallback, remainingWaitMs, true, nowMs); // 如果append()未成功,说明RecordBatch已满,需要新建RecordBatch if (result.abortForNewBatch) { int prevPartition = partition; // 调用onNewBatch()方法,触发新的分区 partitioner.onNewBatch(record.topic(), cluster, prevPartition); // 获取新的分区值 partition = partition(record, serializedKey, serializedValue, cluster); // 封装TopicPartition tp = new TopicPartition(record.topic(), partition); // 再次运行append()方法,这时会新建一个RecordBatch并将写入失败的消息追加到新的RecordBatch中 result = accumulator.append(tp, timestamp, serializedKey, serializedValue, headers, interceptCallback, remainingWaitMs, false, nowMs); }
性能提升
- 使用Sticky分区策略可以减少Network Thread发送请求的数量,提高Kafka发送消息的吞吐量。对于key不为null的情况,Sticky分区策略和RoundRobin分区策略的性能差异不大,但在key为null的场景下,Sticky策略可以显著提高吞吐量。
通过引入Sticky分区策略,Kafka进一步提升了生产者的性能,特别是在处理大量无key消息时,能够更有效地利用网络资源和减少延迟。
3.误区:linger.ms=0
只产生单记录批次
- 常见误解:人们可能会认为,将
linger.ms
设置为 0 会导致生产者立即发送每条消息,从而产生大量单记录的批次。
实际行为
- 批量处理:即使
linger.ms=0
,如果多条消息几乎同时到达并且目标分区相同,生产者会将它们组合成批一起发送,以减少网络请求次数。 - 处理时间:生产者在发送消息前需要进行一些内部处理,如等待序列化完成、压缩数据等。在这些处理完成之前,生产者会暂时保留消息,形成批次。
- 网络请求:网络请求本身也有最小延迟,即使立即发送消息,也可能不如批量发送高效。生产者会利用这一点,尽可能地将消息组合成批。