高性能
RocketMQ 高性能的本质,是它把存储引擎做成了"以磁盘为持久化层、以 PageCache 为性能层"的架构,让磁盘消息也能接近内存的访问速度。下面分几个关键点讲清楚"体现在哪"和"为什么"。
一、顺序写磁盘------写入高性能的根基
体现:单机写入 TPS 可达 10 万级(异步刷盘),机械盘上也能达到万级;相比 Kafka 早期方案不遑多让。
为什么 :RocketMQ 把所有 Topic 的消息统一顺序追加到一个 CommitLog 文件(而非 Kafka 的按 Topic/Partition 分多文件)。磁盘顺序写不寻道,磁头连续移动,吞吐可达 100MB/s(机械盘)到 500MB/s+(SSD)。顺序写甚至比内存随机访问还快------这是磁盘存储能扛高并发的前提。
固定 1G 文件 + 预分配,避免运行时文件扩容和元数据抖动;文件写满再切下一个,本质就是把磁盘当"环形追加日志"用。
二、零拷贝------消费端高性能的关键
体现:消费端单机吞吐可达数十万 TPS,CPU 占用却不高,长链路下延迟稳定在毫秒级。
为什么 :传统读文件再发网络要 4 次拷贝(磁盘→内核 buffer→用户态→socket buffer→网卡),外加 4 次上下文切换。RocketMQ 消费时用 FileChannel.transferTo(底层 sendfile) ,数据直接从 PageCache 拷贝到 socket,全程不经用户态,拷贝次数减到 2 次、上下文切换减到 2 次。这是消费吞吐能吊打"读文件到 JVM 堆再发出去"方案的核心原因。
写入端则用 mmap(MappedByteBuffer) ,把 CommitLog 文件映射到用户态内存地址,Java 直接写 PageCache,省掉 write 系统调用的用户态→内核态拷贝。
三、PageCache 作为"性能层"------读写都受益
体现:消费者滞后 broker 不远时,几乎不读盘;生产速率波动时,PageCache 起到削峰缓冲作用。
为什么 :写入直接进 PageCache,OS 的 write-behind 后台异步刷盘;消费时若数据还在 PageCache(没被刷下),直接命中,等于读内存。OS 还会自动 read-ahead 预读,把后续要消费的块提前加载。所以 RocketMQ 实际上把 PageCache 当作"消息的内存层",磁盘只是兜底持久化层。
这也是为什么异步刷盘模式性能远超同步刷盘------前者写入 PageCache 立即返回,后者必须等 fsync 落盘。
四、数据与索引分离------消费定位高性能
体现:按 queue 或 key 查询消息非常快,即使 CommitLog 巨大(几百 G)依然毫秒级定位。
为什么 :RocketMQ 把消息体(CommitLog,顺序追加、巨大)和索引(ConsumeQueue,按 queue+offset 组织、稀疏、每条 20 字节)物理分离。ConsumeQueue 体积极小,几乎全部驻留 PageCache。消费时先查 ConsumeQueue 拿到 commitlog offset,再回查 CommitLog------两步都是内存级访问,且 ConsumeQueue 顺序友好、Cache 命中率高。
对比"一个 Topic 一个文件"的方案,RocketMQ 这种统一 CommitLog + 多 ConsumeQueue 索引的设计,写入是纯顺序(不分文件),消费是索引驱动的随机读但有 PageCache 兜底,两全其美。
五、文件预热------避免运行时缺页抖动
体现:新文件启用瞬间不会出现大量缺页中断导致的延迟尖刺。
为什么 :RocketMQ 对新建的 CommitLog 文件做 mmap + 每页写入 1 字节,主动触发缺页中断,把页提前加载到内存并建立页表。否则运行时首次写每页都会触发缺页中断,批量消息写入时会产生明显延迟抖动。这是一个容易被忽视但对稳定延迟很重要的细节。
六、批量 + 异步------放大单次操作的有效负载
体现:网络 IO 次数少,单次系统调用处理多条消息。
为什么:Producer 端聚合多条消息一次发送;Broker 收到批量消息批量落盘;Consumer 端 pull 也是批量。一次磁盘顺序写、一次网络 IO 处理 N 条消息,摊薄了固定开销(系统调用、锁、网络 RTT)。
七、锁竞争优化------多线程并发写入不退化
体现:多 Producer 并发写入时,TPS 不线性退化。
为什么 :CommitLog 追加写入用自旋锁(SpinLock)或可配的 ReentrantLock,临界区极短(仅一次追加 + 一次索引更新)。ConsumeQueue 由专门的 ReputMessageService 单线程异步构建,无锁。整体把锁的粒度压到最小,避免"高并发下变串行"。
总结:为什么高性能
一句话概括------RocketMQ 用"顺序追加 + 零拷贝 + PageCache 兜底 + 索引分离"四件套,把磁盘存储做成了接近内存的访问模型。磁盘只承担持久化兜底,热路径几乎全在内存(PageCache)里跑;再叠加批量、异步、锁优化,把单机吞吐推到极限。
取舍是:这套设计对 PageCache 依赖很重,当消费者滞后太多、数据已被刷出 PageCache 时,会退化成随机读磁盘,性能急剧下降。所以实践中要控制 CommitLog 体积、给 PageCache 留足够内存,或用"消费者贴近生产端"的消费模式,才能持续享受这套架构的性能红利。
高并发
先把"高并发"和"高性能"分开------这俩经常混着说,但侧重点不同,分清楚才能看懂 RocketMQ 高并发到底强在哪。
先分清:高并发 ≠ 高性能
- 高性能:厨师做饭做得快,一分钟出 10 道菜------侧重"单点处理快"。
- 高并发:餐厅同时能坐下 1000 桌客人点单不崩------侧重"同时能接多少路请求"。
厨师再快,一次也只能炒一锅,如果同时来 1000 桌订单,他接不下还是会崩。高并发要解决的是"同时来了海量请求,系统还能稳住接住"。RocketMQ 的高并发就体现在五个层面,下面逐个说。
一、连接层:单 Broker 同时撑数万长连接
生产环境单台 Broker 可以同时维持数万个 Producer 和 Consumer 的长连接,每秒接收数万次请求不抖动。这是高并发最直观的指标------客户端再多,连得上、接得住。
为什么扛得住 :基于 Netty 的 NIO + Reactor 线程模型 。一个 Selector 多路复用器用一个线程监听几千个连接的事件,哪个连接有数据可读就处理哪个,不阻塞、不为空闲连接占用线程。对比"一个连接一个线程"的阻塞 IO 模型(一个连接占一个线程,1 万连接就要 1 万线程,线程切换直接打爆 CPU),NIO 用几十个线程就能稳稳撑几万连接。这是网络层扛高并发的基础。
二、Topic 数:单 Broker 撑数万 Topic 不退化(杀手锏)
这是 RocketMQ 相对 Kafka 最强的高并发优势。单 Broker 可以承载几万甚至十万级 Topic,写入 TPS 基本不掉。
为什么这是高并发?因为在真实业务里,一个公司会有几千个微服务、几万个 Topic(订单、支付、库存、风控、埋点、日志......),每个 Topic 都要并发收消息。Kafka 的设计是一个 Topic-Partition 一个文件目录,Topic 一多,写入就变成在几千个文件之间随机写,磁盘随机 IO 严重退化,TPS 断崖式下降。所以 Kafka 适合"少数 Topic、每个吞吐大"的场景。
RocketMQ 的设计是所有 Topic 的消息全追加到同一个 CommitLog 文件 ------不管多少 Topic、多少 Queue,写入始终是同一个文件的顺序追加 。Topic 从 1 个涨到 10000 个,写入路径完全一样,性能基本不退化。这是"海量 Topic 并发写入"能扛住的根基。
三、Producer 层:上千实例同时往一个 Broker 写
业务高峰时(比如双 11 下单),可能有几百上千个 Producer 实例同时往同一个 Broker 灌消息。RocketMQ 的高并发体现在------这些并发写入全部汇聚到同一个 CommitLog 顺序追加流,互不阻塞。
为什么做得到:刚才"锁优化"那轮讲过------多线程并发写入同一个 CommitLog,临界区极短、用自旋锁、索引构建挪出锁外。所以 1000 个 Producer 同时写,TPS 基本随并发数线性增长到饱和,不退化。写入路径无随机 IO + 锁不拖后腿,这两点是 Producer 高并发的根。
四、Consumer 层:水平扩展到任意并发
RocketMQ 一个 Topic 默认有 16 个 Queue(可配 4/8/16/32...),同一个 Consumer Group 内,消费者数量上限 = Queue 数。流量涨了?加 Consumer 实例就行,队列自动负载均衡给新实例,消费吞吐线性提升。
这种"按 Queue 分片"的设计,让消费端理论上可以扩展到任意并发------单个 Broker 撑不下,加 Broker;一个 Consumer Group 撑不下,加 Queue 数 + 加 Consumer。这是 RocketMQ 应对"消费侧高并发"的核心机制。
对比一下:如果是"一个 Topic 一个文件"的设计,消费端要拉消息时,N 个消费者读 N 个文件,磁盘随机 IO 会爆。RocketMQ 用 ConsumeQueue(20 字节一条的稀疏索引,几乎全在 PageCache)做路由,消费时先查 ConsumeQueue 拿到 commitlog offset,再回查 CommitLog,ConsumeQueue 小到几乎全在内存,N 个消费者查它不会产生磁盘压力。
五、批量放大:一次请求处理 N 条消息
RocketMQ 高并发的最后一块拼图是批量 :Producer 把多条消息聚合成一次网络请求发送;Broker 一次落盘一批;Consumer 一次拉一批。单次操作处理 N 条消息,有效吞吐 = 请求 QPS × batch size。
这看起来是性能优化,其实也是并发优化------把"1000 个并发请求"压缩成"100 个并发请求,每个带 10 条消息",锁次数、网络次数、系统调用次数直接降一个数量级 ,让同样的并发压力下系统更轻松。这正是"扛得多"的关键思路之一------不是单纯硬扛,而是把多次合成一次。
总结:高并发靠什么扛住
RocketMQ 高并发不是靠"机器硬刚",而是层层化解:
- 网络层用 NIO Reactor:少量线程扛海量连接
- 存储层用统一 CommitLog:多 Topic 不退化、不随机写
- 写入层用自旋锁 + 短临界区:多 Producer 并发不互相拖累
- 消费层用 Queue 分片:消费者水平扩展到任意并发
- 操作层用批量:把并发次数本身压下去
一句话------把"同时来 10000 路请求"通过统一追加、短锁、分片、批量,化解成"1000 次高效操作",让磁盘和 CPU 都吃得下。这就是"高并发"真正体现在哪、为什么扛得住。
高可用
好。"高可用"其实经常和"高可靠"混着说,但严格讲是两件事,分清楚才能看懂 RocketMQ 在防什么:
- 高可用 :快递站停电了,你的包裹还是能送到------侧重"服务不断"。
- 高可靠 :包裹不会丢、不会送错------侧重"数据不错"。
RocketMQ 的高可用(含数据可靠性)一共五层兜底,一层失败下一层接住,所以无论机器宕、网络抖、消费者崩,最终要么服务还在、要么数据还在,不会两失。下面逐层讲。
一、NameServer 集群:控制面"无人能挂掉"
Broker 在哪、Topic 路由信息由 NameServer 管理,它是控制中心。如果只有一个 NameServer,它挂了整个系统就瞎了。所以 RocketMQ 把 NameServer 做成无状态集群------部署多个,互相不通信。
体现:NameServer 集群里任意一台甚至多台挂掉,只要有一台活着,路由服务就在。Producer/Consumer 配置多个 NameServer 地址,一个连不上自动切下一个。
为什么能挂掉不慌 :每个 NameServer 节点都是独立、完整、无状态的路由表,互相不依赖、不复制。这比 ZooKeeper(要选主、要一致)简单得多------简单=不容易坏。这是 RocketMQ 设计上比早期 Kafka(依赖 ZK)轻量、健壮的地方。
二、心跳感知 + 故障剔除:发现坏得快,不连坏机器
体现:Broker 挂了,Producer 不会傻乎乎往死机器上发消息。
机制:Broker 每 10 秒向所有 NameServer 上报心跳;NameServer 120 秒收不到某 Broker 心跳就判定它"死了";Producer 每 30 秒从 NameServer 拉一次最新路由表,发现死 Broker 自动从发送列表剔除,往活着的发。
为什么重要 :故障不可怕,可怕的是故障了还在用。快速感知 + 自动剔除 = 永远只和活的机器打交道。这是高可用的"感知层"。
三、主从复制 + Dledger:存储层"主挂了备顶上"
这是高可用的核心层。Broker 单机一定会挂,所以必须有备份。
方案一:Master-Slave 主从复制 。Slave 实时从 Master 同步数据,Master 挂了 Slave 顶上接消费。但传统主从有个短板------Master 挂了要人工切换 Slave 为新 Master,切换期间写入不可用。
方案二:Dledger 集群(基于 Raft 协议) 。多个 Broker 节点组成 Raft 组,自动选 Leader,Leader 挂了自动选新 Leader,无需人工、秒级切换 。数据写入要多数派节点确认(3 节点要 2 个、5 节点要 3 个),Leader 挂了只要多数派还在,数据完整、服务秒级恢复。
体现 :单台 Broker 宕机,整个集群几秒内恢复写入能力,消息不丢。这是"无人工干预"高可用的关键。
为什么 Dledger 比传统主从强:Raft 协议保证了"大多数同意才算成功"的强一致性,同时自动选主避免了人工切换窗口。这就把"主从架构"升级成了"自治集群",真正具备生产级高可用。
四、消费者重平衡(Rebalance):消费端"挂一两个不影响"
体现 :Consumer Group 里某个消费者进程崩了,剩下消费者自动接管它的队列,消费不中断。
机制 :消费者每 20 秒从 NameServer 拉一次路由,发现队列分配变化(比如某消费者挂了、某 Broker 加了 Queue)就重新分配队列。挂掉的那个消费者的活儿,自动分给其他活着的消费者。
为什么重要:消费高峰期,总有消费者会重启、会 OOM、会卡死。如果挂了就停消费,会堆积成灾。Rebalance 让"挂了不影响整体",是消费侧高可用的保证。
五、数据层兜底:消息"绝不丢、绝不送错"
以上是"服务可用",但光有服务可用不够------还得保证消息本身不丢。这才是高可靠的硬指标。
5.1 刷盘策略(持久化)
- ASYNC_FLUSH(异步刷盘) :消息进 PageCache 立即返回成功,后台刷盘。性能高,但 Broker 宕机时 PageCache 里没刷下盘的消息会丢。
- SYNC_FLUSH(同步刷盘) :消息真正写入磁盘才返回成功。性能低一个数量级,但 Broker 宕机也不丢。
体现 :金融、订单类业务选同步刷盘,性能换可靠;日志、埋点选异步刷盘,可靠换性能。用户按场景选。
5.2 副本确认(多机兜底)
- ASYNC_MASTER:Master 写完就返回,Slave 异步复制。Master 挂了可能丢没复制到 Slave 的数据。
- SYNC_MASTER(同步双写) :Master 写完等 Slave 复制确认才返回成功。Master+Slave 同时挂才丢数据,概率极低。
- Dledger 多数派:过半数节点写入成功才返回,多机同时挂多数派才丢数据,概率更低。
体现 :单机故障不丢、双机同时故障才丢、多机多数派故障才丢------故障容忍度随副本数上升指数级提高。
5.3 ACK 确认 + 消费重试 + 死信队列(消费兜底)
这是消费侧"绝不丢消息"的三层保险:
- ACK 确认 :消费者必须显式返回 CONSUME_SUCCESS 才更新 offset。没返回、返回失败、进程崩了------offset 不动,下次重新拉这条消息。
- 自动重试 :消费失败的消息进入重试队列 %RETRY%group,按1s/5s/10s/30s/1m/2m...2h 递增间隔重试 16 次,总时长约 4.6 小时。给临时故障(如下游依赖抖动)充足的恢复时间。
- 死信队列(DLQ) :重试 16 次还失败,消息进入 %DLQ%group 死信队列,不再消费但永久保留,等人工介入处理。
体现:消费失败不会丢消息,最多进入死信队列等人处理。这是消费可靠性的最后一道墙。
5.4 事务消息(分布式事务兜底)
跨服务场景下,"本地事务成功 + 消息发送成功"两件事必须同时成功或同时失败。RocketMQ 用半消息 + 本地事务 + 回查三段式解决:
- Producer 先发"半消息"(消费者不可见)→ Broker 确认收到
- 执行本地事务(成功 / 失败)
- 本地事务成功 → 半消息转正,消费者可见;本地事务失败 → 半消息删除
- 如果 Producer 没回传事务状态(比如崩了),Broker 主动回查 Producer,根据上次状态决定半消息命运
体现:Producer 中途崩溃也不会让消息卡死或丢失,Broker 主动回查把"半死不活"的消息救回来。
总结:高可用靠什么扛住
RocketMQ 高可用一句话------把故障当成常态来设计,每一层都假设下一层会挂,提前准备好兜底。
- NameServer 集群:控制面挂几个不影响
- 心跳感知 + 剔除:自动避开坏机器
- 主从/Dledger:存储层自动容灾
- Rebalance:消费层自动接管
- 刷盘/副本/ACK/重试/死信/事务:消息层不丢不漏
五层叠加 = 任一层失败都有人兜底。高可用的本质不是"机器不挂",而是"挂了也能用、消息也不丢"。RocketMQ 把它做到极致,所以才被金融、电商、物联网这些"挂一秒损失百万"的场景广泛采用。