微服务架构与MQ
RabbitMQ场景分析与优化
RabbitMQ在网易蜂巢中的应用和案例分享
1微服务架构与MQ
微服务架构是一种架构模式,它将单体应用划分成一组微小的服务,各服务之间使用轻量级的通信机制交互。
上图左边是单体架构应用,把所有业务功能放在单个进程中,需要扩展时以进程为单位水平复制到多台机器。
上图右边是微服务架构应用,将每个业务功能以独立进程(服务)的方式部署,可以按需将微服务分别部署在多台机器,实现水平扩展。
微服务各服务之间使用“轻量级”的通信机制。所谓轻量级,是指通信协议与语言无关、与平台无关。
微服务通信方式:
同步:RPC,REST等
异步:消息队列
同步通信方式
优点:
实现方便。
协议通用,比如HTTP大家都非常熟知。
系统架构简单,无需中间件代理。
缺点:
客户端耦合服务方。
通信双方必须同时在线,否则会造成阻塞。
客户端需要知道服务方的Endpoint,或者需要支持服务发现机制。
异步通信方式
优点:
系统解耦和。
通信双方可以互相对对方无感知。
缺点:
额外的编程复杂性。比如需要考虑消息可靠传输、高性能,以及编程模型的变化等。
额外的运维复杂性。比如消息中间件的稳定性、高可用性、扩展性等非功能特性。
今天的主题是消息队列在微服务架构中的应用与实践。
消息队列中间件如何选型?主要会考虑以下几点:
协议:AMQP、STOMP、MQTT、私有协议等
消息是否需要持久化
吞吐量
高可用支持,是否单点
分布式扩展能力
消息堆积能力和重放能力
开发便捷,易于维护
社区成熟度
选择RabbitMQ的原因:
开源,跨平台
灵活的消息路由策略
持久化,消息可靠传输
透明集群,HA支持
支持高性能高并发访问
支持多种消息协议
丰富的客户端、插件和平台支持
支持RPC解决方案
2RabbitMQ场景分析与优化
RabbitMQ是一个实现了AMQP(高级消息队列协议)协议的消息队列中间件。
AMQP基本模型:
1. Queue
2. Exchange: Direct, Fanout, Topic, Header
3. Binding: BindingKey, RouteKey
总结:生产者将消息发送到Exchange,Exchange通过匹配BindingKey和消息中的RouteKey来将消息路由到队列,最后队列将消息投递给消费者。
消息可靠传输是业务系统接入MQ时首先要考虑的问题。一般消息可靠性有三个等级:
At most once: 最多一次
At least once: 最少一次
Exactly once: 恰好一次
RabbitMQ支持其中的“最多一次”和“最少一次”两种。其中“最少一次”投递实现机制:
生产者confirm。如何开启:使用confirm.select
消息持久化。
消费者ack。如何开启:使用basic.consume(…, no-ack=false)
这里说下RabbitMQ消息持久化(写磁盘)的两个场景:
显式指定消息属性:delivery-mode=2
内存吃紧时,消息(包括非持久化消息)转存到磁盘,由memory_high_watermark_paging_ratio参数指定阈值。
RabbitMQ的消息持久化是通过以下机制实现的:
消息体写文件
异步刷盘,合并请求,减少fsync系统调用次数
当进程mailbox没有新消息时,实时刷盘
confirm机制下,等fsync系统调用完成后返回basic.ack确认
RabbitMQ开启消息可靠性参数需要注意:
unack消息在服务器端没有超时,只能等待客户端连接断开,重新入队等待投递。
消息存在重复投递的情况,需客户端去重:
a)基于业务层的MsgId。
b)基于RabbitMQ的Redelivered flag标记: 不完全靠谱,仍旧可能收到重复消息。
保障性能:
a)批量publish, ack。
b)更快的磁盘(SSD,RAID等)。
c)少堆积。
消息乱序。
生产者confirm机制是三个可靠性参数中对性能影响最大的。一般来说有三种编程方式:
普通confirm模式。每发送一条消息后,调用waitForConfirms()方法,等待服务器端confirm。实际上是一种串行confirm。
批量confirm模式。每次发送一批消息后,调用waitForConfirms()方法,等待服务器端confirm。
异步confirm模式。注册一个回调方法,服务器端confirm了一条(或多条)消息后SDK会回调这个方法。
下面是一些生产者confirm机制的专项性能测试数据:
总结:
遵循线程数越大,吞吐量越大的规律。当线程数量达到一个阈值之后,吞吐量开始下降。
不论哪种confirm模式,通过调整客户端线程数量,都可以达到一个最大吞吐量值。
异步和批量confirm模式两者没有明显的性能差距。所以,只需从可编程性的角度选择异步或批量或者两者结合的模式即可。相比而言,普通confirm模式只剩编程简单这个理由了。
下面讲下RabbitMQ的高可用机制。
官方提供的高可用方案:cluster + ha policy
cluster机制:多个全联通节点之间元信息(exchange、queue、binding等)保持强一致,但是队列消息只会存储在其中一个节点。
优点:提高吞吐量,部分解决扩展性问题。
缺点:不能提升数据可靠性和系统可用性。
ha policy机制:在cluster机制基础上可以指定集群内任意数量队列组成镜像队列,队列消息会在多节点间复制。实现数据高可靠和系统高可用。
设置参数:ha-mode和ha-params可以细粒度(哪些节点,哪些队列)设置镜像队列。
设置参数:ha-sync-mode=manual(默认)/automatic可以指定集群中新节点的数据同步策略。
有一点需要注意:镜像队列对网络抖动非常敏感,默认参数配置下,出现脑裂后RabbitMQ集群不会自我恢复,需要人工介入恢复,务必加好监控和报警。
RabbitMQ流控机制 流控类型:
内存流控:由vm_memory_high_watermark参数控制,默认0.4
磁盘流控:由disk_free_limit参数控制,默认50M
单条连接流控:触发条件是下游进程的处理速度跟不上上游进程。
RabbitMQ内部进程关系调用图
注意:当触发流控(全局内存/磁盘流控,单条连接流控)时,生产者端的publish方法会被阻塞,生产者需要做的是:
注册block事件,被流控时,会收到一个回调通知。
异步化处理生产者发送消息,不要阻塞主流程。
3RabbitMQ在网易蜂巢中的应用和案例分享
网易蜂巢平台的服务化架构如下,服务间通过RabbitMQ实现通信:
网易蜂巢消息服务器设计目标:实现一个路由灵活、数据可靠传输、高可用、可扩展的消息服务器。
设计要点:
Exchange类型为topic。
BindingKey就是队列名。
每个服务(图例中的REPO/CTRL/WEB等)启动后会初始化一条AMQP连接,由3个channel复用:一个channel负责生产消息,一个channel从TYPE(REPO/CTRL/WEB等)类型的队列消费消息,一个channel从TYPE.${hostname}类型的队列消费消息。
应用场景举例:
点对点(P2P)或请求有状态服务:消息的RouteKey设置为TYPE.${HOSTNAME}。如host1上的WEB服务需要向host2上的REPO服务发送消息,只需将消息的RouteKey设置为REPO.host2投递到Exchange即可。
请求无状态服务:如果服务提供方是无状态服务,服务调用方不关心由哪个服务进行响应,那么只需将消息的RouteKey设置为TYPE。比如CTRL是无状态服务,host1上的WEB服务只需将消息的RouteKey设置为CTRL即可。CTRL队列会以Round-Robin的均衡算法将消息投递给其中的一个消费者。
组播:如果服务调用方需要与某类服务的所有节点通信,可以将消息的RouteKey设置为TYPE.*,Exchange会将消息投递到所有TYPE.${HOSTNAME}队列。比如WEB服务需通知所有CTRL服务更新配置,只需将消息的RouteKey设置为CTRL.*。
广播:如果服务调用方需要与所有服务的所有节点通信,也就是说对当前系统内所有节点广播消息,可以将消息的RouteKey设置为*.*。
总结:通过对RouteKey和BindingKey的精心设计,可以满足点对点(私信)、组播、广播等业务场景的通信需求。
优缺点分析:
优点:
路由策略灵活。
支持负载均衡。
支持高可用部署。
支持消息可靠传输(生产者confirm,消费者ack,消息持久化)。
支持prefetch count,支持流控。
缺点:
存在消息重复投递的可能性。
超时管理,错误处理等需业务层处理。
对于多服务协作的场景支持度有限。比如以下场景:WEB=> CTRL=>REPO=>CTRL=> WEB。这个时候就需要CTRL缓存WEB请求,直至REPO响应。
实践案例分享
案例一:GC引起的MQ crash
1.环境参数:
11.png
2.现象:
RabbitMQ崩溃,产生的erl_crash.dump文件内容如下:
12.png
3.直接原因:
从数据来看,虚拟机内存共4G,Erlang虚拟机已占用约1.98G内存(其中分配给Erlang进程的占1.56G),此时仍向操作系统申请1.82G,因为操作系统本身以及其他服务也占用一些内存,当前系统已经分不出足够的内存了,所以Erlang虚拟机崩溃。
4.分析:
本例有两个不符合预期的数据:
1. 内存流控阈值控制在1.67G左右(4G*0.4),为什么崩溃时显示占用了1.98G?
2. 为什么Erlang虚拟机会额外再申请1.82G内存?
因为:
RabbitMQ每个队列设计为一个Erlang进程,由于进程GC也是采用分代策略,当新老生代一起参与Major GC时,Erlang虚拟机会新开内存,根据root set将存活的对象拷贝至新空间,这个过程会造成新老内存空间同时存在,极端情况下,一个队列可能短期内需要两倍的内存占用量。这就是RabbitMQ将内存流控的安全阈值默认设置为0.4的原因。
内存流控参数vm_memory_high_watermark 为0.4的意思是,当RabbitMQ的内存使用量大于40%时,开始进行生产者流控,但是该参数并不承诺RabbitMQ的内存使用率不大于40%。
5.如何解决(规避)?
RabbitMQ独立部署,不与其他Server共享内存资源。
进一步降低vm_memory_high_watermark值,比如设置成0.3,但是这种方式会造成内存资源利用率太低。
升级RabbitMQ至新版(3.4+),新版RabbitMQ对内存管理有过改进。
案例二:镜像队列的单节点磁盘故障造成消息丢失
1.环境参数:RabbitMQ: 3.1.5 (ha policy, ha-mode:all)
2.现象:
a)节点A的数据盘故障(磁盘控制器故障、无法读写),所有原本A上的生产者消费者failover到B节点。
b)从节点B的WebUI上看,所有队列信息(包括队列元信息和数据)丢失,但是exchange、binding、vhost等依旧存在。
c)节点B的日志中出现大量关于消费请求的错误日志:
13.png
d)从生产者端看来一切正常,依旧会收到来自节点B的confirm消息(basic.ack amqp方法)。
3.分析:
上述现象实际上有两个坑在里面:
在数据可靠传输方面,镜像队列也不完全可靠。这是一个Bug。RabbitMQ 3.5.1版本以下都存在这个问题。
要保证消息可靠性,生产者端仅仅采用confirm机制还不够。对于那些路由不可达的消息(根据RouteKey匹配不到相应队列),RabbitMQ会直接丢弃消息,同时confirm客户端。
4.如何解决(规避)?
升级RabbitMQ到新版,至少3.5.1及以上。
生产者basic.publish方法设置mandatory参数,它的作用是:对于那些路由不可达的消息,RabbitMQ会先通过basic.return消息向生产者返回消息内容,然后再发送basic.ack消息进行确认
Q & A
Q1:为保障消息队列稳定性,消息队列监控点主要有哪些?
A1:首先是服务器的基础监控是要的。CPU,内存,磁盘IO都是敏感项。特别是内存,报警阈值可能要设置在50%以下,原因前面也说过,极端情况下一个队列的内存占用量可能是两倍当前值。
然后MQ业务的监控也需要加,比如消息堆积数,unack的消息数,连接数,channel数等等。RabbitMQ有提供rest api可以拿到这些监控。
还有因为RabbitMQ对网络分区非常敏感,所以日志监控也要加。RabbitMQ出现网络分区或者触发流控都会在它的运行日志中有输出。
大致就是这么几点。
Q2:rabbitmq有尝试结合netty提高网络处理能力?
A2:erlang是actor模型代表,我觉得它的网络处理能力也不比netty差的。