KafkaProducer 源码深度剖析:batch 发送的条件

为什么要单独讲 batch 发送条件

前面讲 Sender 线程时提到,Sender 每轮调用 accumulator.ready() 获取有可发送批次的 Broker 集合,然后 drain() 拉取。但 ready() 到底怎么判断一个 batch "可以发了"?这个判断逻辑直接决定了延迟和吞吐的平衡。

很多人对 batch 发送有误解:以为调了 batch.size=32KB 就一定会攒到 32KB 才发,以为 linger.ms=0 就是每条立即发。这篇把这些误解全部澄清。

01 ProducerBatch 的状态机

一个 batch 从创建到完成,经历 5 个状态:

  • EMPTY:刚创建,还没有任何消息。从 BufferPool 拿到 16KB 内存块,构造 MemoryRecordsBuilder。
  • FILLING:业务线程正在 append。这是 batch 停留时间最长的状态,也是攒批发生的阶段。
  • READY:满足发送条件,可以被 Sender drain。注意 READY 只是"可以发",不是"已经发"。
  • DRAINED:Sender 已经把它从 Deque 里取出来,构建成 ProduceRequest 发出去了,等 Broker ACK。
  • EXPIRED:超过 delivery.timeout.ms 还没成功,放弃发送,回调异常。

状态转换的核心是 FILLING → READY,这一步有 5 个触发条件,任一满足即转换。

02 ready() 的 5 个触发条件

Sender 每轮调用 accumulator.ready(nowMs),内部遍历每个分区的 Deque,取队头 batch 调用 batch.ready(nowMs, lingerMs) 判断。判断逻辑是一个短路或:

java 复制代码
public boolean ready(long nowMs, long lingerMs) {
    return isFull() ||                // 条件1:写满
           (nowMs - lastAppendTime >= lingerMs) ||  // 条件2:linger超时
           flushInProgress ||         // 条件3:flush中
           closed ||                  // 条件4:已关闭
           metadataExpired;           // 条件5:元数据过期
}

条件1:写满 batch.size。 isFull() 判断 estimatedBytesWritten >= batch.size。注意是 estimated(估算值),包含消息体、记录头、CRC、压缩开销,不是纯消息体字节数。所以实际写满时可能略大于 batch.size。

条件2:linger.ms 超时。 nowMs - lastAppendTime >= lingerMs。距离最后一次 append 的时间超过 linger.ms。默认 linger.ms=0,所以这个条件在每次 append 后立即满足------但这只是说 batch 变 READY 了,真正发送还要等 Sender 下一轮。

条件3:flushInProgress。 业务线程调用 producer.flush() 时设置这个标志,所有 batch 立即变 READY,绕过 batch.size 和 linger.ms。

条件4:closed。 producer.close() 时设置,剩余数据强制发送。

条件5:metadataExpired。 分区 Leader 变化(如 Broker 宕机触发重选举),旧 batch 发往的 Leader 可能已经不是 Leader 了,需要重新路由。

03 Sender 检查 ready 的完整链路

从 Sender 线程的 runOnce 到真正发出去,完整链路是四步:

  1. runOnce(nowMs):获取当前时间戳。
  2. ready(nowMs) :遍历所有分区 Deque,检查队头 batch 是否 ready。返回 readyNodes(有 ready batch 的 Broker 集合)和 nextReadyCheckDelayMs(下一个 batch 多久后会 ready,用于计算 poll 超时避免空转)。
  3. drain(readyNodes) :只遍历 readyNodes 里的 Broker,按节点拉取 ready batch。检查 in-flight < max.in.flight,累计大小 ≤ max.request.size。
  4. sendProduceRequests:每个 Broker 的批次列表构建一个 ProduceRequest,调用 NetworkClient.send()。

关键细节:ready() 只检查队头 batch。 因为 Deque 是 FIFO,队头最老,如果队头都不 ready,后面的更不 ready。队头 ready 了才会继续检查队头下一个(可能多个 batch 同时 ready)。

04 常见的 5 个坑

坑1:linger.ms=0 就以为是"每条立即发"。 linger.ms=0 只表示 ready 条件立即满足,但真正发送要等 Sender 下一轮 runOnce。Sender 轮询间隔通常 1-5ms(取决于 poll 超时),所以实际延迟是几 ms 不是 0。要真正低延迟,用 flush() 主动触发,但会牺牲吞吐。

坑2:只调 batch.size 不调 linger.ms。 小流量场景 batch 永远写不满,linger.ms=0 导致每条都发,batch.size 白调。高吞吐场景必须配合:batch.size=32KB + linger.ms=5-10ms。batch.size 是"攒到多大",linger.ms 是"最多等多久",哪个先到触发哪个。

坑3:ready 了就以为一定发出去了。 ready 只是"可以被 drain",还要等 Sender 下一轮 + 该节点 in-flight < max.in.flight。如果 Broker 慢导致 in-flight 满了,ready 的 batch 会继续等,可能超过 delivery.timeout.ms 变成 EXPIRED。监控 record-send-rate 和 request-latency-avg,in-flight 满了说明 Broker 是瓶颈。

坑4:flush() 频繁调用导致攒批失效。 flush() 把所有 batch 标记 ready,绕过 batch.size 和 linger.ms。每条消息后都 flush = 退化成单条发送,吞吐暴跌 10 倍以上。flush 只用于"必须立即发"的场景(关闭前、事务提交前),不要每条都调。

坑5:batch.size 设太大导致单条消息塞不进去。 batch.size 是单个批次上限,但单条消息不能超过 batch.size------否则会创建一个只含这一条消息的大 batch。如果单条消息 > max.request.size,直接抛 RecordTooLargeException。大消息场景:batch.size ≥ 单条消息大小,max.request.size ≥ batch.size,Broker 端 message.max.bytes 同步调大。

写在最后

batch 发送条件的本质是一个**"或"逻辑**:写满、超时、flush、close、元数据过期,任一满足即 ready。理解了这 5 个条件,就能解释很多"为什么我的消息没立即发出去"的问题。

调优的核心是在 batch.size(吞吐)和 linger.ms(延迟)之间找平衡:高吞吐场景两个都调大,低延迟场景 linger.ms=0 + 小 batch.size,不要只调一个。

相关推荐
Data-Miner12 小时前
需要做财务数据分析,有什么好用的工具?从准备到出报告的实操指南
大数据·数据库·数据分析
阿里云大数据AI技术13 小时前
StarRocks × Paimon C++:加速湖上数据分析
大数据·人工智能
用户36105886261214 小时前
Flink 配置深度剖析:从配置加载到参数调优的完整指南
大数据·flink
ha_lydms14 小时前
Window Functions(窗口函数)
大数据·数据仓库·hadoop·spark·数据处理·dataworks·maxcompute
2601_9520477914 小时前
委外入库单同步:旺店通到金蝶云星空的实战配置
大数据·钉钉·制造
阿里云大数据AI技术14 小时前
【云栖实录】一切数据,皆可计算:MaxCompute 升级为AI Native全模态计算服务
大数据·人工智能·agent
珠海西格电力15 小时前
物联网(IoT)在零碳园区中的关键作用:万物互联与数据采集
大数据·人工智能·物联网·安全·系统架构·能源
AI产品测评官15 小时前
2026企业AI招聘选型指南:四类技术路线与硬科技场景适配方案
大数据·人工智能·科技·求职招聘·ai-native
SelectDB技术团队15 小时前
Snowflake 的开源替代怎么选:能力对照、三条部署路径与迁移执行
大数据·数据库·云原生·开源·snowflake·apache doris