云原生应用架构实践-消息中间件

消息中间件

消息中间件是分布式系统中重要的组件,如图 4-12 所示,它能够解决应用耦合、异步 投递消息、流量削峰等问题,实现高性能、高可用、可伸缩和最终一致性架构,是大型分 布式系统不可缺少的中间件。 

图 4-12 消息中间件

1.消息模型 

队列(Queue) 

队列模型下,一条消息只会被一个消费者处理,如图 4-13 所示。在多个消费者的情况下,生产者投递的消息一般会平均投递到不同的消费者。 

图 4-13 队列

如果使用场景单一,如日志统计,但数据量比较大,需要有多个消费者分流,使用队 列模型是比较好的选择。

发布订阅(Pub/Sub) 

发布订阅模型下,一条消息会被每一个消费者(或者订阅者)处理,生产者投递的消 息会复制到每一个消费者。如图 4-14 所示。 

图 4-14 发布订阅

针对一条消息(或者事件)需要做多件事情,如针对用户注册消息,需要发一条确认 短信,还需要发一封欢迎邮件,并针对用户资料给用户推荐感兴趣的内容,那么使用发布 订阅模型会比较合适。

在上述场景下,针对发送邮件的订阅者,如果一个订阅者处理不过来,需要多个订阅 者分流该怎么办?主流的消息中间件针对这种场景,提出了一个消费者群组(Consumer Group)的概念。在一个群组内,遵循队列模型,而不同群组之间,遵循发布订阅模型。

投递模型(Quality of Service,QoS) 

在不可靠的网络环境下,消息投递模型定义了消息投递的质量。不同的投递模型有不 同的实现难度,使用方可以根据网络的质量及业务需求来选择不同的投递模型。

QoS0:最多一次 

只保证消息会尽最大努力投递出去,不保证消费者一定能收到消息。生产者不会重传 消息,消费者也不会在收到消息时给予确认。一般在你不太关心少量消息是否丢失的场景 下可以使用 QoS0。

QoS1:最少一次 

消息至少投递到消费者一次,消费者需要在收到消息时进行确认,在没收到消息确认 时,有可能需要重传。因为网络的不可靠性,确认消息有可能丢失,导致消费者有可能收 到重复消息。在你需要确保每一条消息都被处理,但不关心或者说能够容忍消息重复的情 况下,使用 QoS1。

QoS2:有且仅有一次 

消息一定会投递到消费者,并且保证只出现一次,不会出现消息重复。这种情况下, 消息是可靠的,但是投递效率也是最慢的,实现角度也是最难的。 

目前,主流消息中间件都只提供 QoS1 级别的消息投递质量,需要使用方在处理消息 时考虑或者有相应机制来保证消息重复不会带来问题。

2. 使用场景

解耦 

消息中间件提供的最主要特性就是解耦。所谓解耦,就是消息的生产者和消费者可以 不需要依赖彼此的存在,生产者只需要把消息发送到消息中间件就可以,不需要依赖消 费者处理服务一定在线,等消费者上线以后再处理消息。解耦以后,使用者只需要关心 核心的流程,依赖其他服务但不那么重要的事情,就只需要通过消费中间件投递一条通知 就行。

举个简单的例子,对于用户注册这个流程来说,一般需要做如下事情。

服务端收到注册用户的信息,做简单检查后,写入数据库。

向用户发一个注册成功的欢迎邮件。

向用户发一条绑定手机号码的短信。

调用推荐系统,向用户推荐一批兴趣相同的用户或者感兴趣的内容。

观察这几个流程,会发现用户在注册时其实只关注第一个步骤,即是否注册成功。后 续的步骤对于用户注册来说都不是必要的。所以当第一步完成后,就可以向用户返回注册 成功,然后把新用户注册成功的信息发送到消息中间件,由相应的处理者(或者消费者) 来针对新用户注册成功的信息进行处理,比如邮件发送系统可以针对用户信息向用户发送 欢迎邮件,推荐系统根据用户提交的信息,匹配兴趣相同的用户并推荐关注,或者把用户 感兴趣的内容推送给用户。

异步化 

上述用户注册的过程,使用消息中间件以后,一方面是解耦,事件产生者不需要依赖 于事件处理,另一方面产生的效果是异步化。用户注册主流程只需要将新用户注册的事件 投递到消息中间件即可,不需要等待具体处理流程完成,比如说不用等欢迎邮件实际发出。 异步化可以提升处理效率,将不重要或者不影响核心流程的逻辑异步处理,提高主流程的 处理响应速度。

削峰 

在之前的用户注册流程中,主流程只需要写入数据库就可以完成,理论上用户注册的 吞吐量可以达到数据库写入吞吐量的上限。但是对于短信发送来说,由于短信网关的限制, 吞吐量不会很高,和数据库写入吞吐量可能不在一个量级上,但对于用户来说,晚收到绑 定手机的短信一般不会有太大问题。消息中间件一般都有堆积消息的能力,在消费者的处 理能力跟不上生产者的生产速率时,消息中间件的消息堆积能力就可以提供一定的缓冲, 让消费者(比如短信发送系统)平缓处理进入的消息,而不至于压垮整个系统。

3. 实践经验

RabbitMQ 是互联网企业使用比较多的消息中间件,它是基于 AMQP(Advanced Message Queuing Protocol)协议的开源实现,也支持其他协议(STOMP、MQTT、Websocket 等),使用广泛。这里我们就以 RabbitMQ 为例简单介绍在实际项目使用过程中常碰到的问 题及解决思路。

消息吞吐量 

为了支持 QoS1 的消息投递质量,RabbitMQ 提供了生产者确认机制(confirm)、消费 者确认机制(ack)、持久化机制和高可用机制等。这些机制一方面保证了消息的可靠到达, 但另一方面也会对消息的吞吐量产生较大的影响,根据我们的测试,持久化大概会对吞吐 量有 40%~50%的影响,生产确认机制有 20%左右的影响,消费者确认机制有 10%左右的 影响,而高可用机制大概有 30%~40%左右的影响。

使用时要根据不同的业务场景选择合适的配置。在测试环境或者对单点故障可以容忍 的场景下,可以选择不使用高可用机制,但对于不能容忍单点故障的服务来说,必须使用 高可用机制。而对于能够容忍消息丢失或者容忍某些场景下丢失消息的服务来说,持久化、 生产者确认机制、消费者确认机制都可以有选择地使用,具体要根据不同场景业务的需求, 再结合测试结果选择合适的配置。

资源流控 

为了保证 RabbitMQ 服务器正常运行,RabbitMQ 做了一些保护措施,其中就包括资源 (内存、磁盘)级别的流控,当内存占用或者磁盘占用到一定比例时,就会触发全局流程, 所有生产者将不能发送消息到服务端,但消费者依然可以处理消息。出现这种情况的原因 一般是因为消费速度跟不上生产速度,要么本身处理速度慢,要么消费者出现问题时已经 停止运行或者已经没在处理消息,消息大量堆积,导致内存或者磁盘占用过多。如果因为 消费者出现问题导致消息堆积,就需要尽快排查问题;如果单纯因为消费速度跟不上,要 么调整消费者所在服务器规格,要么水平扩展,添加多个消费者。另外,在使用 RabbitMQ 的时候,为了避免因为机器故障导致消费者不可用,一般情况下,我们都应该至少部署双 副本。不管怎么样,我们都需要在使用过程中做好监控,观察生产消费速度是否匹配,适 时调整,也要做好资源监控,在流程发生前处理问题,不影响业务。

网络分区 

前面提到高可用机制,它依赖 RabbitMQ 提供的集群模式,而 RabbitMQ 的集群模式不能很好地处理网络分区,所以官方不建议在广域网的环境下使用。但即使在局域网内使用, 还是有可能出现网络分区。RabbitMQ 会自己检测网络分区,并在日志或者管理页面提醒使 用者已经发生网络分区。发生网络分区时,集群的两个分区都可以正常工作,但两个分区 都认为另外一个分区的机器有故障不可用。要从网络分区的状态中恢复,只能选择其中一 个分区做为主分区,重启另外一个分区中的所有机器,让它们重新加入集群并同步主分区 的状态,但被重启分区中的状态数据会丢失。等重启的分区都加入集群后,要重启整个集 群,网络分区的提示才会消失。RabbitMQ 目前提供了网络分区的恢复机制,有以下几个可 选项。 

ignore:默认,不做任何处理,如果你的网络非常可靠,比如所有节点连在一个交 换机上,可以选择这种模式。

pause_minority:网络有一点不稳定的情况下使用,重启节点较少的分区。 {pause_if_all_down, [nodes], ignore | autoheal}:如果分区中的节点不能连接到指定 节点,则重启该分区。 

autoheal:网络不稳定的情况下使用,保留客户端连接数比较多的分区,重启其他 分区。

支持大量连接 

如果将 RabbitMQ 使用在需要支撑大量连接的场景下(比如物联网),就需要做一些调 整。一般系统默认都会对打开的文件句柄数有所限制,所以如果要支持大量连接,首先要 调整文件句柄数,Linux 下一般可以通过 ulimit 来调整。如果对性能有要求,还要进行针对 性调优,涉及 RabbitMQ 层面、Erlang 虚拟机(RabbitMQ 使用 Erlang 语言)层面、操作系 统层面,以及 TCP 连接层面等,这里不详细展开,具体可以参考官方文档。

文章节选自《云原生应用架构实践》 网易云基础服务架构团队 著 

参考文档:https://sq.163yun.com/blog/article/221344803046789120