RocketMQ 数据持久化、高可用、高性能、读写原理、扩容总结

前言

本文是建立在堆 rocketmq 有一定的了解后对 rocketmq 进行了抽象的高度终结

数据持久化、高可用、高性能、读写原理、扩容几个关键点去梳理

数据持久化

顺序写入利用 PageCache 减少磁盘随机 seek,提升写入性能

一个 Topic 具有多个 MessageQueue,MessageQueue 分布在不同的 Broker 中进行存储,同时每个 MessageQueue 又在其它节点中保存有备份数据

发往当前 broker 的所有 topic 消息都顺序写入在一个 commitlog 文件中,一个 commitlog 文件默认为 1GB 写满了就创建一个新的文件

为了区分当前写入的消息是哪一个 topic 哪一个 messagequeue 的随后需要将地址信息写入到 /consumequeue/#{topic}/#{messagequeue} 中的 ConsumeQueue 文件中

ConsumeQueue 中只存储索引,所以一个文件能够存储更多的数据,同时每写满 1GB 创建一个新的文件

利用 mmap 映射 commitlog 和 consumequeue 文件到 PageCache 中,通过对 PageCache 的读写来提升性能,减少频繁磁盘 seek 开销

通过 mmap 建立映射关系后,还需要通过预热将磁盘数据加载一部分到 PageCache 中

同时为了支持我们可以根据 key 查询消费指定的消息,这就需要使用稀疏索引来实现,具体就是会维护 IndexFile 索引文件,在每存储一段消息之后,记录一条消息

RocketMQ 数据持久化、高可用、高性能、读写原理、扩容总结

高可用

元数据的高可用

集群元数据存储在 NameServer 中,每个 Broker 启动的时候都会向所有的 NameServer 发起注册同步上报自身的信息,比如负责了哪些 topic 的哪些 messagequeue,broker 自身的 ip 地址名称等

Broker 每隔 30S 向服务器发起心跳,上报元数据信息 ip 地址、名称、负责的 topic(增量变化的)nameserver 会更新当前心跳的时间以及元数据信息

NameServer 每隔 10S 检测一次 broker 列表中是否有 broker 超过了 120S 没有收到心跳,有就标记其下线

broker 和数据高可用

每一台 broker 上都有 DLedger 组件,基于 DLedger 组件实现服务的高可用

Leader 每隔一段时间向 Follower 发起心跳请求,Folllower 响应 ack,当 Leader 没有收到 Follower 的 ack 说明 Follower 挂了,当在超时时间内 Follower 没有收到 Leader 的心跳就认为 leader 挂了开启选举

当 Follower 宕机后只要集群节点数满足过半结点就继续工作,当 Follower 恢复后会基于当前同步的偏移量请求 Leader 同步后续数据

当 Leader 宕机后 DLedger 开启选举模式基于 raft 算法,选举出新的 Leader 节点,如果集群存活节点数少于过半结点,集群将无法启动

当 Leader 收到写入请求后 DLedger 代理接管 commitlog 组件,将数据同步发送到 Follower 中,当收到过半 ack 后认为消息写入成功,同时为了保证高性能,过半结点都是写入 os cache 就返回了

RocketMQ 数据持久化、高可用、高性能、读写原理、扩容总结

高性能

在客户端可以基于批量发送提高 broker 的吞吐量

broker 在写入 commitlog、consumequeue、indexfile 都是基于 mmap 来完成读写的

在 broker 端所有数据都是顺序写入 commitlog 文件,每个 commitlog 文件为 1GB 大小,之所以为 1GB 大小是因为底层要依赖 mmap,而 mmap 能操作的最大只能为 2G

对于 commitlog 来说数据整体写入都是顺序的,但是对于其中要写入 messagequeue 中的偏移量,由存在众多文件整体来看是随机写入的,但是 messagequeue 由于只存储了地址信息所以较小的一个文件能够存储更多的内容,同时 messagequeue 也是基于 mmap 来实现的

只要内存空间足够,预热阶段做好,绝大部分的写入都是非常的快的,commitlog 占据一大部分 os cache 内存、每个 messagequeue 占据一小部分 os cache,写入之后执行异步批量刷盘动作,基于 DLedger 将数据同步到多副本来保证刷盘期间可能导致的数据丢失问题

基于多副本 os cache 同步是一种即完成的数据备份高可用,同时又兼顾了性能的实现方式,如果采用同步刷盘的话单机的性能会大大折扣,这种情况下除非写入数据到 os cache 中还没有来的刷盘所有的 broker 全部掉电才会发生数据丢失,概率极低

分布式系统中提升单节点的性能,无疑就是提升了整个系统的吞吐量,每个 Topic 数据分布在不同的 broker 同步 LB 算法将 IO 压力均匀分摊

写数据

写数据首先需要从 NameServer 中拉取当前 topic 的元数据信息,如有哪些 messagequeue,每个 messagequeue 在哪一台 broker 中

然后本地根据指定 key 或者 LB 算法挑选要发往的 messagequeue ,如果发送失败则会移除当前 broker 从新计算要发送的 messagequeue

请求到达 broker 之后会基于 DLedger 实现数据一致性的写入多个副本

先写 commitlog 然后将偏移量写入 consumequeue 同时会维护 indexfile 用来检索指定 key 用于支持从指定位置消费

读数据

每台消费者都会从 nameserver 中获取到消费的 topic 对应的 broker 列表,会与每一个 broker 建立长连接

broker 收到消息后会在内存中记录消费者(消费组)和topic的对应关系,由于 broker 会与 nameserver 不停的发送心跳交换消息,所以 broker 是知道当前有一共有哪些消费者,哪些 messagequeue 的,将这个关系发送给客户端,客户端按照 LB 算法挑选自己负责哪些 messsagequeue 消费即可

当有消费者宕机的时候,失去与 broker 的心跳,broker 会感知到移除对应的关系,向客户端发送进入 reblance 阶段停止消费的指令,此时将移除某个消费者后的数据发送给客户端,客户端做排序后再次基于 lb 算法分配各自负责的 messagequeue

除此之外客户端每隔 30S 会请求 NameServer 拉取一次元数据信息,更新本地缓存

客户端每隔 20S 检测到 topic 的 messagequeue、消费组下的消费者发生变更后会通知 broker,broker 会下发再次进入 reblance 阶段

扩容

NameServer 扩容,NameServer 一般不会承载太高的读写压力,所以一般情况下无需扩容,并且 NameServer 是互相对立的,扩容新增节点也是比较简单但是却无法实现实时动态扩容

一般是 topic 的 messagequeue 太少,tps 导致单点的集群压力太大 IO,此时只需要新增 broker 节点,然后在管理控制台对该 topic 的 messagequeue 进行扩容即可

需要注意的是如果之前我们有实现顺序消息,key % messagequeue_num 来选中队列的话,新增 messagequeue 会导致顺序出现一些问题,因为新的消费者会消费新的队列和老的消费者消费原来的队列的老消息(如果消费的慢了一点),导致无法满足顺序消费

此时可以采用的策略是(最好在业务低峰期执行):新增一个 topic 将队列数翻倍,将新的数据写入新的队列,同时监听老的队列其下的消费者组是否已经完成了所以数据的消费,完成后再配置中心写入一个完成的变量标识,随后开启新队列的消费

当 broker 物理机的硬盘容量不够的时候,一种简单的方式是纵向扩容,另外一种方式也可以采用部署一个新的集群,将新的消息写入到新的队列中,监听老集群的消费队列是否已经完成了消费,如果已经完成了就切换到新集群来消费即可