
为什么要单独讲 batch 发送条件前面讲 Sender 线程时提到Sender 每轮调用accumulator.ready()获取有可发送批次的 Broker 集合然后drain()拉取。但ready()到底怎么判断一个 batch “可以发了”这个判断逻辑直接决定了延迟和吞吐的平衡。很多人对 batch 发送有误解以为调了batch.size32KB就一定会攒到 32KB 才发以为linger.ms0就是每条立即发。这篇把这些误解全部澄清。01 ProducerBatch 的状态机一个 batch 从创建到完成经历 5 个状态EMPTY刚创建还没有任何消息。从 BufferPool 拿到 16KB 内存块构造 MemoryRecordsBuilder。FILLING业务线程正在 append。这是 batch 停留时间最长的状态也是攒批发生的阶段。READY满足发送条件可以被 Sender drain。注意 READY 只是可以发不是已经发。DRAINEDSender 已经把它从 Deque 里取出来构建成 ProduceRequest 发出去了等 Broker ACK。EXPIRED超过 delivery.timeout.ms 还没成功放弃发送回调异常。状态转换的核心是 FILLING → READY这一步有 5 个触发条件任一满足即转换。02 ready() 的 5 个触发条件Sender 每轮调用accumulator.ready(nowMs)内部遍历每个分区的 Deque取队头 batch 调用batch.ready(nowMs, lingerMs)判断。判断逻辑是一个短路或publicbooleanready(longnowMs,longlingerMs){returnisFull()||// 条件1写满(nowMs-lastAppendTimelingerMs)||// 条件2linger超时flushInProgress||// 条件3flush中closed||// 条件4已关闭metadataExpired;// 条件5元数据过期}条件1写满 batch.size。isFull()判断estimatedBytesWritten batch.size。注意是 estimated估算值包含消息体、记录头、CRC、压缩开销不是纯消息体字节数。所以实际写满时可能略大于 batch.size。条件2linger.ms 超时。nowMs - lastAppendTime lingerMs。距离最后一次 append 的时间超过 linger.ms。默认 linger.ms0所以这个条件在每次 append 后立即满足——但这只是说 batch 变 READY 了真正发送还要等 Sender 下一轮。条件3flushInProgress。业务线程调用producer.flush()时设置这个标志所有 batch 立即变 READY绕过 batch.size 和 linger.ms。条件4closed。producer.close()时设置剩余数据强制发送。条件5metadataExpired。分区 Leader 变化如 Broker 宕机触发重选举旧 batch 发往的 Leader 可能已经不是 Leader 了需要重新路由。03 Sender 检查 ready 的完整链路从 Sender 线程的 runOnce 到真正发出去完整链路是四步runOnce(nowMs)获取当前时间戳。ready(nowMs)遍历所有分区 Deque检查队头 batch 是否 ready。返回readyNodes有 ready batch 的 Broker 集合和nextReadyCheckDelayMs下一个 batch 多久后会 ready用于计算 poll 超时避免空转。drain(readyNodes)只遍历 readyNodes 里的 Broker按节点拉取 ready batch。检查in-flight max.in.flight累计大小 ≤max.request.size。sendProduceRequests每个 Broker 的批次列表构建一个 ProduceRequest调用 NetworkClient.send()。关键细节ready() 只检查队头 batch。因为 Deque 是 FIFO队头最老如果队头都不 ready后面的更不 ready。队头 ready 了才会继续检查队头下一个可能多个 batch 同时 ready。04 常见的 5 个坑坑1linger.ms0 就以为是每条立即发。linger.ms0 只表示 ready 条件立即满足但真正发送要等 Sender 下一轮 runOnce。Sender 轮询间隔通常 1-5ms取决于 poll 超时所以实际延迟是几 ms 不是 0。要真正低延迟用flush()主动触发但会牺牲吞吐。坑2只调 batch.size 不调 linger.ms。小流量场景 batch 永远写不满linger.ms0 导致每条都发batch.size 白调。高吞吐场景必须配合batch.size32KB linger.ms5-10ms。batch.size 是攒到多大linger.ms 是最多等多久哪个先到触发哪个。坑3ready 了就以为一定发出去了。ready 只是可以被 drain还要等 Sender 下一轮 该节点 in-flight max.in.flight。如果 Broker 慢导致 in-flight 满了ready 的 batch 会继续等可能超过 delivery.timeout.ms 变成 EXPIRED。监控record-send-rate和request-latency-avgin-flight 满了说明 Broker 是瓶颈。坑4flush() 频繁调用导致攒批失效。flush() 把所有 batch 标记 ready绕过 batch.size 和 linger.ms。每条消息后都 flush 退化成单条发送吞吐暴跌 10 倍以上。flush 只用于必须立即发的场景关闭前、事务提交前不要每条都调。坑5batch.size 设太大导致单条消息塞不进去。batch.size 是单个批次上限但单条消息不能超过 batch.size——否则会创建一个只含这一条消息的大 batch。如果单条消息 max.request.size直接抛 RecordTooLargeException。大消息场景batch.size ≥ 单条消息大小max.request.size ≥ batch.sizeBroker 端 message.max.bytes 同步调大。写在最后batch 发送条件的本质是一个**或逻辑**写满、超时、flush、close、元数据过期任一满足即 ready。理解了这 5 个条件就能解释很多为什么我的消息没立即发出去的问题。调优的核心是在 batch.size吞吐和 linger.ms延迟之间找平衡高吞吐场景两个都调大低延迟场景 linger.ms0 小 batch.size不要只调一个。