三、业界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存储灰度数据示意图)
这里我们解决了状态存储与同步的问题。