KafkaProducer 源码深度剖析:RecordAccumulator 封装消息流程

为什么需要 RecordAccumulator

前面几篇讲了 send() 怎么序列化、怎么选分区。但选完分区后,消息不是直接发出去的——那样每条消息一次网络请求,吞吐会惨不忍睹。

Kafka Producer 的核心优化就是攒批:同一分区的消息先在内存里攒成一个批次(默认 16KB),攒满了(或 linger.ms 到了)再一次性发给 Broker。RecordAccumulator 就是做这件事的。

它解决两个问题:

  • 攒批:多条消息合并成一个网络请求,减少 IO 次数
  • 路由:按分区组织,Sender 拉取时知道哪些分区有 ready 批次

01 数据结构:每个分区一个双端队列

RecordAccumulator 内部核心是一个ConcurrentMap<TopicPartition, Deque<ProducerBatch>>。每个分区对应一个 Deque,Deque 里是这个分区的所有批次。

为什么是 Deque 而不是普通 Queue?因为读写两端不同:

  • 写端(right):业务线程把新批次加到队尾,正在攒批
  • 读端(left):Sender 线程从队头拉 ready 批次发出去
  • 失败重传:如果一个批次发出去但 ACK 失败,它要重新入队——插到队头,保证尽快重试

Deque 正好支持两端操作。ConcurrentMap 保证不同分区的 Deque 之间并发安全——两个分区的业务线程同时 append,不用互相等。

除了 batches 这个 Map,还有一个incomplete: Set<ProducerBatch>。所有已经发出去但还没收到 ACK 的批次都在这个集合里。为什么要这个 Set?因为如果没有强引用,这些批次可能被 GC 回收——但网络上还飘着它们的字节,ACK 回来时找不到对应对象就炸了。

02 append() 流程:找尾批次,写不进去就新建

业务线程调accumulator.append()时,流程是一条决策链:

第一步:拿到该分区的 Deque。没有就新建一个。

第二步:看队尾最后一个批次。如果队尾批次还没满、没被 abort,就尝试往里写。

第三步:尝试写入。MemoryRecordsBuilder 把 key、value、timestamp 顺序写入 ByteBuffer。写得进去就返回。

第四步:写不进去(满了),分配新批次。从 BufferPool 里拿一块 ByteBuffer(默认 batch.size=16KB),新建 ProducerBatch,加到 Deque 队尾,把消息写进去。

第五步:内存不够。如果 BufferPool 里没有可用的 ByteBuffer,就阻塞等待,最多等 max.block.ms(默认 60 秒)。等不到就抛异常。

这里有个容易踩的坑:linger.ms=0 不代表消息立刻发出去。它只是说"攒满 batch.size 就发,不用等时间"。如果消息量很小,一个批次永远攒不满 16KB,就会一直等。线上低流量场景调大 linger.ms(比如 5ms)比调小 batch.size 更有效——让小批次攒得快一点,而不是把每个批次拆得更小。

03 ByteBuffer 布局:一条消息在内存里长什么样

一个 ProducerBatch 底层就是一块 ByteBuffer。写入格式是 RecordBatch v2:

[Batch Header(公共头)] baseOffset | length | magic(=2) | crc attributes | lastOffsetDelta | baseTimestamp | maxTimestamp [Record 1] length | attributes | timestampDelta | keyLength | key | valueLength | value [Record 2] ... [Record N] ... [剩余空闲空间]

几个关键设计:

为什么用 timestampDelta 而不是绝对时间戳?同一批次的消息时间戳相差很小(默认在同一毫秒或几毫秒内),存差值(varint 变长整数)比存 8 字节绝对时间省得多。baseTimestamp 存在 Batch Header 里只存一次。

为什么在 batch 级别压缩而不是单条压缩?同一个 batch 里的消息通常相似(同一个用户、同一个事件类型),batch 级别压缩率远高于逐条压缩。gzip 能压到原来的 1/5,zstd 更好。

单条消息超过 batch.size 怎么办?直接分配一块单独的 ByteBuffer,大小就是这条消息的大小(受 max.request.size 限制,默认 1MB)。它自己成一个 batch,不需要和其他消息凑。

04 内存管理:BufferPool 和背压

RecordAccumulator 不是无限内存。总大小由 buffer.memory 控制(默认 32MB)。所有批次共用这 32MB。

BufferPool 管理这些内存:分配 ByteBuffer、回收 ACK 后的 ByteBuffer。当 32MB 用完了,新的 append 请求就阻塞——这就是背压。

线上最常见的内存问题:

  • 消息太大:单条消息 10MB,超过 buffer.memory,直接 OOM。调 max.request.size 和 buffer.memory。
  • 发送太慢:Broker 响应慢,批次都在 incomplete 集合里不释放,32MB 用完,业务线程阻塞 60 秒后抛异常。这是 Broker 侧问题,不是 Producer 配置问题。
  • 分区太多:每个分区都攒一个批次,32MB 被切成 100 个批次,每个批次只有 320KB——比 batch.size=16KB 大得多但利用率低。

经验值:buffer.memory 至少是 batch.size × 活跃分区数 × 2。比如 50 个分区,每个 16KB,32MB 够用;如果 500 个分区,就要调到 160MB。

05 写在最后

RecordAccumulator 看起来就是个 Map,但它是 Producer 性能的核心:攒批减少网络请求,Deque 支持失败重传,BufferPool 控制内存上限。理解了 append() 流程,就理解了为什么调 batch.size、linger.ms、buffer.memory 能影响吞吐——它们分别控制"批次多大"、“等多久”、“总共多少内存”。

相关新闻

HTML5视频播放实战:从video标签到编码兼容与性能优化

HTML5视频播放实战:从video标签到编码兼容与性能优化

1. 先搞清楚&#xff1a;为什么视频不能直接塞进网页1.1 从GIF到HTML5视频的演进前几年还经常能看到有人用GIF做演示动效&#xff0c;一个十几秒的录屏转成GIF之后动辄几十MB&#xff0c;加载起来页面卡得不行。GIF本质上是图像格式&#xff0c;不是为连续画面临时设计的&#…

2026/11/13 15:50:05 阅读更多 →
朴素贝叶斯与SVM同场竞技:垃圾邮件识别Python工程全解析

朴素贝叶斯与SVM同场竞技:垃圾邮件识别Python工程全解析

简介&#xff1a;这是一个基于朴素贝叶斯与支持向量机&#xff08;SVM&#xff09;的垃圾邮件识别系统&#xff0c;适合机器学习入门者、毕业设计及分类项目实战人群。项目通过对正常邮件和垃圾邮件进行语料训练&#xff0c;完成词频统计与模型对比&#xff0c;实现邮件自动分类…

2026/11/14 10:56:39 阅读更多 →
Ollama+通义千问本地部署大模型:三步实操与避坑指南

Ollama+通义千问本地部署大模型:三步实操与避坑指南

这两年AI大模型算是彻底走进普通开发者的视野了。不过很多人对“本地部署”这件事一直有种错觉&#xff0c;觉得要么需要一台几万块的服务器&#xff0c;要么得折腾CUDA、Docker、各种依赖环境&#xff0c;最后把自己劝退。直到我试了Ollama加通义千问这条组合路线&#xff0c;…

2026/11/14 7:22:34 阅读更多 →

最新新闻

Electron桌面应用开发实战:从环境搭建到打包发布全流程

Electron桌面应用开发实战:从环境搭建到打包发布全流程

做前端这些年&#xff0c;我大部分时间都在跟浏览器标签页打交道&#xff0c;直到第一次把桌面应用跑起来&#xff0c;才发现原来自己一直被限制在地址栏和刷新按钮围起来的框框里。Electron这个词&#xff0c;最开始是在某段时间频繁出现在同事的讨论里&#xff0c;一个用HTML…

2026/11/15 5:13:35 阅读更多 →
滑盖智能锁T109深度解析:结构原理、安装调试与故障排查

滑盖智能锁T109深度解析:结构原理、安装调试与故障排查

简介&#xff1a;亚太天能T109滑盖智能锁产品说明书是一份PDF格式的官方技术文档&#xff0c;主要面向智能锁安装人员、终端用户及售后维护者&#xff0c;帮助快速掌握产品安装、配置与常见故障处理。全包仅1个PDF文件&#xff0c;大小4.37MB&#xff0c;内容结构清晰&#xff…

2026/11/15 5:13:35 阅读更多 →
PMSM弱磁控制原理与Simulink建模:高速AGV驱动系统设计指南

PMSM弱磁控制原理与Simulink建模:高速AGV驱动系统设计指南

上个月我在调试一台高速AGV的驱动系统&#xff0c;整车要求空载跑到8m/s&#xff0c;电机转速已经拉到额定点&#xff0c;驱动器的输出电压也顶到了母线电压极限附近。再往上踩速度指令&#xff0c;转速纹丝不动&#xff0c;三相电流反而开始震荡。那会儿我的第一反应就是&…

2026/11/15 5:13:35 阅读更多 →
C++核心知识点:从内存管理到STL的实战指南

C++核心知识点:从内存管理到STL的实战指南

C 这门语言我前前后后折腾了十几年&#xff0c;用它写过嵌入式驱动、做过游戏服务端&#xff0c;也搭过基础组件库。每次看到新人一头扎进"C 核心知识点"这个搜索词&#xff0c;我其实挺感慨的——这门语言的难点从来不是语法多&#xff0c;而是知识点之间的耦合太深…

2026/11/15 3:37:41 阅读更多 →
T109滑盖智能锁怎么装?安装调试与故障排查全攻略

T109滑盖智能锁怎么装?安装调试与故障排查全攻略

简介&#xff1a;亚太天能T109滑盖智能锁产品说明书以PDF格式发布&#xff0c;适合智能锁用户、安装人员与售后维修人员阅读。内容围绕T109系列指纹密码刷卡锁展开&#xff0c;先介绍前面板、后面板、锁体、机械钥匙、电池盒盖等部件名称与功能&#xff0c;再给出完整安装步骤&…

2026/11/15 4:18:38 阅读更多 →
LLM知识降熵:从信息垄断到理解权下放

LLM知识降熵:从信息垄断到理解权下放

你有没有过这种经历&#xff1a;翻开源码注释、论文摘要或者一份专业报告&#xff0c;每个字都认识&#xff0c;连在一起却像天书。想找人聊聊&#xff0c;身边要么没人懂&#xff0c;要么懂的懒得跟你解释。后来我试着把同一段内容丢给一个大语言模型&#xff0c;几秒钟之后&a…

2026/11/15 5:13:35 阅读更多 →

日新闻

周新闻

月新闻