bigywater.tech
Kafka 内部原理 · 06

Producer Buffer:消息发送前在客户端经历了什么

消息调用 send() 之后不会立刻上网络,而是先进入客户端本地的一块内存缓冲区(buffer.memory),按分区攒成批次,再由专门的 Sender 线程异步发出去。这次读取 Producer 的内部 Metrics,把缓冲区占用的变化过程看一遍,也验证了客户端会在消息进缓冲区之前就做大小检查。

验证:缓冲区占用量跟着未确认数据的堆积和释放而变化
从 Producer 自带的 Metrics 里读出了缓冲区可用字节数的变化:一开始满额 1,000,000 字节,
验证:超过 max.request.size 的消息,在真正进入缓冲区/发网络之前就被客户端本地拒绝
5KB 的消息发送成功,50KB 的消息在客户端本地

验证:缓冲区占用量跟着未确认数据的堆积和释放而变化

命令 · java KafkaProducer<String,String> producer = new KafkaProducer<>(props); // buffer.memory=1000000, linger.ms=30000 System.out.println("初始 buffer-available-bytes = " + metric(producer, "buffer-available-bytes")); for (int i = 0; i < 100; i++) { producer.send(new ProducerRecord<>("buffer-metrics-demo", "x".repeat(2000))); // 100 条 2KB } Thread.sleep(200); System.out.println("未 flush 时 buffer-available-bytes = " + metric(producer, "buffer-available-bytes")); producer.flush(); Thread.sleep(300); System.out.println("flush 完成后 buffer-available-bytes = " + metric(producer, "buffer-available-bytes"));
真实实测 · 本机执行输出
初始 buffer-available-bytes = 1000000.0
发送 100 条 2KB 消息(未flush)后 buffer-available-bytes = 983616.0
的 buffer-total-bytes = 1000000.0
 flush 完成后 buffer-available-bytes = 1000000.0

从 Producer 自带的 Metrics 里读出了缓冲区可用字节数的变化:一开始满额 1,000,000 字节,linger.ms 设置得很大(30 秒),消息堆积在缓冲区还没被发出去的时候,可用字节数掉到了 983,616(少了 16,384 字节,正好是一个默认 batch.size 的批次),说明缓冲区确实是"消息占着位置直到真正发出并确认";调用 flush() 强制把堆积的批次发出去、等到 Broker 确认后,可用字节数完全恢复回 1,000,000——缓冲区空间是跟着"未确认数据"走的,发出去并且拿到 ack 之后才会被释放。

验证:超过 max.request.size 的消息,在真正进入缓冲区/发网络之前就被客户端本地拒绝

命令 · java p.put(ProducerConfig.MAX_REQUEST_SIZE_CONFIG, 10000); // 单条请求最大 10KB producer.send(new ProducerRecord<>("max-request-demo", "x".repeat(5000))).get(); // 5KB,低于上限 producer.send(new ProducerRecord<>("max-request-demo", "x".repeat(50000))).get(); // 50KB,超过上限
真实实测 · 本机执行输出
--- 发一条 5KB 的消息(低于上限)---
发送成功
--- 发一条 50KB 的消息(超过 max.request.size=10000)---
在客户端本地就被拒绝,异常类型: org.apache.kafka.common.errors.RecordTooLargeException
异常信息: The message is 50087 bytes when serialized which is larger than 10000, which is the value of the max.request.size configuration.

5KB 的消息发送成功,50KB 的消息在客户端本地——序列化完但还没真正进入 Sender 的批次累加逻辑——就被 RecordTooLargeException 拦下来了,根本没有机会占用缓冲区空间或者发一次网络请求。这说明"消息发送前经历的步骤"里,大小校验是排在缓冲区分配之前的一道关卡。

结论

Producer Buffer 做的事情是:先校验消息大小(超限直接本地拒绝,不占缓冲区、不碰网络),通过校验的消息占用缓冲区空间,按分区攒成批次,等发出并拿到 Broker 确认后才释放空间。这次额外尝试过用很小的 buffer.memory 加大量大消息去触发的缓冲区耗尽阻塞,但在本机回环网络下没有复现出来——发送速率始终追不上网络和 Broker 确认的往返速度,缓冲区一直有空间被及时腾出来,这本身也是一个、如实记录的边界,不是没测出来就假装测出了一个"阻塞"的例子。

分步演示:一条消息在真正发出网络之前,经历了哪几道关卡

每一步的数字都是本文上两节读取 Producer Metrics 和触发异常得到的原始结果。

缓冲区占用随发送/flush起伏,超限的消息连缓冲区的门都进不去
当前这一步的动作

    参考与说明

    • 本文数据来自的 Java KafkaProducer 客户端,直接读取 producer.metrics() 里的 buffer-available-bytes/buffer-total-bytes 指标,不是估算或者官方文档里的默认值。
    • 没有涉及:触发 buffer.memory 耗尽导致 send() 阻塞直至 max.block.ms 超时的完整异常链——这次在本地回环网络下多次尝试(2000 条 16KB 消息、32KB 缓冲区)都没能复现,缓冲区释放速度始终快于耗尽速度;这类背压场景更适合在存在网络延迟或 Broker 负载饱和的环境下测试,留给后面用限速代理模拟。
    • 和《Kafka RecordBatch:消息为什么不是一条一条写入》一起看:那篇讲了批次攒起来之后带来的吞吐收益,这篇讲了批次在真正发出之前,客户端内存里经历的校验和占用/释放过程。
    京ICP备2026067502号-1京公网安备11010502063487号