本文作者:区二立 - 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」公众号,获取更多技术干货