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


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

    • 微服务架构与MQ

    • RabbitMQ场景分析与优化

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

    1微服务架构与MQ

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

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

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

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

    微服务通信方式:

    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

    总结:生产者将消息发送到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消息持久化(写磁盘)的两个场景:

    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机制的专项性能测试数据:

    总结:

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

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

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

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

     

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

     

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

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

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

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

    设置参数: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内部进程关系调用图

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

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

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

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

    网易蜂巢平台的服务化架构如下,服务间通过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.环境参数:

     

    2.现象:

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

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

    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差的。

  • 相关阅读:
    CREATE AGGREGATE
    技术文档列表
    jQuery 判断表单中多个 input text 中至少有一个不为空
    Java实现 蓝桥杯 算法提高 奥运会开幕式
    Java实现 蓝桥杯 算法提高 最长滑雪道
    Java实现 蓝桥杯 算法提高 最长滑雪道
    Java实现 蓝桥杯 算法提高 最长滑雪道
    Java实现 蓝桥杯 算法提高 最大值路径
    Java实现 蓝桥杯 算法提高 最大值路径
    Java实现 蓝桥杯 算法提高 最大值路径
  • 原文地址:https://www.cnblogs.com/yangxiaolan/p/5786119.html
Copyright © 2020-2023  润新知