RocketMQ 5.0:无状态代理模式的探索与实践
本文作者:金吉祥, Apache RocketMQ PMC Member,阿里云智能高级技术专家
背景

首先,让我们来看下是遇到了哪些痛点问题,促使我们去探索一种无状态代理的 RocketMQ
RocketMQ 拥有一套极简的架构,多语言客户端通过自定义的 Remoting 协议与后端 NameServer 和 Broker 建立 TCP 长连接,然后进行消息的路由发现以及完整的消息收发。这套架构的优势是:架构极简,客户端 与 Broker 通过 TCP 直连的模式,拥有较高的性能以及较低的延迟。同时,这套架构采取的是队列模型,非常适合基于队列批量高速拉取消息的场景。
同时,RocketMQ 在上云过程中面临了各种各样的挑战。
首先,云上用户需要更为丰富的业务语义消息,包括事务、定时、顺序以及死信等。为了满足用户的业务侧需求,需要在原先架构的客户端侧和 Broker 侧分别进行开发,用户必须升级客户端之后才能享受新功能。
其次,上云过程需要面对更为复杂的网络环境,不同场景下需要不同类型网络的接入。有些用户为了便捷性,期望能够交付公网的接入点,让不同地域的消费者、发送者都能连接到同一个消息服务;而另一种用户为了安全性,需要内网接入点来隔离一些非法的网络请求;RocketMQ 原先的架构在应对多网络类型的接入诉求时,成本是比较高的,多网络类型的接入必须同时覆盖 NameServer 和 Broker 的每一台机器才行。比如我们需要对内部 Broker 进行扩容场景下,如果原先的 Broker 拥有多种类型的网络接入诉求,那么新扩容的 Broker 也需要额外绑定上多种类型的网络接入点之后才能正常对外交付。
做下总结,面对上云的挑战,此前的架构逐渐暴露出了如下诸多痛点:
① 富客户端形态:客户端包含了大量企业级特性。用户必须升级客户端才能享受新功能,过程十分漫长。且同样的功能交付必须在多个多语言版本里都进行适配才能满足多语言客户端的接入,工作量巨大。
② 客户端与 Broker 所有节点的直连模式满足多类型网络接入的成本较高。
③ 按照队列进行负载均衡和消息拉取,后端扩缩容时会触发客户端 rebalance,导致消息延迟或重复消费,消费者会有明显的感知;此外, 基于队列的模型非常容易导致一个用户饱受困扰的问题:单个故障消费者消费卡住会导致消息在服务端大量堆积。
RocketMQ5.0 无状态代理模式

为了解决上述痛点,RocketMQ 5.0 提出了无状态代理模式。
新架构在原先的客户端和 Broker 中间插入了代理层。策略上是将客户端的无状态功能尽可能下移到代理层,同时也将 Broker 侧的无状态功能尽可能上移到代理层。在这里,我们将客户端的原有的负载均衡机制、故障隔离、push/pop 消费模型下移到了代理层,将 Broker 的访问控制、多协议适配、客户端治理以及 NameServer 的消息路由能力上移到了代理层,最终打造出的代理层的丰富能力,包含访问控制、多协议适配、通用业务能力、治理能力以及可观测性等。
在构建代理层的过程中,我们必须坚持的一个原则就是:客户端和 Broker 往代理层迁移的能力必须是无状态的,这样才能保证后续代理层是可以随着承接的流量大小进行动态扩缩容的。

在引入无状态代理层后,RocketMQ 原先客户端、Broker 的直连架构就演变为上图的无状态代理模式架构:
从流量上看,代理层承接了客户端侧所有流量, Broker 和 NameServer 不再直接对用户暴露,用户唯一能看到的组件只有代理层(Proxy)。
这里,Proxy 的职责包括以下几个方面:
-
多协议适配: Proxy 具备解析和适配不同协议的能力,包含 remoting 、gRPC、HTTP 以及后续可能衍生出来的 MQTT 和 AMQP 等协议。Proxy 对不同协议进行适配、解析,最终统一翻译成与后端 Broker 和 NameServer 之间的 remoting 协议。
-
流量治理和流量分发: Proxy 承接了客户端侧所有流量,因此能够很轻松地基于某些规则识别出当前流量的特性,然后根据一定的规则将流量分发到后端不同的 Broker 集群,甚至进行精准的流量控制、限流等。
-
功能扩展:包括访问控制比如允许哪个用户访问后端 Broker 集群中的哪个 Topic、消息轨迹的统一收集以及整体的可观测性等。
-
Proxy 能扮演 NameServer,交付给客户端查询 TopicRoute 的能力。
-
Proxy 能够无缝兼容用户侧的 Pop 或者 Push 消费模式:在 Proxy 和 Broker 侧采用 Pop 消费模式来避免单个队列被锁导致消息在服务端堆积的历史遗留问题。
同时,我们也可以看到 Proxy 具有以下两大特性:
① 无状态,可根据用户以及客户端的流量进行水平扩缩容。
② 计算型,比较消耗 CPU,因此在部署时需要尽可能给 Proxy 分配多些 CPU。
做下总结,无状态代理模式解决了原先架构的多个痛点:
① 将客户端大量业务逻辑下移到代理层,打造出轻量客户端。同时,依托于 gRPC 协议的标准化以及传输层代码自动生成的能力,能够快速适配多种语言的客户端。
② 客户端只会与 Proxy 层连接,针对多网络类型接入的诉求,可以将多网络类型绑定到 Proxy 层,由于 Broker 和 NameServer 不再直接对客户端暴露,转而只需对 Proxy 暴露内网的连接即可,多网络类型接入的诉求可以只在 Proxy 这个组件就封闭掉;同时,Proxy 的无状态的特性保证了多类型网络接入是与集群规模无关的。
③ 消费模式进行了无感知切换,无论客户端侧选择的是 Pop 还是 Push 消费模式,最终统一替换为 Proxy 与 Broker 侧的 Pop 消费模式,避免单个客户端消费卡住导致服务端消息堆积的历史问题。
RocketMQ 无状态代理模式技术详解

新架构在客户端和 Broker 之间引入了代理层,客户端所有流量都需要多一跳网络、多经历一次序列化 / 反序列化的过程,这对端到端消息延迟敏感的用户是极其不友好的,因此我们设计了合并部署形态。
合并部署形态下,Proxy 和 Broker 按照 1:1 的方式对等部署,且 Proxy 和 Broker 实现进程内通信,满足低延迟、高吞吐的诉求。同时,Proxy 仍然具备多协议适配的能力,客户端会与所有 Proxy 建立连接进行消息收发保证能够消费到所有消息。
代码实现上,我们通过构造器来实现合并部署和分离部署的两种形态。用户可自行选择部署形态。如果用户选择合并部署的形态,则在构建 Proxy 处理器之前,会先构造 BrokerController,并向 Proxy 的处理器注册, 本质上是为了告知 Proxy 处理器:后续的请求往我这个 BrokeController 发;如果用户选择分离部署模式,则无须构建 BrokerController,直接启动 Proxy 处理器即可。

对于这两种部署模式的比较,首先,合并部署和分离部署同时具备了多协议的适配能力,都能够解析用户侧和客户端侧的多协议请求;且具备模型抽象,能够解决富客户端带来的一系列痛点。
部署架构上,合并部署是一体化的架构,易于运维;分离部署是分层的架构,Proxy 组件独立部署,Proxy 和 broker 按业务水位分别进行扩缩。
性能上,合并部署架构少一跳网络和一次序列化,有较低的延迟和较高的吞吐;分离部署多一跳网络和一次序列化,开销和延迟有所增加。延迟具体增加多少主要依赖于 Proxy 和 Broker 之间的网络延迟。
资源调度上,合并部署状态下比较容易获得稳定的成本模型,因为组件单一;分离部署形态下 Proxy 是 CPU 密集型,Broker 和 NameServer 也逐渐退化成存储和 IO 密集型,需要分配比较多的内存和磁盘空间,因此需要进行细粒度的分配和资源规划,才能让分离部署形态资源的利用率达到最大化。
多网络类型接入成本上,合并部署成本较高,客户端需要与每一个 Proxy 副本都尝试建立连接,然后进行消息收发。因此,多网络接入类型场景下,Proxy 进行扩缩容时需要为每台 Proxy 绑定不同类型的网络;分离部署模式成本较低,仅需要在独立部署的 Proxy 层尝试绑定多网络类型的接入点即可,同时是多台 Proxy 绑定到同一个类型的网络接入点即可。
针对业务上的选型建议:
如果对端到端的延迟比较敏感,或期望使用较少的人力去运维很大集群规模的 RocketMQ 部署,或只需要在单一的网络环境中使用 RocketMQ 的,比如只需内网访问,则建议采用合并部署的模式。
如果有多网络类型接入的诉求比如同时需要内网和公网的访问能力或想要对 RocketMQ 进行个性化定制,则建议采用分离部署的模式,可以将多网络类型的接入点封闭在 Proxy 层,将个性化定制的改造点也封闭在 Proxy 层。

社区新推出的 PopConsumer 消费模式和原先的 PushConsumer 消费模式存在较大区别。PushConsumer 是基于队列模型的消费模式,但存在一些遗留问题。比如单个 PushConsumer 消费卡住会导致服务端侧消息堆积。新推出的 Pop 消费模式是基于消息的消费模型,PopConsumer 会尝试和所有 broker 连接并消费消息,即便有一个 PopConsumer 消费卡住,其他正常的 PopConsumer 依然能从所有 broker 里拉取到消息进行消费,不会出现消息堆积。
从 Proxy 代理层角度看,它能够无缝适配 PushConsumer 和 PopConsumer,最终将两种消费模式都映射到 Pop 消费模式,与后端 Broker 建立消费连接。具体来看,PushConsumer 里的 pull 请求会被转成 PopConsumer 消费模式里的 pop 请求,提交位点的 UpdateConsumeOffset 请求会被转换成消息级别的 ACK 请求,SendMessageBack 会被转换成修改消息的不可见时间的请求,以达到重新被消费的目的。

Proxy 最上层为协议的适配层,通过不同的端口对客户端暴露服务,不同协议通过不同端口请求到 Proxy 之后,协议适配层会进行适配,通过通用的 MessagingProcessor 模块,将 send、pop、ack、ChangeInvisibleTime 等请求转换成后端 Remoting 协议的请求,与后端的 Broker 和 NameServer 建立连接,进行消息收发。
多协适配的优势有以下几个方面:
① 加速了 RocketMQ 的云原生化,比如更容易与 Service Mesh 相结合。
② 基于 gRPC 的标准性、兼容性及多协议多语言传输层代码的生成能力,打造 RocketMQ 的多语言瘦客户端,能够充分利用 gRPC 插件化的连接多路复用、序列化 / 反序列化的能力,让 RocketMQ 客户端更加轻量化,将更多精力聚焦在消息领域的业务逻辑。

做下技术方案的总结:
无状态代理模式通过客户端下移、Broker 侧上移无状态的能力到代理层,将访问控制、客户端治理、流量治理等业务收敛到代理层,既能够满足快速迭代的诉求,又能对变更进行收敛,更好保障整个架构的稳定性:有了分层架构之后,更多业务逻辑的开发会聚焦在 Proxy 层, 下一层的 Broker 和 NameServer 趋于稳定,可以更多地关注存储的特性。Proxy 的发布频率远远高于底层 Broker 的发布频率,因此问题收敛之后,稳定性也得到了保证。
多协议适配,基于 gRPC 的标准性、兼容性以及多语言传输层代码生成的能力打造 RocketMQ 的多语言瘦客户端。
Push 到 Pop 消费模式的无感知切换,将消费位点的维护收敛到 Broker, 解决了单一消费者卡住导致消息堆积的历史遗留问题。
另外,我们也尝试探索了可分可合的部署形态,保证同一套代码可分可合,满足不同场景下对性能部署成本、易运维性的差异化诉求。在大部分的场景下,依然建议选择合并部署的形态。如果有对多网络类型接入的诉求,或对 RocketMQ 有极强的定制化诉求,则建议选择分离部署的形态,以达到更好的可扩展性。
代理层无状态的特性,极大降低了适配多类型网络接入诉求的成本。
未来规划

未来,我们期望 RocketMQ 底层的 Broker 和 NameServer 更多聚焦在存储的特性上,比如业务型消息存储的事务、定时、顺序等,快速构建消息索引、打造一致性多副本提升消息可靠性、多级存储来达到更大的存储空间等。
其次,对无状态代理层依照可插拔的特性开发,比如对访问控制、抽象模型、协议适配、通用业务能力、治理能力等按模块进行划分,使语义更丰富,可按照不同场景的诉求可插拔地部署各种组件。
最后,我们期望这一套架构能够支持阿里云上的多产品形态,致力于打造云原生消息非常丰富的产品矩阵。
加入 Apache RocketMQ 社区
十年铸剑,Apache RocketMQ 的成长离不开全球接近 500 位开发者的积极参与贡献,相信在下个版本你就是 Apache RocketMQ 的贡献者,在社区不仅可以结识社区大牛,提升技术水平,也可以提升个人影响力,促进自身成长。
社区 5.0 版本正在进行着如火如荼的开发,另外还有接近 30 个 SIG(兴趣小组)等你加入,欢迎立志打造世界级分布式系统的同学加入社区,添加社区开发者微信:rocketmq666 即可进群,参与贡献,打造下一代消息、事件、流融合处理平台。
更多推荐
所有评论(0)