4. 批量发送与吞吐量优化:batch.size与linger.ms的调优策略

好,咱们进入第四章。这一章聊的是吞吐量和延迟的平衡问题。说白了,就是怎么让Kafka生产者发得快,但又不能把消息憋太久。

很多新手上来就调batch.size和linger.ms,结果要么吞吐上不去,要么延迟高得离谱。我刚开始搞Kafka时也踩过这个坑——调大batch.size后吞吐确实上来了,但业务方投诉说消息延迟从5ms飙到了200ms。嗯,这就是典型的“顾此失彼”。

4.1 批量发送的核心机制

先搞清楚一件事:Kafka生产者不是来一条发一条的。它会先把消息攒起来,攒够一批再一起发。这个“攒”的过程,就是批量发送。

为什么要这么做?你想想看,每条消息都单独发一次网络请求,光TCP握手和协议开销就能把吞吐拖死。批量发送能显著减少网络往返次数,提升吞吐量。

核心公式:吞吐量 ≈ 单次请求消息数 × 每秒请求数。批量越大,单次请求的消息数越多,吞吐越高。

但这里有个矛盾:攒得越多,等待时间就越长。这就是延迟和吞吐的天然冲突。

4.2 batch.size:攒多少才算够?

batch.size控制的是单个批次的最大字节数。默认值是16KB。注意,它不是指“必须攒满16KB才发”,而是“批次大小上限”。

我个人习惯把这个参数和消息的平均大小挂钩。举个例子:

  • 如果每条消息1KB,16KB的batch能装16条,还行。
  • 如果每条消息100字节,16KB能装160条,绰绰有余。
  • 如果每条消息10KB,16KB只能装1条,那批量效果基本为零。

我在项目中遇到过这样一个场景:业务方上报的日志每条只有200字节左右,但QPS高达10万。默认16KB的batch.size完全够用,因为很快就能攒满。但另一个场景是推送图片元数据,每条消息8-12KB,16KB的batch经常只装1条就发了,吞吐上不去。后来我把batch.size调到了64KB,吞吐提升了将近3倍。

调优建议:先算一下消息平均大小,然后让batch.size能装下32-64条消息。比如消息平均1KB,batch.size设成32KB-64KB比较合理。

4.3 linger.ms:等多久才算合适?

linger.ms是“最多等多久”。默认值是0,意思是“有消息就发,不等”。

等等,这跟批量发送不是矛盾吗?

其实不矛盾。当linger.ms=0时,生产者确实会立即发送,但发送的是当前批次里的消息。如果此时批次还没满,那就发一个“半满”的批次。这样批量效果就大打折扣。

我建议这样理解:

  • batch.size决定“攒多少”
  • linger.ms决定“最多等多久”
  • 两者是“或”的关系——谁先满足就触发发送

举个例子:batch.size=32KB,linger.ms=10ms。如果10ms内攒满了32KB,立即发;如果10ms到了还没攒满,也发,发的是当前已攒的数据。

注意:linger.ms设得太大,延迟会线性增长。我曾经见过有人设成500ms,结果吞吐是上去了,但业务方投诉说消息延迟半分钟。这显然不行。

4.4 延迟与吞吐的平衡策略

怎么平衡?没有万能公式,但有套路。我一般按以下步骤来:

  1. 先定延迟目标。业务允许的最大延迟是多少?比如100ms、500ms?
  2. 再算吞吐目标。每秒需要发送多少条消息?每条多大?
  3. 然后调linger.ms。一般设成延迟目标的10%-20%。比如延迟目标100ms,linger.ms设10-20ms。
  4. 最后调batch.size。保证在linger.ms时间内能攒够一个合理的批次。

下面这张图能帮你理清思路:

批量发送调优决策流程 业务需求输入 延迟敏感型 吞吐优先型 策略:低延迟 linger.ms = 0-5ms batch.size = 16KB-32KB 牺牲部分吞吐保延迟 策略:高吞吐 linger.ms = 10-50ms batch.size = 64KB-512KB 接受一定延迟换吞吐 持续监控 & 动态调整

4.5 实战配置示例

光说理论不行,咱们直接上配置。假设有两个典型场景:

参数 场景A:实时风控 场景B:日志采集
延迟要求 < 50ms < 5s
消息大小 ~500字节 ~2KB
QPS 5000 50000
batch.size 16KB 128KB
linger.ms 5ms 50ms
compression.type none snappy

场景A是实时风控,延迟敏感。我设了5ms的linger.ms,16KB的batch.size。因为消息小、QPS高,5ms内基本能攒满一个批次,延迟可控。

场景B是日志采集,吞吐优先。我设了50ms的linger.ms,128KB的batch.size,还开了snappy压缩。这样单次请求能发更多消息,吞吐量拉满。

小技巧:开了压缩后,batch.size可以适当设大一些。因为压缩后的数据体积更小,同样的batch.size能装更多原始消息。

4.6 避坑指南

最后分享几个我踩过的坑:

  • 坑一:把batch.size设得特别大,比如1MB。结果内存压力飙升,GC频繁,反而拖低了吞吐。建议不要超过512KB。
  • 坑二:linger.ms设成0,然后抱怨批量没效果。嗯,你都没给批次攒的时间,当然没效果。
  • 坑三:只调batch.size不调linger.ms。如果消息量不大,batch永远攒不满,等于没调。
  • 坑四:忽略max.request.size。这个参数默认1MB,如果batch.size超过它,会报错。记得同步调整。

我曾经在一个日志采集项目里,把batch.size从16KB调到256KB,但忘了调linger.ms(默认0)。结果吞吐一点没变,因为消息一来就发了,根本攒不成大批次。后来把linger.ms设成20ms,吞吐直接翻倍。