RocketMQ实现了灵活的多分区和多副本机制,有效的避免了集群内单点故障对于整体服务可用性的影响。存储机制和高可用策略是RocketMQ稳定性的核心,社区上关于RocketMQ目前存储实现的分析与讨论一直是一个热议的话题。本文想从一个不一样的视角,着重于谈谈我眼中的这种存储实现是在解决哪些复杂的问题,因此我从本文最初的版本中删去了冗杂的代码细节分析,由浅入深的分析存储机制的缺陷与优化方向。
RocketMQ的架构模型与存储分类
先来简单介绍下RocketMQ的架构模型。RocketMQ是一个典型的发布订阅系统,通过Broker节点中转和持久化数据,解耦上下游。Broker是真实存储数据的节点,由多个水平部署但不一定完全对等的副本组构成,单个副本组的不同节点的数据会达到最终一致。对于单个副本组来说同一时间最多只会有一个可读写的Master和若干个只读的Slave,主故障时需要选举来进行单点故障的容错,此时这个副本组是可读不可写的。NameServer是独立的一个无状态组件,接受Broker的元数据注册并动态维护着一些映射关系,同时为客户端提供服务发现的能力。在这个模型中,我们使用不同主题(Topic)来区分不同类别信息流,为消费者设置订阅组(Group)进行更好的管理与负载均衡。
如下图中间部分所示:
服务端BrokerMaster1和Slave1构成其中的一个副本组。服务端Broker1和Broker2两个副本组以负载均衡的形式共同为客户端提供读写。
RocketMQ目前的存储实现可以分为几个部分:
1、元数据管理
具体指当前存储节点的主题Topic,订阅组Group,消费进度ConsumerOffset。多个配置文件Config,以及为了故障恢复的存储Checkpoint和FileLock。用来记录副本主备身份的Epoch/SN(sequencenumber)文件等(5.0-beta引入,也可以看作term)
2、消息数据管理,包括消息存储的文件CommitLog,文件版定时消息的TimerLog。
3、索引数据管理,包括按队列的顺序索引ConsumeQueue和随机索引IndexFile。
1.元数据管理与优化
为了提升整体的吞吐量与提供跨副本组的高可用能力,RocketMQ服务端一般会为单个Topic创建多个逻辑分区,即在多个副本组上各自维护部分分区(Partition),我们把它称为队列(MessageQueue)。同一个副本组上同一个Topic的队列数相同并从0开始连续编号,不同副本组上的MessageQueue数量可以不同。
例如topic-a可以在broker-1主副本上有4个队列,编号(queueId)是0-3,在broker-1备副本上完全相同,但是broker-2上可能就只有2个队列,编号0-1。在Broker上元数据的组织管理方式是与上述模型匹配的,每一个Topic的TopicConfig,包含了几个核心的属性,名称,读写队列数,权限与许多元数据标识,这个模型类似于K8s的StatefulSet,队列从0开始编号,扩缩队列都在尾部操作(例如24个队列缩分区到16,是留下了编号为0-15的分区)。这使得我们无需像Kafka一样对每个分区单独维护状态机,同时大幅度的简化了关于分区的实现。
我们会在存储节点的内存中简单的维护MapString,TopicConfig的结构来将TopicName直接映射到它的具体参数。这个设计足够的简单,也隐含了一些缺陷,例如它没有实现一个原生Namespace机制来实现存储层面上多租户环境下的元数据的隔离,这也是RocketMQ5.0向云原生时代迈进过程中一个重要的演进方向。
当Broker接收到外部管控命令,例如创建或删除一些Topic,这个内存Map中就会对应的更新或者删除一个KV对,需要立刻序列化一次并向磁盘覆盖,否则就会造成丢失更新。对于单租户的场景下,Topic(Key)的数量不会超过几千个,文件大小也只有数百KB,速度是非常快。但是在云上大多租的场景下,一个存储节点的Topic可以达到十几MB。每次变更一个KV就全量向磁盘覆盖写这个大文件,这个操作的开销非常高,尤其是在数据需要跨集群,跨节点迁移,或者应急情况下扩容逃生场景下,同步写文件严重延长了外围管控命令的响应时间,也成为云上大共享模式下严峻的挑战之一。在这个背景下,两个解决方案很自然的就产生了,即批量更新接口和增量更新机制。
批量更新指每次服务端可以接受一批TopicConfig的更新,这样Broker刷写文件的频率就显著的降低。增量更新指将这个Map的持久化换成逻辑替换成KV型的数据库或实现元数据的Append写,以Compaction的形式维护一致性。
除了最重要的Topic信息,Broker还管理着Group信息,消费组的消费进度ConsumerOffset和多个配置文件。Group的变更和Topic类似,都是只有新建或者删除时才需要持久化。而ConsumeOffset是用来维护每个订阅组的消费进度的,结构如MapString/topicName
groupId/,Map。这里我们从文件本身的作用和数据结构的角度进行分析下,TopicGroup虽然数量多,但是变化的频率还是比较低的,而提交与持久化位点时时刻刻都在进行,进而导致这个Map几乎在实时更新,但是上一次更新后的数据(last