网易蜂巢微服务架构:用RabbitMQ实现轻量级通信

时间:2021-03-22 13:08:07

本次分享内容由三个部分组成:

  • 微服务架构与MQ

  • RabbitMQ场景分析与优化

  • RabbitMQ在网易蜂巢中的应用和案例分享

1微服务架构与MQ

微服务架构是一种架构模式,它将单体应用划分成一组微小的服务,各服务之间使用轻量级的通信机制交互。

网易蜂巢微服务架构:用RabbitMQ实现轻量级通信

上图左边是单体架构应用,把所有业务功能放在单个进程中,需要扩展时以进程为单位水平复制到多台机器。

上图右边是微服务架构应用,将每个业务功能以独立进程(服务)的方式部署,可以按需将微服务分别部署在多台机器,实现水平扩展。

微服务各服务之间使用“轻量级”的通信机制。所谓轻量级,是指通信协议与语言无关、与平台无关。

微服务通信方式:

  1. 同步:RPC,REST等

  2. 异步:消息队列

 

同步通信方式

优点:

  1. 实现方便。

  2. 协议通用,比如HTTP大家都非常熟知。

  3. 系统架构简单,无需中间件代理。

缺点:

  1. 客户端耦合服务方。

  2. 通信双方必须同时在线,否则会造成阻塞。

  3. 客户端需要知道服务方的Endpoint,或者需要支持服务发现机制。

 

异步通信方式

优点:

  1. 系统解耦和。

  2. 通信双方可以互相对对方无感知。

缺点:

  1. 额外的编程复杂性。比如需要考虑消息可靠传输、高性能,以及编程模型的变化等。

  2. 额外的运维复杂性。比如消息中间件的稳定性、高可用性、扩展性等非功能特性。

 

今天的主题是消息队列在微服务架构中的应用与实践。

 

消息队列中间件如何选型?主要会考虑以下几点:

  1. 协议:AMQP、STOMP、MQTT、私有协议等

  2. 消息是否需要持久化

  3. 吞吐量

  4. 高可用支持,是否单点

  5. 分布式扩展能力

  6. 消息堆积能力和重放能力

  7. 开发便捷,易于维护

  8. 社区成熟度

 

选择RabbitMQ的原因:

  1. 开源,跨平台

  2. 灵活的消息路由策略

  3. 持久化,消息可靠传输

  4. 透明集群,HA支持

  5. 支持高性能高并发访问

  6. 支持多种消息协议

  7. 丰富的客户端、插件和平台支持

  8. 支持RPC解决方案

2RabbitMQ场景分析与优化

RabbitMQ是一个实现了AMQP(高级消息队列协议)协议的消息队列中间件。

AMQP基本模型:

1. Queue

2. Exchange: Direct, Fanout, Topic, Header

3. Binding: BindingKey, RouteKey

网易蜂巢微服务架构:用RabbitMQ实现轻量级通信

总结:生产者将消息发送到Exchange,Exchange通过匹配BindingKey和消息中的RouteKey来将消息路由到队列,最后队列将消息投递给消费者。

 

消息可靠传输是业务系统接入MQ时首先要考虑的问题。一般消息可靠性有三个等级:

  1. At most once: 最多一次

  2. At least once: 最少一次

  3. Exactly once: 恰好一次

RabbitMQ支持其中的“最多一次”和“最少一次”两种。其中“最少一次”投递实现机制:

  1. 生产者confirm。如何开启:使用confirm.select

  2. 消息持久化。

  3. 消费者ack。如何开启:使用basic.consume(…, no-ack=false)

网易蜂巢微服务架构:用RabbitMQ实现轻量级通信

这里说下RabbitMQ消息持久化(写磁盘)的两个场景:

  1. 显式指定消息属性:delivery-mode=2

  2. 内存吃紧时,消息(包括非持久化消息)转存到磁盘,由memory_high_watermark_paging_ratio参数指定阈值。

 

RabbitMQ的消息持久化是通过以下机制实现的:

  1. 消息体写文件

  2. 异步刷盘,合并请求,减少fsync系统调用次数

  3. 当进程mailbox没有新消息时,实时刷盘

  4. confirm机制下,等fsync系统调用完成后返回basic.ack确认

 

RabbitMQ开启消息可靠性参数需要注意:

  1. unack消息在服务器端没有超时,只能等待客户端连接断开,重新入队等待投递。

  2. 消息存在重复投递的情况,需客户端去重:

    a)基于业务层的MsgId。

    b)基于RabbitMQ的Redelivered flag标记: 不完全靠谱,仍旧可能收到重复消息。

  3. 保障性能:

    a)批量publish, ack。

    b)更快的磁盘(SSD,RAID等)。

    c)少堆积。

  4. 消息乱序。

 

生产者confirm机制是三个可靠性参数中对性能影响最大的。一般来说有三种编程方式:

  1. 普通confirm模式。每发送一条消息后,调用waitForConfirms()方法,等待服务器端confirm。实际上是一种串行confirm。

  2. 批量confirm模式。每次发送一批消息后,调用waitForConfirms()方法,等待服务器端confirm。

  3. 异步confirm模式。注册一个回调方法,服务器端confirm了一条(或多条)消息后SDK会回调这个方法。

下面是一些生产者confirm机制的专项性能测试数据:

网易蜂巢微服务架构:用RabbitMQ实现轻量级通信

总结:

  1. 遵循线程数越大,吞吐量越大的规律。当线程数量达到一个阈值之后,吞吐量开始下降。

  2. 不论哪种confirm模式,通过调整客户端线程数量,都可以达到一个最大吞吐量值。

  3. 异步和批量confirm模式两者没有明显的性能差距。所以,只需从可编程性的角度选择异步或批量或者两者结合的模式即可。相比而言,普通confirm模式只剩编程简单这个理由了。

下面讲下RabbitMQ的高可用机制。

 

官方提供的高可用方案:cluster + ha policy

 

cluster机制:多个全联通节点之间元信息(exchange、queue、binding等)保持强一致,但是队列消息只会存储在其中一个节点。

网易蜂巢微服务架构:用RabbitMQ实现轻量级通信

优点:提高吞吐量,部分解决扩展性问题。

缺点:不能提升数据可靠性和系统可用性。

ha policy机制:在cluster机制基础上可以指定集群内任意数量队列组成镜像队列,队列消息会在多节点间复制。实现数据高可靠和系统高可用。

网易蜂巢微服务架构:用RabbitMQ实现轻量级通信

设置参数:ha-mode和ha-params可以细粒度(哪些节点,哪些队列)设置镜像队列。

设置参数:ha-sync-mode=manual(默认)/automatic可以指定集群中新节点的数据同步策略。

有一点需要注意:镜像队列对网络抖动非常敏感,默认参数配置下,出现脑裂后RabbitMQ集群不会自我恢复,需要人工介入恢复,务必加好监控和报警。

RabbitMQ流控机制  流控类型:

  1. 内存流控:由vm_memory_high_watermark参数控制,默认0.4

  2. 磁盘流控:由disk_free_limit参数控制,默认50M

  3. 单条连接流控:触发条件是下游进程的处理速度跟不上上游进程。

网易蜂巢微服务架构:用RabbitMQ实现轻量级通信

RabbitMQ内部进程关系调用图

注意:当触发流控(全局内存/磁盘流控,单条连接流控)时,生产者端的publish方法会被阻塞,生产者需要做的是:

  1. 注册block事件,被流控时,会收到一个回调通知。

  2. 异步化处理生产者发送消息,不要阻塞主流程。

3RabbitMQ在网易蜂巢中的应用和案例分享

网易蜂巢平台的服务化架构如下,服务间通过RabbitMQ实现通信:

网易蜂巢微服务架构:用RabbitMQ实现轻量级通信

网易蜂巢微服务架构:用RabbitMQ实现轻量级通信

 

网易蜂巢消息服务器设计目标:实现一个路由灵活、数据可靠传输、高可用、可扩展的消息服务器。

网易蜂巢微服务架构:用RabbitMQ实现轻量级通信

 

设计要点:

  1. Exchange类型为topic。

  2. BindingKey就是队列名。

  3. 每个服务(图例中的REPO/CTRL/WEB等)启动后会初始化一条AMQP连接,由3个channel复用:一个channel负责生产消息,一个channel从TYPE(REPO/CTRL/WEB等)类型的队列消费消息,一个channel从TYPE.${hostname}类型的队列消费消息。

 

应用场景举例:

  1. 点对点(P2P)或请求有状态服务:消息的RouteKey设置为TYPE.${HOSTNAME}。如host1上的WEB服务需要向host2上的REPO服务发送消息,只需将消息的RouteKey设置为REPO.host2投递到Exchange即可。

  2. 请求无状态服务:如果服务提供方是无状态服务,服务调用方不关心由哪个服务进行响应,那么只需将消息的RouteKey设置为TYPE。比如CTRL是无状态服务,host1上的WEB服务只需将消息的RouteKey设置为CTRL即可。CTRL队列会以Round-Robin的均衡算法将消息投递给其中的一个消费者。

  3. 组播:如果服务调用方需要与某类服务的所有节点通信,可以将消息的RouteKey设置为TYPE.*,Exchange会将消息投递到所有TYPE.${HOSTNAME}队列。比如WEB服务需通知所有CTRL服务更新配置,只需将消息的RouteKey设置为CTRL.*。

  4. 广播:如果服务调用方需要与所有服务的所有节点通信,也就是说对当前系统内所有节点广播消息,可以将消息的RouteKey设置为*.*。

总结:通过对RouteKey和BindingKey的精心设计,可以满足点对点(私信)、组播、广播等业务场景的通信需求。

优缺点分析:

优点:

  1. 路由策略灵活。

  2. 支持负载均衡。

  3. 支持高可用部署。

  4. 支持消息可靠传输(生产者confirm,消费者ack,消息持久化)。

  5. 支持prefetch count,支持流控。

缺点:

  1. 存在消息重复投递的可能性。

  2. 超时管理,错误处理等需业务层处理。

  3. 对于多服务协作的场景支持度有限。比如以下场景:WEB=> CTRL=>REPO=>CTRL=> WEB。这个时候就需要CTRL缓存WEB请求,直至REPO响应。

实践案例分享

案例一:GC引起的MQ crash

1.环境参数:

 

网易蜂巢微服务架构:用RabbitMQ实现轻量级通信

2.现象:

RabbitMQ崩溃,产生的erl_crash.dump文件内容如下:

网易蜂巢微服务架构:用RabbitMQ实现轻量级通信

3.直接原因:

从数据来看,虚拟机内存共4G,Erlang虚拟机已占用约1.98G内存(其中分配给Erlang进程的占1.56G),此时仍向操作系统申请1.82G,因为操作系统本身以及其他服务也占用一些内存,当前系统已经分不出足够的内存了,所以Erlang虚拟机崩溃。

4.分析:

本例有两个不符合预期的数据:
1. 内存流控阈值控制在1.67G左右(4G*0.4),为什么崩溃时显示占用了1.98G?
2. 为什么Erlang虚拟机会额外再申请1.82G内存?

因为:

  1. RabbitMQ每个队列设计为一个Erlang进程,由于进程GC也是采用分代策略,当新老生代一起参与Major GC时,Erlang虚拟机会新开内存,根据root set将存活的对象拷贝至新空间,这个过程会造成新老内存空间同时存在,极端情况下,一个队列可能短期内需要两倍的内存占用量。这就是RabbitMQ将内存流控的安全阈值默认设置为0.4的原因。

  2. 内存流控参数vm_memory_high_watermark 为0.4的意思是,当RabbitMQ的内存使用量大于40%时,开始进行生产者流控,但是该参数并不承诺RabbitMQ的内存使用率不大于40%。

5.如何解决(规避)?

  1. RabbitMQ独立部署,不与其他Server共享内存资源。

  2. 进一步降低vm_memory_high_watermark值,比如设置成0.3,但是这种方式会造成内存资源利用率太低。

  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的日志中出现大量关于消费请求的错误日志:

网易蜂巢微服务架构:用RabbitMQ实现轻量级通信

d)从生产者端看来一切正常,依旧会收到来自节点B的confirm消息(basic.ack amqp方法)。

3.分析:

上述现象实际上有两个坑在里面:

  1. 在数据可靠传输方面,镜像队列也不完全可靠。这是一个Bug。RabbitMQ 3.5.1版本以下都存在这个问题。

  2. 要保证消息可靠性,生产者端仅仅采用confirm机制还不够。对于那些路由不可达的消息(根据RouteKey匹配不到相应队列),RabbitMQ会直接丢弃消息,同时confirm客户端。

4.如何解决(规避)?

  1. 升级RabbitMQ到新版,至少3.5.1及以上。

  2. 生产者basic.publish方法设置mandatory参数,它的作用是:对于那些路由不可达的消息,RabbitMQ会先通过basic.return消息向生产者返回消息内容,然后再发送basic.ack消息进行确认

 

Q & A

Q1为保障消息队列稳定性,消息队列监控点主要有哪些?

A1首先是服务器的基础监控是要的。CPU,内存,磁盘IO都是敏感项。特别是内存,报警阈值可能要设置在50%以下,原因前面也说过,极端情况下一个队列的内存占用量可能是两倍当前值。

然后MQ业务的监控也需要加,比如消息堆积数,unack的消息数,连接数,channel数等等。RabbitMQ有提供rest api可以拿到这些监控。

还有因为RabbitMQ对网络分区非常敏感,所以日志监控也要加。RabbitMQ出现网络分区或者触发流控都会在它的运行日志中有输出。

大致就是这么几点。

Q2rabbitmq有尝试结合netty提高网络处理能力?

A2erlang是actor模型代表,我觉得它的网络处理能力也不比netty差的。