vivo 基于原生 RabbitMQ 的高可用架构实践
一、背景说明
vivo 在 2016 年引入 RabbitMQ,基于开源 RabbitMQ 进行扩展,向业务提供消息中间件服务。
2016~2018 年,所有业务均使用一个集群,随着业务规模的增长,集群负载越来越重,集群故障频发。
2019 年,RabbitMQ 进入高可用建设阶段,完成了高可用组件 MQ 名字服务以及 RabbitMQ 集群的同城双活建设。
同时进行业务使用集群的物理拆分,严格按照集群负载情况和业务流量进行业务使用集群的分配以及动态调整。
在 2019 年高可用建设后至今,业务流量增加了十倍,集群未出现过严重故障。
RabbitMQ 是实现了 AMQP 协议的开源消息代理软件,起源于金融系统。
具有丰富的特性:
消息可靠性保证,RabbitMQ 通过发送确认保证消息发送可靠、通过集群化、消息持久化、镜像队列的方式保证消息在集群的可靠、通过消费确认保证消息消费的可靠性。
RabbitMQ 提供了多种语言的客户端。
提供了多种类型的 exchange,消息发送到集群后通过 exchange 路由到具体的 queue 中。
RabbitMQ 提供了完善的管理后台和管理 API,通过管理 API 可以快速与自建监控系统整合。
RabbitMQ 在具体实践中发现的问题:
为保障业务高可用使用多套集群进行物理隔离,多套集群无统一平台进行管理。
原生 RabbitMQ 客户端使用集群地址连接,使用多套集群时业务需要关心集群地址,使用混乱。
原生 RabbitMQ 仅有简单的用户名/密码验证,不对使用的业务应用方进行鉴权,不同业务容易混用 exchange/queue 信息,造成业务应用使用异常。
使用的业务应用方较多,无平台维护消息发送方、消费方的关联信息,多个版本迭代后无法确定对接方。
客户端无限流,业务突发异常流量冲击甚至击垮集群。
客户端无异常消息重发策略,需要使用方实现。
集群出现内存溢出等造成集群阻塞时无法快速自动转移到其它可用集群。
使用镜像队列,队列的 master 节点会落在具体某个节点上,在集群队列数较多时,容易出现节点负载不均衡的情况。
RabbitMQ 无队列自动平衡能力,在队列较多时容易出现集群节点负载不均问题。
二、整体架构
1、MQ-Portal--支持应用使用申请
过往业务团队适用 RabbitMQ 时,应用申请的流量以及对接的应用等信息都在线下表格记录,较为零散,更新不及时,无法准确了解业务当前真实的使用情况,因此通过一个接入申请的流程可视化、平台化建立应用使用的元数据信息。
通过 MQ-Portal 的申请流程(如上图),确定了消息发送应用、消费应用、使用 exchange/queue、发送流量等信息使用申请提交后将进入 vivo 内部工单流程进行审批。
工单流程审批通过后,通过工单的接口回调,分配应用具体使用的集群,并在集群上创建 exchange/queue 已经绑定关系。
由于采用多集群物理隔离的方式保证业务在正式环境的高可用,无法简单通过一个 exchange/queue 的名称定位到使用的集群。
每一个 exchange/queue 与集群之间通过唯一的一对 rmq.topic.key 与 rmq.secret.key 进行关联,这样 SDK 启动过程中即可定位到具体使用的集群。
rmq.topic.key 与 rmq.secret.key 将在工单的回调接口中进行分配。
2、客户端 SDK 能力概述
客户端 SDK 基于 spring-message 和 spring-rabbit 进行封装,并在此基础上提供了应用使用鉴权、集群寻址、客户端限流、生产消费重置、阻塞转移等能力。
2.1、应用使用鉴权
开源 RabbitMQ 仅通过用户名密码的方式判断是否允许连接集群,但是应用是否允许使用 exchange/queue 是未进行校验的。
为了避免不同业务混用 exchange/queue,需要对应用进行使用鉴权。
应用鉴权由 SDK 和 MQ-NameServer 协同完成。
应用启动时首先会上报应用配置的 rmq.topic.key 信息到 MQ-NameServer,由 MQ-NameServer 判断使用应用与申请应用是否一致,并且在 SDK 发送消息过程中还会进行二次校验。
2.2、集群寻址
前文说过,应用使用 RabbitMQ 严格按照集群的负载情况和业务流量进行集群的分配,因此具体某个应用使用的的不同的 exchange/queue 可能是分配在不同的集群上的。
为了提升业务的开发效率, 需要屏蔽多集群对业务的影响,因此按照应用配置的 rmq.topic.key 信息进行集群的自动寻址。
2.3、客户端限流
原生 SDK 客户端不进行发送流量限流,在部分应用存在异常持续向 MQ 发送消息时,可能会冲垮 MQ 集群。并且一个集群为多应用共同使用,单一应用造成集群影响将会影响使用异常集群的所有应用。
因此需要在 SDK 中提供客户端限流的能力,必要时可以限制应用向集群发送消息,保障集群的稳定。
2.4、生产消费重置
(1)随着业务规模增长,集群负载持续增加,此时需要进行集群的业务拆分。为了减少在拆分过程中避免业务重启,需要有生产消费重置功能。
(2)集群出现异常,可能会造成消费者掉线,此时通过生产消费重置可以快速拉起业务消费。
为了实现生产消费重置,需要实现一下流程:
重置连接工厂连接参数
重置连接
建立新的连接
重新启动生产消费
同时 MQ-SDK 中有异常消息重发策略,可以避免在生产重置过程中导致的消息发送异常。
2.5、阻塞转移
RabbitMQ 在内存使用超过 40%,或是磁盘使用超限制时会阻塞消息发送。
由于 vivo 中间件团队已经完成了 RabbitMQ 同城双活的建设,因此在出现一个集群发送阻塞时可以通过生产消费重置到双活集群完成阻塞的快速转移。
2.6、多集群调度
随着应用的发展,单集群将无法满足应用的流量需求,并且集群队列均为镜像队列,无法简单的通过增加集群节点的方式实现业务支撑流量单集群的水平扩容。
因此需要 SDK 支持多集群调度能力,通过将流量分散到多个集群上满足业务大流量需求。
3、MQ-NameServer--支持 MQ-SDK 实现故障快速切换
MQ-NameServer 为无状态服务,通过集群部署即可保障自身高可用,主要用于解决以下问题:
MQ-SDK 启动鉴权以及应用使用集群定位。
处理 MQ-SDK 的定时指标上报(消息发送数量、消息消费数量),并且返回当前可用集群地址,确保 SDK 在集群异常时按照正确地址进行重连。
控制 MQ-SDK 进行生产消费重置。
4、MQ-Server 高可用部署实践
RabbitMQ 集群均采用同城双活部署架构,依靠 MQ-SDK 和 MQ-NameServer 提供的集群寻址、故障快速切换等能力保障集群的可用性。
4.1、集群脑裂问题处理
RabbitMQ 官方提供了三种集群脑裂恢复策略。
(1)ignore
忽略脑裂问题不处理,在出现脑裂时需要进行人为干预才可恢复。由于需要人为干预,可能会造成部分消息丢失,在网络非常可靠的情况可以使用。
(2)pause_minority
节点在与超过半数集群节点失联时将会自动暂停,直到检测到与集群超半数节点的通信恢复。极端情况下集群内所有节点均暂停,造成集群不可用。
(3)autoheal
少数派节点将自动重启,此策略主要用于优先保证服务的可用性,而不是数据的可靠性,因为重启节点上的消息会丢失。
由于 RabbitMQ 集群均为同城双活部署,即使单集群异常业务流量也可自动迁移到双活机房集群,因此选择使用了 pause_minority 策略避免脑裂问题。
2018 年多次因网络抖动造成集群脑裂,在修改集群脑裂恢复策略后,已未再出现脑裂问题。
4.2、集群高可用方案
RabbitMQ 采用集群化部署,并且因为集群脑裂恢复策略采用 pause_minority 模式,每个集群要求至少 3 个节点。
推荐使用 5 或 7 节点部署高可用集群,并且控制集群队列数量.
集群队列均为镜像队列,确保消息存在备份,避免节点异常导致消息丢失。
exchange、queue、消息均设置为持久化,避免节点异常重启消息丢失。
队列均设置为 lazy queues,减少节点内存使用的波动。
4.3、同城双活建设
双机房部署等价集群,并且通过 Federation 插件将双集群组成联盟集群。
本机房应用机器优先连接本机房 MQ 集群,避免因专线抖动造成应用使用异常。
通过 MQ-NameServer 心跳获取最新的可用集群信息,异常时重连到双活集群中,实现应用功能的快速恢复。
三、未来挑战与展望
目前对 RabbitMQ 的使用增强主要在 MQ-SDK 和 MQ-NameServer 侧,SDK 实现较为复杂,后期希望可以构建消息中间件的代理层,可以简化 SDK 并且对业务流量做更加细致化的管理。
作者:derek
版权声明: 本文为 InfoQ 作者【vivo互联网技术】的原创文章。
原文链接:【http://xie.infoq.cn/article/33e330125dc06e242c4bae00b】。文章转载请联系作者。
评论 (4 条评论)