一、计划背景
RocketMQ(以下简称MQ)作为消息中间件在事务管理,异步解耦,削峰填谷,数据同步等利用场景中有着宽泛应用。当业务零碎进行灰度公布时,Dubbo与HTTP的调用能够基于业界通用的灰度形式在咱们的微服务治理与网关平台来实现,但MQ已有的灰度计划都不能齐全解决音讯的隔离与切换连接问题,为此,咱们在鲁班MQ平台(蕴含根因剖析、资源管理、订阅关系校验、延时优化等等的扩大)减少了MQ灰度性能的扩大实现。
二、RocketMQ技术特点
为什么MQ的灰度计划迟迟没有实现呢?咱们先来回顾一下RocketMQ的几个核心技术点。
2.1 存储模型的简述
(图2.1 MQ的存储模型)
CommitLog:音讯体理论存储的中央,当咱们发送的任一业务音讯的时候,它最终会存储在commitLog上。MQ在Broker进行集群部署(这里为也简洁,不波及主从局部)时,同一业务音讯只会落到集群的某一个Broker节点上。而这个Broker上的commitLog就会存储所有Topic路由到它的音讯,当音讯数据量达到1个G后会从新生成一个新的commitLog。
Topic:音讯主题,示意一类音讯的逻辑汇合。每条音讯只属于一个Topic,Topic中蕴含多条音讯,是MQ进行音讯发送订阅的根本单位。属于一级音讯类型,偏重于业务逻辑设计。
Tag:音讯标签,二级音讯类型,每一个具体的音讯都能够选择性地附带一个Tag,用于辨别同一个Topic中的音讯类型,例如订单Topic, 能够应用Tag=tel来辨别手机订单,应用Tag=iot来示意智能设施。在生产者发送音讯时,能够给这个音讯指定一个具体的Tag, 在生产方能够从Broker中订阅获取感兴趣的Tag,而不是全副音讯(注:谨严的拉取过程,并不全是在Broker端过滤,也有可能局部在生产方过滤,在这里不开展形容)。
Queue:实际上Topic更像是一个逻辑概念供咱们应用,在源码层级看,Topic以Queue的模式散布在多个Broker上,一个topic往往蕴含多条Queue(注:全局程序音讯的Topic只有一条Queue,所以能力保障全局的程序性),Queue与commitLog存在映射关系。能够了解为音讯的索引,且只有通过指定Topic的具体某个Queue,能力找到音讯。(注:相熟kafka的同学能够类比partition)。
生产组及其ID:示意一类Producer或Consumer,这类Producer或Consumer通常生产或生产同利用域的音讯,且音讯生产与生产的逻辑统一。每个生产组能够定义全局维一的GroupID来标识,由它来代表生产组。不同的生产组在生产时相互隔离,不会影响彼此的生产位点计算。
2.2 音讯发送与生产
(图2.2 音讯发送与拉取模型)
2.2.1 客户端标识
在生产者或消费者集群中,每一个MQ客户端的运行实例,在MQ的客户端会保障产生惟一的ClientID。注:同一利用实例中,既充当生产者,也充当消费者时,其ClientID实际上是同一个取值。
2.2.2 音讯发送
当向某个Topic发送音讯的时候,会首先取得这个Topic的元数据,元数据会包含它有多少个Queue,每个Queue属于哪个Broker。默认的状况下,发送方会抉择一条Queue发送以后音讯,算法类型是轮询,也就是下一条音讯会抉择另一条Queue进行发送。另外MQ也提供了指定某条Queue或者自定义抉择Queue的办法进行音讯的发送,自定义抉择Queue需实现MessageQueueSelector接口。
2.2.3 音讯生产
进行音讯的生产时,同一生产组(GroupID)的消费者会订阅Topic,消费者首先取得Topic的元数据,也就是会取得这个Topic的所有Queue信息。而后这些Queue按规定调配给各个具体的客户端(ClientID),各个客户端依据调配到的Queue计算对应的须要拉取音讯的offset并生成PullRequest,拉取音讯并生产实现后,客户端会生成ACK并更新生产进度。
这里的生产进度是该批音讯未生产胜利的最小offset,如图2.3所示,一批音讯中如果1、5未生产,其余的音讯已生产,此时更新的offset仍是1,消费者如果宕机重启,会从1号开始生产音讯,此时2、3、4号音讯将会反复生产。
(图2.3 生产进度更新示意图)
因而RocketMQ只保障了音讯不会失落,无奈保障音讯不会反复生产,音讯的幂等性须要业务本人实现。
另外,生产方能够指定生产某些Tag的音讯,在pullRequest进行拉取时,会在Broker里会依照存储模型的Queue索引信息按hash值进行疾速过滤,返回到生产方,生产方也会对返回的音讯进行精准的过滤。
2.3 订阅关系一致性
在生产端,同一生产组内(GroupID雷同,本节所形容的前提都是同一生产组内)的各个利用实例MQ客户端须要放弃订阅关系的一致性,所谓订阅关系的一致性就是同一生产组内的所有客户端所订阅的Topic和Tag必须完全一致,如果组内订阅关系不统一,音讯生产的逻辑就会凌乱,甚至导致音讯失落。
2.3.1 订阅关系的保护
每一个利用实例MQ客户端都有独立的ClientID,咱们简略地解说一下订阅关系的保护:
- 各个MQ生产客户端会带着它的ClientID向所有Broker发送心跳包,心跳包中含有具体的本客户端订阅的Topic & Tag等信息,registerConsumer办法会依照生产组将客户端分组存储,同一生产组内的ClientID信息在同一个ConsumerGroupInfo中;
- Broker在接管到任何生产客户端发过来的心跳包后,会在ConsumerManager类中的ConcurrentMapconsumerTable依照生产组名称GroupID作为key存储不同的生产组信息,同一生产组的订阅信息会在每一次收到心跳包后,都会按以后的订阅Topic & Tag信息进行更新保护,也就是相当于只保留最新的心跳包订阅信息(心跳包中的subVersion会标记心跳包版本,当重均衡后果产生扭转后,subVersion会更新,Broker只会保留最新版本的心跳包中的订阅信息),不论这个心跳包来源于这个生产组的哪个ClientID。(因为Tag是依赖于Topic的属性,Topic和Tag订阅关系不统一时Broker对应的处理结果也略有不同,具体可见updateSubscription办法)。
2.3.2 订阅不统一影响
在这里应用例子的形式来论述订阅关系不统一带来的局部问题。假如同一组内的消费者clientA订阅了TOPIC\_A,clientB订阅了TOPIC\_B;TOPIC\_A、TOPIC\_B均为4条Queue,在进行生产调配时,最终Queue的调配后果如下:
(表2.1 音讯费者的Queue调配后果表)
因为clientB没有订阅TOPIC\_A,clientA也没有订阅TOPIC\_B,所以TOPIA\_A中的Queue-2、Queue-3,TOPIC\_B中的Queue-0、Queue-1中的音讯则无奈失常生产。而且在理论过程中,clientA也没法失常生产TOPIC_B的音讯,这时会在客户端看到很多异样,有些Queue的音讯得不到生产产生沉积。
此外,在订阅关系中,不同client所订阅的Tag不一样时,也会产生拉取的音讯与所须要订阅的音讯不匹配的问题。
三、业界MQ灰度计划
(图3.1 灰度调用示意图)
通常,业务灰度只严格地保障RPC服务之间的调用,局部音讯灰度流量的散失或谬误是能够容忍的,如图3-1所示,V\_BFF产生的灰度音讯会被V\_TRADE的失常版本与灰度版本收到并随机生产,导致局部灰度流量没有进入冀望的环境,但整体RPC服务的调用还是隔离了灰度与非灰度环境。当业务对音讯生产的逻辑进行了更改,或者不想让灰度的音讯影响线上的数据时,MQ的灰度就必须要实现。
因为订阅关系的限度,以后的业界实现的MQ灰度计划都是失常版本与灰度版本应用不同的GroupID来实现。以下的计划均应用了不同的GroupID。
3.1 影子Topic的计划
新建一系列新的Topic来解决隔离灰度的音讯。例如对于TOPIC\_ORDER会创立TOPIC\_ORDER_GRAY来给灰度环境应用。
发送方在发送时,对灰度的音讯写入到影子Topic中。生产时,灰度环境只应用灰度的分组订阅灰度的Topic。
3.2 Tag的计划
发送方在发送时,对灰度环境产生的音讯的Tag加注灰度标识。生产方,每次灰度版本公布时只订阅灰度Tag的音讯,失常的版本订阅非灰度的Tag。
3.3 UserProperty的计划
发送方在发送时,对灰度环境产生的音讯的UserProperty加注灰度标识。生产方的客户端须要进行改写,依据UserProperty来过滤,失常的版本会跳过这类音讯,灰度的版本会解决灰度音讯。
3.4 以后的计划缺点
以上三种计划各自的劣势在这里不做比拟,但都存在以下独特的缺点(也有其它的缺点或开发诉求,但不致命),无奈真正实现灰度状态切换回失常状态时音讯不失落解决,导致整个灰度计划都是从入门到放弃的过程:
- 因为应用不同的生产组,那么灰度版本验证通过后,如何正确地连接回原失常版本的生产组的生产位移,做到高效地不失落信息处理呢?
- 灰度的音讯如何确保精确地生产结束,做到落在灰度标识的音讯做到高效地不失落信息处理呢?
- 开启灰度时,灰度音讯的位点从那里开始?状态的细节化如何管控?
四、鲁班MQ平台的灰度计划
实质上,MQ灰度问题的外围就是高效地将灰度与非灰度的音讯隔离开,生产方依照本人的需要来精确获取到对应版本的音讯;当灰度实现后,可能正确地拼接回来音讯的位移,做到不失落解决必要的音讯,也就是状态细节上的治理。为了实现这个目标,本计划别离在以下几点进行了革新。
本计划中波及到的代码为测试代码,次要用于阐明计划,理论代码会更精密解决。
4.1 Queue的隔离应用
(图4.1 Queue的辨别应用)
咱们曾经晓得了Queue是topic的理论执行单元,咱们的思路就是借助Queue来实现v1(失常)音讯、v2(灰度)音讯的辨别,咱们能够固定首尾两条【可配置】Queue专门用来发送与接管灰度的音讯,其余的Queue用来发送失常的线上音讯。咱们应用雷同的生产组(也就是和业界的通用计划不一样,咱们会应用雷同的GroupID),让灰度消费者参加灰度Queue的重均衡,非灰度消费者参加非灰度Queue的重均衡。
这里咱们解决了音讯的存储隔离问题。
4.2 Broker订阅关系革新
灰度版本往往须要变更Topic或Tag,因为咱们没有新增独立的灰度生产组,当灰度版本变更Topic/Tag时,生产组内订阅关系就会不统一,前文也简略解释了订阅关系一致性的原理,咱们须要在Broker做出对应的革新,来兼容灰度与非灰度订阅关系不统一的状况。
同一生产组的订阅信息会在保护在ConsumerGroupInfo的subscriptionTable中,能够在ConsumerGroupInfo中减少创立一份graySubscriptionTable用来存储灰度版本的订阅信息,客户端向Broker发送的心跳包会革新成带有本身的灰度标记grayFlag,依据灰度标记grayFlag来抉择订阅关系存储在subscriptionTable还是graySubscriptionTable;在拉取音讯时,同样向Broker传来grayFlag来抉择从subscriptionTable还是graySubscriptionTable中获取对应的订阅信息。
这里咱们解决了生产订阅一致性问题。
4.3 Producer的革新
发送方的革新绝对简洁,只需确定发送的音讯是否为灰度音讯,通过实现MessageQueueSelector接口,将灰度音讯投递到指定数量的灰度Queue即可。这里咱们把用于灰度的grayQueueSize定义到配置核心中去,以后更多是约定应用Broker的指定Queue号作为灰度应用。
TOPIC\_V\_ORDER共有6条Queue,如图4.2所示,灰度音讯只会发送至首尾的0号与5号Queue,非灰度音讯则会抉择其余的4条Queue发送音讯。
(图4.2 发送后果)
这里咱们解决了生产者正确投递的问题。
4.4 Consumer的革新
生产方波及的革新点次要是灰度Queue与非灰度Queue的重均衡调配策略,与各个客户端灰度标记grayFlag的更新与同步。
灰度重均衡策略的外围就是分类解决灰度和非灰度的Queue,要将灰度的Queue调配至灰度ClientID,将非灰度的Queue调配至非灰度的ClientID,因而,在重均衡之前,会通过Namesrv获取同组内的所有客户端clientId对应最新的grayFlag(也就是状态会记录到Namesrv)。
当灰度版本须要变更为线上版本时,各客户端会同步grayFlag到Namesrv,同时,为了防止灰度音讯还未生产实现,在更新grayFlag之前会先判断灰度Queue中是否存在未生产的音讯,在保障灰度音讯生产实现后才会进行grayFlag的更新。
消费者需应用AllocateMessageQueueGray作为重均衡策略,传入灰度Queue的数量,灰度消费者setGrayFlag为true,能够看出只生产了首尾的0号与5号Queue的音讯,非灰度消费者setGrayFlag为false,能够看出只会生产两头的4条Queue的音讯,在控制台也能够十分清晰的看到Queue的调配后果,grayFlag为true的v2客户端调配到了首尾的Queue,grayFlag为false的v1客户端则调配到了两头的4条Queue。
(图4.3 生产与订阅后果)
当灰度版本须要切换至线上版本时,只需调用updateClientGrayFlag来更新状态即可,能够看出在调用updateClientGrayFlag后,原先v2的两个灰度客户端在生产完灰度Queue的音讯后,grayFlag才真正变为false【状态在namesrv保留】,退出到两头的4条非灰度Queue的重均衡中,原先首尾的2条灰度Queue则没有消费者订阅。
(图4.4 grayFlag更新)
这里咱们解决了状态切换的细节管制解决问题。
4.5 Namesrv的革新
前文提到过,消费者在重均衡时是须要获取组内所有客户端的灰度标识grayFlag,因而,咱们须要一个中央来长久化存储这些grayFlag,这个中央是每个消费者都能够拜访的,咱们抉择将这些信息存储在Namesrv。
- Namesrv绝对比拟轻量,稳定性很好;
- 消费者自身就会与Namesrv建设长连贯,如果该namesrv挂掉,消费者会主动连贯下一个Namesrv,直到有可用连贯为止;
- Broker是理论存储音讯的中央,本身运行压力就绝对较大,用来做灰度数据的同步肯定水平上会加大Broker的压力。
然而Namesrv自身是无状态的节点,节点之间是不会进行信息同步的,灰度数据的一致性须要借助数据库来保障,Namesrv独特拜访同一套数据库就好了,数据库长久化存储灰度信息,每次更新v1、v2的灰度状态时,通过Namesrv批改数据库的数据,在每次重均衡之前,再通过Namesrv拉取本人生产组内的所有实例的灰度状态即可。
(图4.5 Namesrv存储灰度数据示意图)
这里咱们解决了状态存储与同步的问题。
五、灰度场景的校验
测试是校验计划可行性的真谛,上面用简略的demo来验证鲁班平台的MQ灰度计划。
5.1 灰度版本Topic & Tag不变
这种场景在4.3、4.4时曾经做了验证,不再赘述。
5.2 灰度版本Topic减少
假如v1、v2的订阅信息如表5.1所示,则Topic订阅后果如图5.1所示,TOPIC\_V\_ORDER被v1、v2同时订阅,首尾两条Queue调配给灰度v2的客户端,两头4条Queue则调配给非灰度v1的客户端;TOPIC\_V\_PAYMENT只被灰度版本v2订阅,则只会将首尾两条Queue调配给v2的客户端,其余四条Queue不会被客户端订阅。咱们向TOPIC\_V\_ORDER别离发送4条非灰度音讯和灰度音讯,向TOPIC\_V\_PAYMENT发送4条灰度音讯,从图5.2中能够看出TOPIC\_V\_ORDER中的非灰度音讯由v1的两个客户端胜利生产,TOPIC\_V\_ORDER与TOPIC\_V\_PAYMENT的灰度音讯则由v2的两个客户端胜利生产。
(表5.1 订阅信息表)
(图5.1 订阅后果)
(图5.2 生产后果)
5.3 灰度版本Topic缩小
假如v1、v2的订阅信息如表5.2所示,则Topic订阅后果如图5.3所示,TOPIC\_V\_ORDER被v1、v2同时订阅,首尾两条Queue调配给灰度v2的客户端,两头4条Queue则调配给非灰度v1的客户端;TOPIC\_V\_PAYMENT只被非灰度版本v1订阅,则只会将两头的四条Queue调配给v1的客户端,首尾两条Queue不会被客户端订阅。咱们向TOPIC\_V\_ORDER别离发送4条非灰度音讯和灰度音讯,向TOPIC\_V\_PAYMENT发送4条非灰度音讯,从图5.4中能够看出TOPIC\_V\_ORDER与TOPIC\_V\_PAYMENT的非灰度音讯由v1的两个客户端胜利生产,TOPIC\_V\_ORDER中的灰度音讯则由v2的两个客户端胜利生产。
(表5.2 订阅信息表)
(图5.3 订阅后果)
(图5.4 生产后果)
5.4 灰度版本Tag变动
假如v1、v2的订阅信息如表5.3所示,则Topic订阅后果如图5.5所示,TOPIC\_V\_ORDER被v1、v2同时订阅,首尾两条Queue调配给灰度v2的客户端,两头4条Queue则调配给非灰度v1的客户端,咱们向TOPIC\_V\_ORDER别离发送4条Tag=v1的非灰度音讯和Tag=v2的灰度音讯,从图5.6中能够看出Tag为v1的非灰度音讯由v1的两个客户端胜利生产,Tag为v2的灰度音讯则由v2的两个客户端胜利生产。
(表5.3 订阅信息表)
(图5.5 订阅后果)
(图5.6 生产后果)
5.5 灰度版本Topic & Tag混合变动
假如v1、v2的订阅信息如表5.4所示,则Topic订阅后果如图5.7所示,与5.2状况雷同不再赘述。咱们向TOPIC\_V\_ORDER别离发送4条Tag=v1的非灰度音讯和Tag=v2的灰度音讯,向TOPIC\_V\_PAYMENT发送4条灰度音讯,生产后果如图5.8所示,能够看出v2的两个客户端胜利生产了TOPIC\_V\_PAYMENT及TOPIC\_V\_ORDER中Tag=v2的灰度音讯,而v1的两个客户端则只生产了TOPIC\_V\_ORDER中Tag=v1的非灰度音讯。
(表5.4 订阅信息表)
(图5.7 订阅后果)
(图5.8 生产后果)
六、结语
理论的MQ灰度版本,咱们还对MQ的发送与生产方做了对立的封装,业务方只需配置graySwitch、grayFlag即可,graySwtich标记是否须要开启灰度音讯,在graySwitch开启的前提下,grayFlag才会失效,用来标记以后客户端是否为灰度客户端。
在多零碎交互时,业务零碎可通过开关graySwitch来管制是否全量生产其余零碎的灰度与非灰度音讯,通过grayFlag来管制是独自生产灰度音讯还是非灰度音讯。graySwitch、grayFlag参数可放在配置核心做到热失效,当须要切换灰度流量时,可开发相应的脚本统一化更改grayFlag,实现全链路灰度流量的无损切换。
另外,咱们对于切换状态借助Namesrv做了充沛细节上的管制,保障在真正执行切换前,未生产完的音讯会被生产结束才真正的执行切换。
在此,也非常感谢阿里开源RocketMQ这个消息中间件!
作者:vivo流程IT团队-Ou Erli、Xiong Huanxin