消息中间件在分布式架构体系中的应用非常广泛,要想实现蓝绿发布,只在微服务调用层面实现还远远不够。在进行具体的方案设计之前,我们还是先来看一下我们这个项目中消息中间件的部署情况:
这里有四个应用,简单解释一下。
- 应用 1 支持蓝绿发布,并且处理完业务后,需要向消息中间件中的 topic_A 主题发送消息。
- 应用 2 不支持蓝绿发布,但同样需要在处理完业务后,向消息中间件中的 topic_A 发送消息。
- 应用 3 不支持蓝绿发布,需要处理完业务逻辑后,向消息中间件中的主题 topic-B 发送消息。
- 应用 4 中创建了两个消费组,其中 consumer_group_a 订阅 topicA,支持接入蓝绿;而 consumer_group_b 没有接入蓝绿。
那么怎么基于这一条件来设计和实施蓝绿方案呢?这又涉及到一个隔离机制的问题。因为无论是蓝绿发布还是全链路压测,需要着重解决的一个问题就是消息的隔离性。蓝绿发布的本质就是对消息进行分类,蓝颜色的消息只能被蓝颜色的消费者消费,绿颜色的消息只能被绿颜色的消费者消费。
消息中间件领域通常有“基于消息主题”和“基于消息属性”两种隔离机制。我们先来看第一种隔离机制,基于消息主题的物理隔离机制:
基于主题的隔离机制在消息服务端是分开存储的,属于物理层面的隔离。在消息消费端,由于应用使用不同的消费组进行消费,每一个消费组在物理层面也是互不影响的,每一个消费组有独立的线程池、消费进度等。
消息中间件中的另外一种隔离机制是基于消息属性的。例如,蓝绿两种颜色的消息使用的是同一个主题,但我们可以在消息中添加一个属性,标识这条消息的颜色。其存储示意图如下:
这样,不同属性的消息就可以共用一个主题了。消息发送端在发送消息时,会为消息设置相应的属性,将它存储到消息的属性中。然后单个消费端应用会创建蓝绿两个消费组,都订阅同一个主题。消费组拉取到消息后,需要先解码找到对应的消息属性,蓝颜色消费者只真正处理属性为 BLUE 的消息,那些属性为 GREEN 的消息会默认向服务端返回“消费成功”。这样就在客户端实现了消息过滤机制。
目前主流消息中间件的隔离机制都是基于消息属性的。
基于消息属性的蓝绿设计方案
基于消息属性的隔离机制的一个显著的特点是,蓝绿消息使用的是同一个主题。因此我们需要在不同环境的生产者发送消息时,为消息设置不同的颜色。
和在微服务领域实现蓝绿发布一样,我们通过系统参数为应用设置所属环境:
通常每一家公司都会有一个统一的开发框架,会基于目前主流的 RocketMQ、Kafka 客户端进行封装,或者使用类似 rocketmq-spring 这样的开源类库。为了防止对业务代码进行侵入,通常会采用拦截器机制,拦截消息发送 API,然后在拦截器中根据系统参数,为消息设置对应的属性。从系统参数中获取颜色值的示例代码如下:
private static final String COLOR_SYS_PROP = "color";
private static final String COLOR_ENV = System.getProperty(COLOR_SYS_PROP, "");
当不同环境的消息发送者将消息发送到消息服务器后,消费端就要按颜色将消费分开了。虽然消费端的隔离机制是通过不同的消费组来实现的,每一个消费组拥有自己独立的消费者线程池、消费进度,组与组之间互不影响。但是消费端不能简单粗暴地用系统参数来区分消费组的颜色,因为一个应用中可能存在多个消费组,这些消费组并不都开启了蓝绿机制。
所以基于消费组的蓝绿定义,首先需要在消费者的元信息中定义。例如,我们公司在申请消费组时,可以根据环境为消费组设置是否启用蓝绿机制。如下图所示:
蓝绿发布状态可选择:蓝、绿、所有。这里的“所有”表示消费组未开启蓝绿,选择“蓝”或“绿”都表示消费组开启蓝绿。
消费组是如何进行消息过滤的呢?我们来看下部署示意图:
我们看应用 3 会部署在蓝、绿两个环境,但是在原始的镜头项目代码中我们只会定义一个基本的消费组,例如 dw_test_consumer_group,蓝绿发布要求我们这套代码用不同的系统属性定义后,就能分别实现消息的过滤。
那我们如何动态开启蓝绿发布机制呢?我总结了下面两个实现要点。
应用启动时,首先获取系统参数 color 的值(如果有设置),并根据设置的值改写原消费组的名称。如果 color 的值为 BLUE,那我们在调用 RocketMQ 底层 DefaultMqPushConsumer 时,传入的消费组名称为 _BLUE_dw_test_consumer_group;如果 color 的值为 GREEN,那最终会创建的消费组名称就是 _GREEN_dw_test_consumer_group。
消费者启动后开始处理消费,在真正调用用户定义的消息业务处理器(MessageListener)之前,需要将消息进行解码,然后提取消息属性中 color 的值,用 mqProColor 表示,如果 mqProColor 的值与系统参数 color 中的值相等,就调用用户定义的消息业务处理器。否则就认为消费成功,直接给 MQ 服务器返回“成功”,相当于跳过这条消息的处理。
这么乍一看,蓝颜色的消费者消费 color=BLUE 的消息,绿颜色的消费者消费 color=GREEN 的消息,这不是很“完美”地解决了蓝绿发布的问题了吗?
事实不是这样的。因为 topic 中发送的消息有可能不带颜色,例如应用 -1 需要发送消息到 TOPIC_A 中, 这个应用接入了蓝绿,会发送蓝色或者绿颜色的消息。但应用 -2 没有接入蓝绿,所以应用 -2 发送的消息是不包含颜色的。按照上面的方案,这部分消息将无法被消费,最终结果就是:消息丢失。
那怎么解决消息消费丢失的问题呢?
我们可以在消费组元信息中定义不带颜色的消息由哪个环境来消费。我在公司实践时,消费者的蓝绿发布状态有下面三个值。
- 所有: 表示该消费组未接入蓝绿。
- 蓝:表示该消费组接入蓝绿,并且消息属性中未带颜色的消息由蓝环境的消费者进行消费。
- 绿:表示该消费组接入蓝绿,并且消息属性中未带颜色的消息由绿环境的消费者进行消费。
这样定义了之后,应用启动时,如果消费者的蓝绿状态为蓝,我们会同时启动两个消费组,一个消费组为 _BLUE_dw_test_consumer_group,用来专门消费蓝颜色的消费者;另外一个消费组为 dw_test_consumer_group,用来消费不带颜色的消息。蓝环境的应用在启动时只会创建一个消费组,那就是 _GREEN_dw_test_consumer_group。
同时,我们还支持在蓝绿之间进行切换。如果将消费组的蓝绿状态由 BLUE 变为 GREEN,我们会将原本在蓝环境的 dw_test_consumer_group 关闭,然后在绿环境中新增一个 dw_test_consumer_group 消费组。这样,我们就在消息中间件层面实现了蓝绿发布。
实现蓝绿的关键其实最终都落在了“如何有效隔离消息”这个问题上。基于消息属性的隔离,是在发送端使用一个主题,在每一条消息中添加一个属性 color 来存储消息的颜色,而消费端采取不同的消费组来消费消息。其中,蓝颜色的消息由蓝消费组消费,绿颜色的消息由绿消费组消费,没有颜色的消息由默认消费组来消费。这本质上是在消费端将数据从服务端全量拉取下来,然后在消费端进行了一层过滤,各个消费组都会读取到很多无效数据,无形中放大了拉取消息的调用次数。
而基于主题的隔离机制,是在消息发送时就将消息分别发送到不同的主题中,在消费端对各个消费组进行分工。蓝颜色的消费组只订阅蓝颜色主题,绿颜色的消费者只订阅绿颜色的主题,这就实现了有针对性的消费,效率更高。
此文章为5月Day25学习笔记,内容来源于极客时间《中间件核心技术与实战》,强烈推荐该课程