本文作者:蔡高扬,Apache RocketMQ Committer, 阿里云智能技术专家。
背景
上图左侧为 RocketMQ 4.x版本集群,属于非切换架构。NameServer 作为无状态节点可以部署多份,broker 集群可以部署多组 broker ,每一组有一个 Broker Master 和多个 Broker Slave 。运行过程中如果某一组 master 故障,消息发送会路由到正常的 master 上,普通消息可以从原 Broker Slave 继续消费。
但非切换架构存在若干问题,比如定时消息或事务消息需要由 Master 进行二次投递,如果 Master 故障,则需要人工介入将 Master 重新恢复。因此, RocketMQ 5.0 提出了自主切换架构。
自主切换架构新增了一个 Controller 模块,负责选主。当某个 Broker Master 故障,会选择合适的 Broker Slave 提升为 Master,无需人工介入。
如果要在生产环境中部署一套集群,需要规划整个集群的机器资源(比如哪些模块部署在哪些机器、哪些机器需要什么样的资源规格等),然后安装 JDK 等依赖软件。每个组件还需要准备有其配置文件、启动脚本,再启动各个组件。整个过程十分耗费人力,而且存在误操作可能。
而在云基础设施上部署 RocketMQ 面临更多挑战:
首先,在云基础设施上创建不同规格的虚拟机更为方便,因此在云基础设施上部署时,一个虚拟机上往往只会部署一个模块,以实现资源隔离。然而,多节点部署也带了更高的操作成本。而系统内部组件的宕机、恢复、迁移等行为也需要进行支持。
从社区角度看,因为社区面向不同用户,不同用户往往会在不同云基础服务提供商上进行部署。但是从 IaaS 层设施看,不同云厂商提供的接口并不统一。
为了解决上述问题,社区借鉴了面向接口编程的思路:不直接操作基础设施,而是通过标准接口。而 Kubernetes 正是这样一个容器编排的“标准接口”。于是,社区在解决 RocketMQ 在云基础设施的部署问题时,选择基于 Kubernetes 进行部署,不同云厂商负责从 Kubernetes 到具体云 IaaS 层的调度:将有状态 RocketMQ 集群托管到 Kubernetes 集群,充分利用 Kubernetes 提供的部署、升级、自愈相关能力,同时也能享受到 Kubernetes 社区的生态红利。
Kubernetes 将 Pod、Deployment、Service、Ingress 等都封装成抽象资源。在部署 RocketMQ 集群时,只需将相关的 Kubernetes 资源编排好,而资源最终如何在云基础设施上进行编排则交由云服务提供商来完成。
然而,直接基于 Kubernetes 原生资源进行部署也存在一些不足。
比如用 Kubernetes 部署时,经常需要操作 YAML 文件,会涉及到 Deployment、StatefulSet、Service、Ingress 之类的资源需要大量配置,碰到复杂的资源定义就像“面向 YAML 编程”。
另外, Kubernetes 在支持有状态应用的管理上也存在局限。RocketMQ 集群的状态可归纳为两块。其一为集群拓扑关系,包括 RocketMQ Broker 主备关系以及 RocketMQ 不同模块之间的相互依赖(比如 Broker 需要依赖 NameServer、Controller 等);其二为存储状态,包括 Cluster 名称、 Broker 名称、 Broker ID 、新扩容 Broker 元数据等。
如何自动化管理以上状态也是必须解决的问题。
于是社区成立了 RocketMQ Operator 项目,用于支撑 RocketMQ 集群在云基础设施上的自动化运维与管理。
如上图所示,最右侧为 RocketMQ Operator 模块,实时与 Kubernetes API Server 进行交互。一方面会将 RocketMQ 集群(包括 NameServer、Broker 等模块)正常部署,同时也会利用 RocketMQ Admin Tool 实时地维护集群状态,比如 NameServer 地址等。
一、Kubernetes Operator原理
[]()
Kubernetes Operator 是一种相对简单灵活且编程友好的管理应用状态的解决方案。其工作原理分为两部分:一部分是利用自定义 API 资源( CRD )描述管理状态应用,可以认为是一个面向用户的接口,用户描述需要部署或运维的资源;另一部分是自定义控制器,根据自定义资源对象的变化完成运维动作。
上图中间的 Operator 控制循环可以视作自定义资源和 Kubernetes 资源之间的桥梁,它会不断监听自定义资源的状态变化,根据状态以及内部逻辑更新 Kubernetes 资源。同时也会根据 Kubernetes 资源的变化更新自定义资源的状态。
自定义对象与 Pod 或 Deployment 类似,只是它并不是 Kubernetes 内部提供的对象,而是需要用户自定义,并告知 Kubernetes。Custom Resource 指自定义API 资源的实例,Custom Resource Definition 指 CR 的定义。
如上图,比如有一个类型为 Container 的 CRD,定义了三个属性,分别是 Container 名称、Container 对应的 image 和 Container 监听的端口。下面的 CR 为具体自定义资源,其名称为 nginx,image 为 DockerHub 上的最新版本,监听80端口。与大家比较熟悉的数据库表进行类比,Container 可以认为是一张表,具体定义(Spec)可以类比为每一列的定义,每一行数据即不同的自定义资源(CR)。
自定义资源提供 API 对象,真正负责将自定义资源转换成 Kubernetes 内部资源的工作则由自定义控制器实现。
自定义控制器里有 Informer 模块,会不断地调用 Kubernetes API Server 的 listAndWatch 接口,以获得所监听 CR 的变化。CR 的变化事件和 CR 对象会被加入 Delta FIFO Queue,以 Key-Value 的方式保存在本地存储,并将 Key 加入 WorkQueue。最右侧的控制循环会不断地从 WorkQueue 取出相关 Key,根据 Key 从 Informer 的 Local Store 查询对应的 CR 对象。接下来将对象定义的期望状态与目前实际状态进行比对,如果有差异,则执行内部逻辑。最终使得实际状态与 CR 定义的期望状态达到一致。
二、RocketMQ Operator设计
社区在实现 RocketMQ Operator 时,并非直接通过 controller-runtime 底层接口,而是依赖 Operator SDK 作为脚手架,帮助生成相关代码。开发人员在进行 RocketMQ Operator 开发时,只需要专注 RocketMQ 集群本身的编排逻辑。
目前 RocketMQ Operator 的模块有 Name Service、Controller、Broker、TopicTransfer 和 Console,与 RocketMQ 模块基本一致。各模块通过不同 CRD 进行编排,其优点为架构、代码比较清晰,不同对象均有独立的 CRD 定义和对应的 Controller 实现。缺点为缺少 RocketMQ 集群维度的描述,代码实现、配置上可能存在重复。关于 CRD 的演进,欢迎社区同学结合各自的实践提出建议。
Name Service 模块负责 NameServer 在 Kubernetes 集群的运维管控操作,包括部署、扩缩容、提供 NameServer 集群IP列表等。Broker 需要向 NameServer 注册路由信息,因此 NameServer 的地址极为重要,需要作为内部状态实时地进行维护。当 NameServer 进行扩缩容时,Broker 集群能够自动感知 NameServer 地址的更新。
上图为 NameServer CR 定义示例,主要属性有:
- NameServer 集群实例数
- NameServer 镜像
- hostNetwork 可以设置为 true 或 false ,true 则表示通过 hostNetwork 提供 Node IP,供 Kubernetes 集群外部的客户端访问。
Controller 模块定义了 Dledger controller 集群。当 Broker 启用自主切换模式时,需要维护 Controller 的访问地址,其中采用了两种机制:
第一种:Service。该方式会暴露 Controller 集群的统一访问地址供Broker 访问。Broker 的访问请求会路由到任意 Controller 节点,Controller 节点会返回 Controller 主节点的访问地址,Broker 再与 Controller 主节点进行通讯。
第二种:Headless Service。该机制为每一个 Controller 的提供访问地址,用于 Controller 间进行服务发现。在组建 Controller 集群时,Controller 节点必须与具体的某个 Controller 节点进行通信,因此必须为一对一关系。
Controller 的定义相对简单,只需提供 Controller 数量(数量必须为奇数)、 Controller image ,其他与 NameServer 类似,比如资源、存储的定义。
Broker 模块用于定义 Broker 集群,维护 Broker 组数量以及每组的节点数量,同时负责处理 Broker 集群的运维操作,包括部署、扩缩容以及元数据复制。扩容时,如果新扩容的 Broker 没有 Topic 等元数据,用户流量实际上不会路由到此 Broker。因此,Broker 模块还会负责 Broker 扩容后进行元数据复制。
Broker 的定义相对复杂,包括:
- Broker组的数量。
- 每组的节点数。
- clusterMode定义了broker集群模式,默认部署非切换集群,设置为Controller则部署自主切换集群。
- 其余还包括资源、存储定义等。
TopicTransfer 不是与 RocketMQ 直接映射的模块,它定义了 Topic 和 Consumer Group 元数据迁移运维操作。使用 TopicTransfer 迁移元数据时,首先在目标集群创建指定的 Consumer group 和 Topic ;创建完成后禁写原集群,使得消息不会发送到原集群;等待原集群的消息消费完成后,会将原集群的元数据进行清理。在此过程中,任何一步失败均会进行回滚,确保元数据正确迁移。
Console 模块负责部署 RocketMQ 控制台以及维护其用到的 NameServer 地址,功能相对简单,目前 RocketMQ Console 为无状态节点,其定义方式与 Deployment 的相同。
接下来介绍几个重要的控制器实现。
NameServer 控制器首先会判断 Name Service 对应的 StatefulSet 是否存在,如果不存在,则创建或更新 StatefulSet,直至 NameServer 节点数与期望值相同。然后列出 NameServer 对应的 Pod 地址,并判断地址是否发生了变化。如果是,则会将集群中的 NameServer 地址进行更新,从而保证 Broker 或其他模块能够获取到正确的 NameServer 地址。
Dledger Controller 先创建 Headless Service 用作组建 Controller 集群时服务发现的入口,接着判断 Controller 节点数量是否与期望值一致,如果不一致,则创建 StatefulSet。创建 StatefulSet 时会自动为每个 Dledger Controller 分配 controllerDledgerSelfId。期望节点数目与实际节点数目一致后,才会暴露 Controller 的 Service 地址,供 Broker 访问。
Broker 是有状态应用,因此在扩容或缩容时需要 Broker Controller 进行额外动作。Broker Controller 会以 Broker 组为单位进行调度,每一个 broker 组有 1个 master 节点,并配置 0 到多个 slave 节点。当 Broker 进行扩容时,会新增一组 Broker 并按照用户配置复制元数据到新扩容的 Broker。
Broker 依赖 NameServer和 DLedger Controller,因此会等待 NameServer 、 DLedger Controller 启动完成且两者均正常提供服务后,才会进一步创建 Broker 对应的 StatefulSet ,直到实际节点数目与期望节点数一致。
如果出现扩容情况,则会根据在 CR 定义 的 ScalePodName 字段对应 Pod 将元数据(包括 Topic 、消费组)拷贝到新扩容的 Broker 。
三、快速部署RocketMQ集群
首先,将 RocketMQ Operator 项目克隆到本地,解压后执行 install-operator.sh 脚本即可完成 RocketMQ Operator 的安装。
第二步,配置 Name Service CR。Name Service CR 配置较为重要的字段有两个,其一为 size,即需要部署了多少个 NameServer 节点,其二为 hostNetwork ,默认 false ,此时客户端只能在 Kubernetes 集群内与 NameServer 进行通讯。如果Kubernetes 集群外的客户端需要访问到 RocketMQ 集群,需要将 hostNetwork 配为 true ,NameServer 的接入点需要配置为 NameServer 所在的 Node IP。
第三步,配置 Controller CR。注意 size 需要配置为奇数。Controller 的数据需要持久化存储,可以利用云服务提供商提供的 StorageClass,无需自行维护存储。如果希望配置自己的存储,GitHub 上 RocketMQ Operator 项目代码提供了配置 NFS 存储的相关示例。
第四步,配置 Broker CR。示例中配置了两组 Broker,每组有一个备节点,同时将 clusterMode 设置为 Controller,启动自主切换架构集群。
准备好以上三个模块的相关配置文件之后,执行 kubectl apply 命令提交给 Kubernetes 集群。其余的部署、运维等动作均交由 RocketMQ Operator 自动完成。
成功部署后,可以通过 kubectl get po 命令查看部署的Pod。可以看到部署了4个Broker节点、Controller 和 NameSever 节点各3个。
进入一个 Broker Pod,可以使用 clusterlist 命令查看集群状态,可以看到集群有两组 Broker,每一组各有一主(BID=0)一备。
四、未来展望
RocketMQ Operator 将不断完善,全面支持 RocketMQ 5.0,后续规划主要包含以下工作:
① 镜像统一。目前 RocketMQ Operator 内部也维护了一套 RocketMQ 镜像,但是已经有 RocketMQ Docker 项目,没必要再维护一套镜像。因此,未来社区希望将对两边镜像进行统一,降低管理成本。
② 集群管理。RocketMQ 5.0 版本还提供了另外一种集群部署方式—— BrokerContainer 对等部署。与 4.0 版本传统的主备方式不同, BrokerContainer 会在进程中同时启动一主一备,有两个 BrokerContainer 中的 Broker 互为主备。某一个Container 的主节点故障时,则配对的 Container 中的备节点会进入 Slave Acting Master 状态,负责代理主节点进行定时消息或事务消息等二级消息的处理。
③ 支持部署更多 RocketMQ 组件,包括 RocketMQ Schema Registry、RocketMQ Proxy、RocketMQ Exporter 等。