历史原因,公司存在多个 MQ 同时使用的问题,我们中间件团队在去年下半年开始支持对 Kafka 和 Rabbit 能力的进行封装,初步能够完全支撑业务团队使用。
鉴于在之前已经基本完全实施 Kafka 管控平台、以及 Kafka 集群迁移管控,我们基本可以认为团队对于 Kafka 的把控能力初具规模。
因此,考虑到以下几点原因,我们决定对 RabbitMQ 不再做维护和支持。
原因#
使用混乱和维护困难#
基于我们的数据统计和分析发现,基本上没有服务使用我们自己封装的 RabbitMQ 能力,用到的基本上是spring-amqp
或者原生的Rabbit 使用方式,存在使用混乱,方式不统一的问题,对于排查问题方面存在更多的问题。
另外考虑到对于 MQ 能力支持要做双份,Kafka 和 Rabbit 都要支持相同的功能,对于人力资源方面存在浪费,当然也由于本身目前没有对 RabbitMQ 非常精通的同学,所以对于维护能力这方面存在担忧。
分区容错问题#
RabbitMQ 集群对于网络分区的容错性不高,根据调查发现,系统中 RabbitMQ 高可用方案使用镜像队列,而当 RabbitMQ 出现网络分区时,不同分区里的节点会认为不属于自身所在分区的节点都已经挂了,对于队列、交换器、绑定的操作仅对当前分区有效。
而且,如果原集群中配置了镜像队列,而这个镜像队列又牵涉两个或者更多个网络分区中的节点时,每一个网络分区中都会出现一个 master 节点,对于各个网络分区,此队列都是相互独立的。
在默认的情况下,架构本身存在脑裂的风险,在 3.1 版本下是无法自动恢复的,之后的版本才会自动探测网络分区,人工介入存在数据丢失的风险。
性能瓶颈#
镜像队列解决了 Rabbit 高可用的问题,但是并不能增加负载和性能,线上曾经出现过 RabbitMQ 在高流量下的性能问题,就是因为队列由单个节点承载流量,在高并发情况在集群中单个节点存在性能瓶颈。
即便我们目前大部分场景下 MQ 流量不高,但是一旦出现问题,将成为整个系统的性能瓶颈。
另外我们对 Rabbit 做了一些性能方面的测试:
测试集群一共有 4 台磁盘节点,其中每台 16 核,如果我们不做 Sharding,单队列高 TPS 在 5K 左右,如果是内存节点,官方可以给出的处理极限为 50K/s,如果做 Sharding,单队列处理能力可以达到 10K/s。
上述结论都是以消息能够被正常快速消费为前提,实际上在高流量或者大量消息积压的情况会导致集群性能急剧下降。
运维&管控#
基于以上现有的问题和难点,我们决定对 Rabbit 进行全量迁移至 Kafka,以便能在业务高速发展过程中能够保障对于稳定性、高可用、高性能方面的追求。
在方法论和理论体系层面,我们对业务生产有三板斧:可灰度、可监控、可回滚。
同样,对于消息中间件平台运维我们希望有三板斧:可运维、可观测、可管控,那么目前基于 Kafka 的集群管控和 Kafka Manager 的能力我们已经基本做到了上述几点。
- 高可用:根据自身经验,Kafka 本身拥有极高的平台可用性
- 高性能:Kafka 可支撑极高的 TPS,并且支持水平扩展,可快速满足业务的流量增长需求
- 功能支持:在原有两个 MQ 能力基础上,基础支持顺序消息、延时消息、灰度消息、消息轨迹等
- 运维管控:基于 Kafka Manager 基础上进行二次开发,丰富管控能力和运维支撑能力,提供给开发、运维、测试更好的使用体验和运维能力。
模型对比#
RabbitMQ#
Exchange:生产者将消息发送到Exchange,由交换器将消息通过匹配Exchange Type、Binding Key、Routing Key后路由到一个或者多个队列中。
Queue:用于存储消息,消费者直接绑定Queue进行消费消息
Routing Key:生产者发送消息给 Exchange 会指定一个Routing Key。
Binding Key:在绑定Exchange与Queue时会指定一个Binding Key。
Exchange Type:
- Direct:把消息路由到那些 Binding Key 和 Routing Key 完全匹配的队列中
- Fanout:把消息转发给所有与它绑定的队列上,相当于广播模式
- Topic:通过对消息的 Routing Key 和 Exchange、Queue 进行匹配,将消息路由给一个或多个队列,发布/订阅模式
- Headers:根据消息的 Header 将消息路由到不同的队列,和 Routing Key 无关
Kafka#
Topic:发送消息的主题,对消息的组织形式
Broker:Kafka 服务端
Consumer Group:消费者组
Partition:分区,topic 会由多个分区组成,通常每个分区的消息都是按照顺序读取的,不同的分区无法保证顺序性,分区也就是我们常说的数据分片sharding机制,主要目的就是为了提高系统的伸缩能力,通过分区,消息的读写可以负载均衡到多个不同的节点上
迁移方案#
综上,我们将要对系统中所有使用RabbitMQ的服务进行迁移操作,整个迁移我们应该保证以下 3 点:
- 操作便捷,不能过于复杂,复杂会带来更多的不可控风险
- 风险可控,尽大可能降低迁移对业务的影响
- 不影响业务正常运行
消费者双订阅#
- 对消费者进行改造,同时监听 Rabbit 和 Kafka 消息
- 对生产者进行改造,迁移至Kafka发送消息
- 等待 Rabbit 遗留消息消费完毕之后,直接下线即可
优点:可以做到无损迁移
缺点:
- 需要同时维护两套监听代码,可能有大量的工作量,迁移完成之后还需要再进行一次老代码下线
- 消息无法保证顺序性
基于灰度单订阅#
这是基于双订阅模式的优化,通过使用我们的灰度/蓝绿发布的能力,做到可以不双订阅,不用同时监听两个消息队列的消息。
- 直接修改消费者代码,发布灰度/蓝节点,监听 Kafka 消息
- 生产者改造,往 Kafka 发送消息
- 等待老的 Rabbit 消息消费完毕,下线,这里存在一个问题就是在进行灰度之后全量的过程中可能造成消息丢失的情况,对于这个问题的解决方案要区分来看,如果业务允许少量的丢失,那么直接全量即可,否则需要对业务做一定的改造,比如增加开关,全量之前关闭发送消息,等待存量消息消费完毕之后再全量。
优点:
- 基于双订阅方案改造,可以做到不同时监听两个队列的消息,减少工作量
- 可以做到无损迁移
缺点:同样无法保证消息有序性
实际场景问题#
上述只是针对现状的迁移方案考虑,那么还有一些跟实际和复杂的问题可能需要考虑。
比如消息的场景有可能不是这种简单的发布/订阅关系,可能存在网状、环状的发布/订阅关系,该如何处理?
其实是一样的道理,只要我们能够梳理清楚每个 Exchange 之间的发布/订阅的关系,针对每个 Exchange 进行操作,就能达到一样的平滑迁移效果。
我们要做的就是针对每个 Exchange 进行迁移,而不是针对服务,否则迁移是无法进行下去的,但是这样带来的另外一个问题就是每个服务需要发布多次,而且如果碰到多个复杂消费或者生产的情况要特别小心。
实施细节#
基于现状,我们对所有 Rabbit Exchange 的情况进行了详细的统计,将针对不同的 Exchange 和类型以及功能使用以下方式处理。
- 无用的Exchange、无生产者或者无消费者,还有没有任何流量的,可以直接删除
- Fanout 类型,Exchange 对应 Topic,Queue 对应 Consumer Group,还有存在使用随机队列的,需要对应多个Consumer Group(单独做一个简单的能力封装处理)
- Direct 类型,RoutingKey 对应 Topic,Queue 对应 Consumer Group
- Topic 类型,RoutingKey 对应 Topic,Queue 对应 Consumer Group,实际并未发现使用到通配符情况
- 延迟队列、重试等功能,基于 spring-kafka 做二次封装
验证&监控&灰度&回滚#
验证
- 迁移后针对 Rabbit 验证,通过管理平台流量或者日志输出来确认,而且现状是大部分 Exchange 流量都比较小,所以可能需要自行发送消息验证迁移效果。
- 迁移后针对 Kafka 流量进行验证可以通过 Kafka Manager 平台或者日志
监控
监控通过 Kafka Manager 平台或者现有监控
灰度
方案本身 Consumer 和 Producer 都可以直接灰度发布,预发验证
回滚
服务回滚,按照发布顺序控制回退顺序