消息队列在线迁移实战 | 最佳实践

简介: 如何让MQ的在线迁移同时满足多个重要的需求呢?

前言

消息队列(Message Queue,下文简称MQ)是分布式互联网架构中必不可少的核心组件,包括RocketMQ、Kafka、RabbitMQ等在业界广泛使用的产品,在消息分发、异步解耦、削峰填谷、广播通知等领域发挥着巨大的作用。

在MQ的使用过程中,在线对MQ组件进行迁移是一个非常普遍的需求,在如下的几个场景中,都会涉及到MQ的在线迁移:

(1)规格升级。比如将3 Broker的Kafka集群替换成6 Broker的Kafka集群。

(2)更换另一种MQ产品。比如将RabbitMQ替换为性能和扩展性更强的RocketMQ。

(3)使用云服务替换自建MQ集群。比如将自建的RocketMQ集群替换为云上商业版RocketMQ服务。

在MQ迁移的过程中,存在3个非常重要的需求:

  • 操作简单。
  • 风险可控。
  • 不影响业务系统的正常运行。

如何让MQ的在线迁移同时满足这3个重要的需求呢?本文将对几种可行的方式进行深入探讨。

理论基础

在涉及到MQ在线迁移的所有方案中,都存有一个很重要的原则:对于发往MQ的每一条消息,如果已经被它的消费者成功接收并得到处理,这条消息就不再具有业务含义。已经被成功接收并得到处理的消息,只体现出统计方面的价值,并不需要随着MQ本身的迁移而进行数据迁移。

在数据库迁移的场景中,新旧DB之间的数据迁移是非常重要的工作,这是因为DB中的数据是持久化的数据,需要伴随着数据库的生命周期而长期存在。

而对于MQ而言,消息一旦被消息者接收并得到处理,就不再是持久化的数据,可以直接删除或归档。因此在MQ在线迁移的场景中,对已经处理过的消息,是没有数据迁移必要的。这样就将问题简化为:如何在迁移的过程中确保每一条消息被成功接收并得到处理。

在系统维护期停业务迁移

我们先从一个最简单方案开始体验MQ在线迁移是如何进行的。对于不要求7*24小时连续运行的业务系统,可以利用系统维护期的时间窗口,通过停业务的方式来实现消息队列的迁移。这种迁移方式因为不需要保证业务的连续性,操作起来就非常简单。

(1)初始状态

image.png

进入维护窗口期后,关闭生产者应用,这个时候不会再有的新的消息写入MQ:

image.png

在这个状态下保持一段时间,当MQ上的所有消息都被消费者接收并成功处理后,就可以对消费者进行版本发布,使其从新的MQ上接收消息:

image.png

接下来再启动生产者,使其指向新的MQ,整个操作就已经完成。当系统运行稳定后,可以对原MQ实例进行相关数据归档后直接下线:

image.png

在停业务迁移方案中,最关键一步,在于如何在系统维护窗口期之内,确保原MQ上的所有消息被消费者接收并成功处理。因为生产者关闭之后,不会有新的消息写入原MQ,只要预留足够长的时间,原MQ上堆积的消息一定会被消费者取走。在这个方案中,新MQ和原MQ也可以是不同的产品,比如从RabbitMQ迁移到RocketMQ也是可以支持的,因为生产者和消费者都经过了版本发布的动作,只需要在新版本中对API和收发逻辑进行修改就可以实现。

双订阅方案

在互联网领域,能够容忍维护期将业务暂停的系统越来越少了,7*24不间断服务是行业的趋势,上述的停业务迁移方案就不再适用了,如何在MQ迁移的过程中确保业务持续运行呢?

有一个非常棒的idea是让消费者同时具有从原MQ和新MQ接收消息的能力,这样不管生产方往哪一个MQ发送消息,都能够确保消息得到及时的处理。这是一种不需要暂停业务的方案,我们来看一下具体的步骤。

首先对消费者进行改造,使其同时连上新老两个MQ,具备同时从新老MQ接收消息的能力,这就是所谓的“双订阅”:

image.png

接下来对生产者进行发布,使其往新MQ发送消息,等原MQ上堆积的所有消息被消费者接收并成功处理后,就可以对原MQ下线:

image.png

在对原MQ下线的时候,因为消费者还保持着双订阅的状态,所以最好先切断消费者与原MQ的连接,再关闭原MQ,否则会造成一些异常(取决于SDK的实现)。如果消费者的订阅逻辑实现的足够优雅,可以在不重启消费者的情况下,通过一个指令在线切断消费者与原MQ的连接。

这个方案看似可以用一种对业务无损的方式在线迁移MQ,在实际操作中可行性却很低,其根本原因在于:同时从两个MQ接收消息的改造工作量极大。一般情况下,每一个消费者都在引入MQ产品对应的SDK,并通过MQ提供接入点与MQ建立连接后,接下来就只需要围绕业务逻辑完成所需要的消息订阅操作。这个时候要想同时从一个新的MQ接入消息,需要在代码层面对所有的订阅逻辑进行改造,这是一项非常复杂的工作。

在新MQ和原MQ是不同消息队列产品的情况下,消费者需要同时引入两套不同的SDK,改造难度会变得更大。基于双订阅方案完成MQ的迁移后,还需要考虑将来清理掉消费者从原MQ接收处理消息的遗留代码,这也是需要一定工作量的。

如果在MQ和消费者中间,能有一个中间件来实现双订阅的逻辑,是不是消费者的代码就不需要改造呢?答案是肯定的。但引入这样的中间件本身就是一项非常有挑战的工作,还增加了整个系统的复杂度,如果仅仅是为了MQ的在线迁移而引入一个新的组件,是得不偿失的。

基于工作量和风险的考虑,尽量不要使用双订阅方案。

最优方案

双订阅的本质,在于存在一个消费者可以同时接收新旧两个MQ消息的中间状态,在这个状态下,不管生产者往哪个MQ发送消息,消息都可以得到及时的处理。能不能有一种更简单的方式让消费者可以同时接收新旧两个MQ的消息呢?当然有,而且实现方式更加的简单。

在一个可靠的分布式微服务系统中,应用都可以通过增加节点的方式而进行水平扩容,为了确保整套系统的高可用性,每一个应用都不应该长期处于单实例运行状态,而是通过多个无状态的应用实例组成一个应用集群。因此,在真实环境下,不管是消息的生产者还是消费者,都至少有2个实例在运行。在迁移MQ的过程中,“消费者可以同时接收新旧两个MQ的消息”的中间状态,并不一定要让消费者的每一个实例都通过双订阅来实现,其实只要让一部分实例从原MQ接收消息,另一部分实例从新MQ接收消息就能满足了。通过这样的思路,能极大程度上简化MQ迁移的工作量,而且在迁移的过程中确保业务不受任何影响。

在迁移之前,需要先把元数据信息从原MQ复制到新MQ集群,不同的消息队列产品之前元数据的格式不一样,需要根据业务场景进行元数据的转换,元数据包括Topic、Queue、消费组、基础配置等信息。

image.png

首先,通过灰度发布机制,让一部分消费实例连上新的MQ。如果之前的消费者是单实例,这个时候也可以增加一个新的消费实例来完成这个步骤:

image.png

接下来,让生产者往新的MQ发送消息,这个操作并不一定需要采取一刀切的方式,也可以通过灰度发布的方式让消息的转向逐步转移到新的MQ上来。最终,原MQ将不再接收新的消息,它上面堆积的消息总将会被成功接收处理,这个时候可以继续通过灰度发布的方式解除消费者与原MQ的连接,连接全部解除完之后,原MQ就可以关闭了。

image.png

在这个方案中,对于单个的生产者和消费者,都不存在同时连接新旧两个MQ的情况,因此在改造工作量非常小。而且迁移的过程通过灰度的方式实现,既不会影响业务,又可以进一步的降低风险,是消息队列在线迁移的通用方案。

更复杂的场景

在实际的系统架构中,应用之间的关系会更为复杂,主要体现在3个方面:

  1. 整个系统中存在多个应用以及消息队列之间的相互交互,形成复杂的网状结构。
  2. 两个应用互为生产者消费者。
  3. 多个应用订阅同一个主题,从而发往该主题的每个消息都会被多个应用收到。

image.png

在这种复杂的场景中,本文推荐的消息队列迁移最优方案是否还能适用呢?答案是肯定的。任何一个大型系统都是由若干子系统构成,只要我们按照一定的规律和顺序,对其中的子系统逐个击破,就能化繁为简,解决问题。

首先,我们需要对整个系统进行梳理,找到每一个应用之间的相互关系,画出一张完整的架构图是迁移过程中必不可少的步骤。接下来,对其中需要迁移的消息队列进行编号,严格按照顺序执行在线迁移操作,对每一个消息队列的迁移,都遵循本文所提到的最优方案依次进行。一个迁移队列完全迁移成功后,再迁移下一个消息队列,避免彼此的操作步骤当中存在重叠交错的部分。

对于两个应用互为生产者和消费者的情况,我们应该将迁移工作按照消息的流向拆分成两步。比如对于示例中的应用A和应用B,可以先处理从A发往B的消息。同样,为了降低风险,确保迁移过程中业务不受到影响,灰度方案是有必要采用的,这样就需要消息方应用B拥有多个对等的实例。

image.png

对于多个应用订阅同一个主题的情况,也可以采用类似的思路进行处理,唯一需要注意的是原消息队列上的存留的消息,都被每一个消费者成功接收并成功消费之后,才能将原消息队列下线。

总结下来,消息队列在线迁移的完整步骤为:

  1. 对系统整体架构进行梳理,分步依次执行,避免不同的消息队列之间的迁移存在重叠。
  2. 元数据迁移。
  3. 确保存在一个中间状态:对于所有消费者,都有对等的实例同时连接新旧MQ。
  4. 让消息生产者往新的MQ发送消息。
  5. 等原MQ上所有存留的消息都被每一个消费者成功接收并成功消费之后,下线原MQ。

常见问题

问: 为什么不在新旧MQ之间进行消息数据的同步?

答:对于MQ而言,消息一旦被消息者接收并得到处理,就不再是持久化的数据,可以直接删除或归档。在迁移的过程中,新旧MQ之间消息数据同步是没有必要的,反而会增加迁移的难度,并导致消息被重复接收。

问:迁移过程中需要验证消息幂等性吗?

答: RocketMQ、Kafka等大多数消息队列产品都没有从消息队列服务端本身确保消息只投递一次,需要消费者自行实现对幂等性的保证。因此,不管在消息队列的迁移过程中,还是正常使用中,都应该借助数据库、Redis等外部系统确保消息的幂等性,否则会造成业务逻辑重复处理。

【更多精彩】

1.中间件爆款一折起,还有阿里巴巴十年最佳实践深度解密,点击马上了解https://www.aliyun.com/activity/daily/commercial?spm=5176.20960838.0.0.6a54305etoEn4D

2.【填问卷领淘公仔】点击马上填写问卷:
https://survey.aliyun.com/apps/zhiliao/YmW95Gk8bU

【加入行业实战交流钉钉群】

阿里云专门成立了“互联网架构升级实战课”钉钉群,每周邀请一位阿里云专家在群内进行行业最佳实践直播,每天分享行业前沿干货,钉钉扫码马上加入。
image.png

相关文章
|
2月前
|
消息中间件 NoSQL Java
Redis List:打造高效消息队列的秘密武器【redis实战 一】
Redis List:打造高效消息队列的秘密武器【redis实战 一】
111 0
|
6月前
|
消息中间件 存储 网络协议
企业实战(11)消息队列之Docker安装部署RabbitMQ实战
企业实战(11)消息队列之Docker安装部署RabbitMQ实战
122 0
|
2月前
|
消息中间件 NoSQL Java
Redis Streams在Spring Boot中的应用:构建可靠的消息队列解决方案【redis实战 二】
Redis Streams在Spring Boot中的应用:构建可靠的消息队列解决方案【redis实战 二】
208 1
|
1月前
|
消息中间件 存储 缓存
【Redis实战】有MQ为啥不用?用Redis作消息队列!?Redis作消息队列使用方法及底层原理高级进阶
【Redis实战】有MQ为啥不用?用Redis作消息队列!?Redis作消息队列使用方法及底层原理高级进阶
|
6月前
|
消息中间件 存储 监控
消息中间件第八讲:消息队列 RocketMQ 版实战、集群及原理
消息中间件第八讲:消息队列 RocketMQ 版实战、集群及原理
|
6月前
|
消息中间件 Java 数据安全/隐私保护
企业实战(12)消息队列之Docker安装部署ActiveMQ实战
企业实战(12)消息队列之Docker安装部署ActiveMQ实战
146 0
|
11月前
|
消息中间件 Cloud Native 中间件
带你读《企业级云原生白皮书项目实战》——4.1.3 消息队列RocketMQ版最佳实践(上)
带你读《企业级云原生白皮书项目实战》——4.1.3 消息队列RocketMQ版最佳实践(上)
234 0
|
11月前
|
消息中间件 存储 数据采集
带你读《企业级云原生白皮书项目实战》——4.1.3 消息队列RocketMQ版最佳实践(下)
带你读《企业级云原生白皮书项目实战》——4.1.3 消息队列RocketMQ版最佳实践(下)
220 0
|
11月前
|
消息中间件 存储 分布式计算
带你读《企业级云原生白皮书项目实战》——4.1.6 消息队列Kafka版最佳实践(上)
带你读《企业级云原生白皮书项目实战》——4.1.6 消息队列Kafka版最佳实践(上)
345 0
|
11月前
|
消息中间件 存储 分布式计算
带你读《企业级云原生白皮书项目实战》——4.1.6 消息队列Kafka版最佳实践(下)
带你读《企业级云原生白皮书项目实战》——4.1.6 消息队列Kafka版最佳实践(下)
309 0