vivo 鲁班平台 RocketMQ 消息灰度方案
本文作者:区二立 - vivo 技术架构总监
方案背景
RocketMQ 使用广泛,技术场景下,可以用于异步解耦,比如不同系统间调用业务链上做分段式处理或使用不同语言的两个系统间的解耦;可以用于数据同步,比如基础数据通过 MQ 广播到各个业务领域,实现业务领域的提效;高并发订单或 IM 的推送服务中,可以使用 MQ 做削峰填谷;此外,在分布式事务中,也可以通过 MQ 做最终一致性的事务方案。
RocketMQ 在业务场景下可覆盖很多系统,包括营销系统、生产制造上的各种管控系统、公共平台上人资、移动办公等流程类系统、类似于钉钉的自建 IM 工具以及大数据等。
随着以微服务化为基础的数字化建设转型,完成一项业务必须串联不同团队和不同应用。而不同应用的开发和发布周期相对独立,需要对接的版本不一,因此需要灰度方案。
对于 HTTP 的灰度,很多时候通用的网关即可提供较好的支持,甚至简单地用 Nginx 实现也可以达到效果。微服务层,以 Dubbo 为例,有各种分组比如有扩展的 SPI 补充实现,可以轻松解决灰度方面的困扰。而 MQ 的灰度却没有标准支持,很多系统直接放弃了 MQ 灰度,因此也不得不接受一定时间段内的错误重试。
MQ 技术特点
Broker 是消息服务的核心,提供了消息服务最重要的计算与存储功能。消息发送时会对应一个 Topic,Topic 为逻辑上的概念,内部执行往往是以 Queue 为单位。以普通消息类型的 Topic 为例,Topic 一般有多个 Queue,如图中的 TOPIC_V_PLACE_ORDER 共有四个 Queue,分别在两个 broker 里, broker a 与 broker b 里各有两个 Queue。任何一条消息都必定属于四个 Queue 中的某一条。
每条消息内可指定 tag 标志,用于在逻辑上进一步切分 topic,如上图中的 tagA、tagB。切分维度有多种,可以是 IoT,也可以是增值服务类的产品 order 等。不同 tab 表示不同分类,但它们共享一个 topic。
Queue 可以理解为物理上的区分,broker 的 commitLog 用于存放消息。commitLog 不区分 Topic 和 Queue,不同的 Topic 消息内容会按实际接收的 Queue 存储到其对应的 broker commitLog 上,该消息只会在集群中的某个 broker commitlog 中存在。
Broker commitLog 是公用的,到达某一个 broker 的消息都会存在同一个 commitLog 上,即一个 commitLog 会同时保存不同 topic 的内容。
CommitLog 达到一定大小时(一般为 1G),会新建新的 commitLog 用于存储和接收新消息。commitLog 在物理上存储具体消息,因此必然需要文件记录 Queue 与 commitLog 存储消息之间的位置映射。minOffset(最小位移)和 maxOffset(最大位移)也是存储位置中的重要概念。
ConsumeGroupID 与 groupID 强关联, groupID 消费时会在 broker 上记录 topic Queue 的消费位移,即会根据 Queue 记录不同 groupID 的 consumerOffset。
无论是生产者还是消费者,MQ 都使用 groupID 表示交互的角色。在集群消费的情况下,使用同一个 groupID 的两个 client 会做 Queue 的消费订阅分配,一般会尽量采取平分的方式。而独立的消费组比如上图中 GID_V_PAYMENT,会独占 topic 中的 4 条 Queue。消费组之间相互独立、互不干扰,有各自的消费位移点。
不同的 RocketMQ 实例都有独立的 clientID,作为唯一标识与 broker 打交道。
提交消费位点时,只能提交该消费位点前都已完成消息位移的消息。如上图,3、4 两条消息都已被消费线程处理完成,但 2 依然在处理中,因此实际触发的提交位点为 1。消息 2 完成处理后,会触发消息 4 的提交。
不同的 groupID 之间互相绝缘,但同一个 groupID 却会相互影响。订阅关系指标记了当前应用实例的 groupID 订阅了哪些 Topic(或 topic 的 tag)。每一个应用实例或 clientID 的订阅关系都会随着心跳包一起发送到 broker 上,并在 broker 上以 groupID 作为 key 来存储。
每个 broker 每次接收到不同实例的心跳包时,都会按 groupID 的维度校验、替换订阅关系,即同一个 groupID 的订阅关系会被替换。同一个 groupID 在不同应用实例、不同 clientID ,只要订阅的 Topic 和 tag 有任何不同,都会被最后到来的心跳包的订阅关系覆盖。
如果在不同的应用实例中使用同一个 groupID ,而实例因版本原因导致订阅 Topic 发生变化,则两组实例共存时会互相干扰,导致有些应用实例收不到想要的消息或收到错误的消息。而订阅关系是影响 MQ 灰度方案的核心因素。
上图 clientID_001 和 clientID_002 同属一个消费组 GID_C_INVENTORY, clientID_001 订阅了 TOPIC_A,clientID_002 订阅 TOPIC_B,都使用同一消费组执行订阅,因此,按照分配策略,它们会被交叉分配。分配结果可能是 clientID_001 和 clientID_002 平分 TOPIC_A 的两条 Queue, 也可能会平分 TOPIC_B 的两条 Queue,从而导致异常。这就是订阅方式不一致导致的分配错乱以及处理错乱。
常见灰度方案
常规的灰度方案一般都会选择不同的消费组,处理方式有影子 Topic、Tag 过滤以及 userProperty 过滤。以上几种处理方式都会存在一些缺陷:比如如何保证所有灰度消息都被消费完毕?灰度需要切换时,如何保证灰度消息是被灰度环境消费?灰度订阅切换为正常订阅关系的时候,如何确认消费位点,如何衔接才能保证消息不丢失?此外,运维人员可能对 RocketMQ 或应用内部逻辑不清楚,实际的操控对运维人员而言也是巨大的挑战。
鲁班灰度方案
鲁班灰度方案的核心解决思路是将 Queue 隔离使用。
Queue 是 Topic 的实际执行单元,一个 Topic 有多个 Queue。可以选择一部分 Queue 用于灰度,灰度 Queue 的数量、开始位置等可以在具体的实现里进行定义。如上图,首尾两个 Queue 专门用于灰度。因此,我们只需保障生产者与消费者的灰度与正常环境隔离使用即可,灰度环境的消息只从灰度的 Queue 里取,正常环境的消息从正常环境的 Queue 里拉取。
消费者中无论是灰度还是正常的应用集群,都使用同一个消费组。这会导致订阅关系非常容易不一致。针对于此,我们的解决方案是改造订阅关系。
Broker 的订阅关系维护在 ConsumerGroupInfo 里,其中 subscriptionTable 负责维护 groupID 发送来的订阅心跳包。如果心跳包的订阅关系不一样,则会进行替换。我们新增了 graySubscriptionTable 类,专门负责维护灰度的订阅关系。虽然是同一个 groupID,但使用不同的类来分别维护灰度和非灰度的订阅关系。
生产者的发送策略如下:首先判断目标 topic 是否有灰度消费者,再判断当前消息是否属于灰度范围。如果是,则将灰度消失投递到灰度的 Queue 里;否则,投递到正常的 Queue。
消费者按灰度拉取,正常集群只平分正常的 Queue,灰度集群只平分灰度的 Queue。如上图,ClientID_001 与 ClientID_002 只会分享正常的 Queue,而 ClientID_003 与 ClientID_004 只分享灰度的 Queue,他们共用一个消费组。
如果本消费组使用的 topic 没有灰度,但由于其他消费组影响涉及到灰度 topic,则它也会平分拉取灰度的 Queue。此外,如果 topic 没有涉及灰度集群,则灰度 Queue 会空置不使用,消费者不拉取,生产者不发送。将灰度集群切换为正常集群时,原先灰度的集群会保证将灰度 Queue 消费完成后才真正进行切换,业务上动态切换服务时,MQ 会自动根据实际消费进度进行细节上的管控,保障所有消息不丢弃。
除了业务灰度标识外,MQ 也有自己的灰度标识需要处理,存储于 Namesrv。生产者和消费者获取 Topic 路由消息都由 Namesrv 提供,这也意味着生产者和消费者已经与 Namesrv 建立了连接。可以通过定期将灰度消息更新到 Namesrv 上,生产者也会定期将灰度信息拉到本地来打通整个链路。
Namesrv 存储灰度关系时,需要一个有状态的数据库来进行保存。
如果灰度期间的延时消息在灰度结束后才投递,则会投递到正常 Queue。延时消息临时存在数据库里,能够支持比较细粒度的延时定义。
全局顺序消息会由一条 Queue 变成两条 Queue。我们修改了创建 Queue 的定义,灰度切换回正常环境时,会保证将灰度的消息处理完以后再处理正常的消息。
细节上的控制主要依赖灰度开关 grayFlag 和 graySwitch 两个标识位进行控制。
graySwitch 标志使用灰度的逻辑以及平分所有 Topic 里 Queue 的逻辑,能够兼容不参与灰度的应用,可以平分所有受其他灰度消费组影响的 Topic 的所有 Queue。
grayFlag 用于标记本实例是否为灰度实例,这会影响到订阅关系的保存。它会先查看 graySwitch,再进行自己的判断。
灰度场景校验
实践是检验真理的唯一标准,我们进行了详细的灰度功能校验,分别是灰度版本订阅的 topic&tag 不变、灰度版本订阅的 topic 增加、灰度版本订阅的 topic 减少、灰度版本订阅的 tage 变化以及灰度版本订阅的 topic&tag 混合变化。
加入 Apache RocketMQ 社区
十年铸剑,Apache RocketMQ 的成长离不开全球接近 500 位开发者的积极参与贡献,相信在下个版本你就是 Apache RocketMQ 的贡献者,在社区不仅可以结识社区大牛,提升技术水平,也可以提升个人影响力,促进自身成长。
社区 5.0 版本正在进行着如火如荼的开发,另外还有接近 30 个 SIG(兴趣小组)等你加入,欢迎立志打造世界级分布式系统的同学加入社区,添加社区开发者微信:rocketmq666 即可进群,参与贡献,打造下一代消息、事件、流融合处理平台。
微信扫码添加小火箭进群
另外还可以加入钉钉群与 RocketMQ 爱好者一起广泛讨论:
钉钉扫码加群
关注「Apache RocketMQ」公众号,获取更多技术干货
版权声明: 本文为 InfoQ 作者【阿里巴巴云原生】的原创文章。
原文链接:【http://xie.infoq.cn/article/526dd7e039b88cd5aab5bc91a】。文章转载请联系作者。
评论