作者 | 葛贤亮,单位:中国移动智慧家庭运营中心

​Labs 导读

近年来,互联网技术发展迅猛,各行各业的信息量急剧膨胀。随着云计算和算力网络时代的到来,消息中间件在国内许多行业的关键应用中越来越受到重视。在高并发分布式场景下,合理地利用消息中间件往往能起到突破性能瓶颈与化繁为简的效果。

1 面向消息的中间件

在现代架构设计领域内,基于目的和实现机制,中间件可分为两类:

  • 基于远程过程调用(Remote Procedure Call, RPC)的中间件,允许一个应用程序中的过程调用远程应用程序中的过程,就好像它们是本地调用一样。
  • 面向消息的中间件(Message-Oriented Middleware,MOM),使分布式应用程序可以通过发送和接收消息来进行通信和交换数据。

这两类模型都可以使一个本地组件通过网络协议访问(影响)另一个远程组件。区别在于RPC中间件调用远程组件时是同步操作,必须等待调用过程返回才能往下执行。而MOM中间件则通过高效可靠的消息传递机制进行异步数据传输。

同步与异步的差别导致了RPC模型中,本地组件(调用方)与远程组件(被调用方)必须同时处于运行状态,如果远程组件当前处于升级或故障状态,则RPC调用会失败,此时需要本地组件实现数据缓存及重试机制以确保最终调用成功(也可以根据产品需求特性直接向用户返回失败提示)。而在MOM模型中,几乎所有的消息中间件都实现了消息持久化功能,在本地组件发送消息至远程组件时,消息首先会被打包发送至通信服务器(Broker),通信服务器收到消息后会将消息进行持久化,之后通过底层网络将消息发送至远程组件,远程组件从消息队列接口中读取消息。

图1 RPC调用

图2 MOM调用

如上所述,基于MOM的系统实现了一种持久异步通信模式,允许组件进行更松散的耦合,可在分布式场景下扩展服务(进程)间的通信,并支持异构系统与多开发及时下流行的微服务架构相辅相成,使业务系统具有良好的动态负载伸缩能力。

2 消息中间件作用

实际应用场景中,消息中间件作为事件驱动架构模式的一种实现,通过提供消息队列模型和消息传递机制,也可以在分布式环境下提供应用解耦、异步通信、流量削峰、弹性伸缩、冗余存储、数据同步、最终一致性等功能。

2.1 应用解耦/事件驱动

事件驱动架构(Event Driven Architecture)是一种侧重于以生产、消费为基础的分布式异步架构模式。基于事件驱动架构模式的应用中,系统与系统之间可以通过消息传递的形式驱动业务,以流式的模型处理。其具有高并发、易扩展、松耦合等特点。

相较于通过RPC直接调用(同步/异步),采用消息传输方式使生产者与消费者依靠消息建立逻辑上的联系,生产者与消费者可归属于不同的系统。对于非核心流程,能够将其拆分至不同的消费者中,且支持后续的动态扩展。消费者对消息的消费与否也不会影响生产者中的核心流程。

示例:如在用户注册流程中,当注册成功后,系统需要发送邮件通知与短信通知。发送短信与发送邮件的调用逻辑都是写在注册方法中。用户注册模块与短信模块、邮件模块强耦合。如果后续又有发送微信公众号通知等需求时,只能去修改用户注册流程,与更多的模块产生依赖关系。

图3 应用解耦-用户注册-消息队列

但其实,在用户注册流程中,核心流程是用户信息写入,发送短信、发送邮件等操作都是由用户注册成功后触发而来,若采用事件驱动风格(消息/事件通知),用户模块作为生产者在用户注册成功后产生“注册成功”事件消息,其他模块作为消费者订阅该事件消息,则可以实现用户模块与其他模块(短信、邮件)的解耦。由于生产者不关心事件的后续处理结果,消费者模块可根据实际情况选择不同的调度策略,如并发处理、异步处理或按照闲时、忙时等状态处理。

2.2 异步通信

将非核心流程异步化,可以减少系统响应时间,提升吞吐量,从而提升用户体验。例如:短信通知、APP推送等。

在消息中间件场景中,用户通过前端页面点击注册按钮,平台接收到请求后,在用户模块中执行用户注册流程,注册成功后,发送“注册成功”事件消息至消息服务器(Broker),短信模块接收到事件消息后发送短信至用户手机。

以上流程中,我们忽略掉部分细节,取主要步骤做以下假设,前端页面到用户模块需耗时100毫秒,来回共200毫秒,用户模块执行注册流程需要耗时50毫秒,短信模块执行需要耗时100毫秒。此时短信发送流程由事件消息触发,不会堵塞用户注册主流程,对于用户来说,用户注册流程仅耗时250毫秒。而如果采用传统调用方式,则需要耗时350毫秒。

图4 异步通信

当然RPC方式也可以实现异步模式,如本地组件A同步调用远程组件B,远程组件B收到请求后,将处理过程放置在异步线程池中处理,当前线程则快速返回结果。这种方式下,一是要求远程组件B必须在线(否则会因超时返回调用失败),二是远程组件B中异步线程池执行时可能因进程重启导致任务丢失(任务仅在内存队列中,未进行持久化)。消息中间件的异步特性与消息持久化机制则不存在这两个问题(此处指广义上不存在,若消息中间件服务器故障也会出现生产者发送消息失败的情况)。

2.3 流量削峰

在一些秒杀等互联网电商场景中,当上游系统的吞吐能力高于下游系统时,在流量洪峰时可能会冲垮下游系统。采用线程池方案时,虽然可以对用户进行快速返回,但任务都被堆积在线程池队列中,造成内存占用过大的及进程重启导致任务丢失问题。而消息中间件可以在峰值时堆积消息,而在峰值过去后下游系统慢慢消费消息解决流量洪峰的问题。

示例:用户在支付系统成功结帐后,订单系统会经过短信系统向用户推送扣费通知。短信系统可能因为短板效应,速度卡在网关上(每秒几百次请求),跟前端的并发量不是一个数量级。因而,就形成支付系统和短信系统的处理能力出现差别化。此时可以把消息队列当成可靠的消息暂存地,进行一定程度的消息堆积,下游系统可以根据自己的节奏获取并处理消息。

2.4 最终一致性

一致性的概念来源于本地事务的ACID特性与分布式事务中的CAP理论,是指数据符合期望,相互关联的数据之间不会产生矛盾。

CAP、ACID中讨论的一致性称为“强一致性”(Strong Consistency),有时也称为“线性一致性”(Linearizability,通常是在讨论共识算法的场景中),而把牺牲了C的AP系统又要尽可能获得正确的结果的行为称为追求“弱一致性”。在弱一致性里,人们又总结出了一种稍微强一点的特例,被称为“最终一致性”(Eventual Consistency),它是指:如果数据在一段时间之内没有被另外的操作所更改,那它最终将会达到与强一致性过程相同的结果。

最终一致性不是消息队列的必备特性,但确实可以依靠消息队列来实现最终一致性。反之,如果需要强一致性,关注业务逻辑的处理结果,则使用RPC显得更为合适。

使用消息中间件实现最终一致性可以有两种方案:

方案一:本地消息表 补偿机制。本地消息表用来确保任务不会丢失,补偿机制通过不断重试实现失败消息最终被消费成功。该方案需要注意消息重复与幂等设计。

方案二:使用RocketMQ自带消息事务的消息中间件(消息事务并非银弹,也同样存在着其他分布式事务具有的缺陷)。

图5 最终一致性-MQ 消息事务(RocketMQ)

3 消息中间件副作用

消息中间件带来诸多好处的同时,也会引入很多的弊端:

  • 系统可用性降低:系统可用性在某种程度上降低,比如要考虑消息丢失、消息中间件宕机等问题。
  • 系统复杂性提高:引入消息中间件之后,业务需要考虑消息被重复消费、消息丢失、消息传递顺序等问题。
  • 一致性问题:消息队列的异步机制确实可以提高系统响应速度,但消费者没有正确消费可能会引入一致性问题。

4 消息中间件组成

虽然各个消息中间件实现机制不一样,但基本都会包含以下几种角色:

  • Broker:消息服务器,提供消息核心服务,负责存储/转发消息(转发模式分为 push 和 pull);
  • Producer:消息生产者,业务的发起方,负责生产消息传输给 broker;
  • Consumer:消息消费者,业务的处理方,负责从broker获取消息并进行业务逻辑处理;
  • Topic:主题,发布订阅模式下的消息统一汇集地,不同生产者向topic发送消息,由MQ服务器分发到不同的订阅者,实现消息的广播;
  • Queue:消息队列,PTP(点对点)模式下,特定生产者向特定queue发送消息,消费者订阅特定的queue完成指定消息的接收;
  • Message:消息体,根据不同通信协议定义的固定格式进行编码的数据包,来封装业务数据,实现消息的传输。

5 消息中间件协议

5.1 JMS

JMS即Java消息服务(Java Message Service)应用程序接口,是一个Java平台中关于面向消息中间件(MOM)的API,用于在两个应用程序之间,或分布式系统中发送消息,进行异步通信。它类似于 JDBC(Java Database Connectivity)。

⇊ JMS由以下元素组成:

  • JMS提供者:连接面向消息中间件,JMS接口的实现。提供者可以是Java平台的JMS实现,也可以是非Java平台的面向消息中间件的适配器;
  • JMS客户:生产或消费基于消息的Java的应用程序或对象;
  • JMS生产者:创建并发送消息的JMS客户;
  • JMS消费者:接收消息的JMS客户;
  • JMS消息:包括可以在JMS客户之间传递的数据的对象;
  • JMS队列:一个容纳那些被发送的等待阅读的消息的区域。与队列名字所暗示的意思不同,消息的接受顺序并不一定要与消息的发送顺序相同。一旦一个消息被阅读,该消息将被从队列中移走;
  • JMS主题:一种支持发送消息给多个订阅者的机制。

严格意义上来说,消息领域中的JMS更多的是一个规范而不是一个协议。ActiveMQ是该协议的典型实现。

5.2 AMQP

AMQP(Advanced Message Queuing Protocol),一个提供统一消息服务的应用层标准高级消息队列协议,是应用层协议的一个开放标准,为面向消息的中间件设计。基于此协议的客户端与消息中间件可传递消息,并不受客户端/中间件不同产品,不同开发语言等条件的限制。

AMQP目前已经推出协议1.0版本,实现此协议的比较知名的产品有 StormMQ、RabbitMQ、Apache Qpid等。RabbitMQ实现的AMQP版本是0.9.1,可通过plugin的方式支持1.0版本。

⇊ 以下内容摘自官网:

RabbitMQ implements version 0-9-1 of the AMQP specification in the core, with a number of extensions to the specification.

RabbitMQ implements AMQP 1.0 via a plugin. However, AMQP 1.0 is a completely different protocol than AMQP 0-9-1 and hence not a suitable replacement for the latter. RabbitMQ will therefore continue to support AMQP 0-9-1 indefinitely.

--来源:https://www.rabbitmq.com/specification.html

5.3 MQTT

MQTT(Message Queuing Telemetry Transport,消息队列遥测传输协议),是一种基于发布/订阅(publish/subscribe)模式的“轻量级”通讯协议,该协议构建于TCP/IP协议上,由IBM在1999年发布。

MQTT最大优点在于,可以以极少的代码和有限的带宽,为连接远程设备提供实时可靠的消息服务。作为一种低开销、低带宽占用的即时通讯协议,使其在物联网、小型设备、移动应用等方面有较广泛的应用。

RabbitMQ通过插件可以支持该协议。

图6 MQTT应用场景

5.4 STOMP协议

STOMP(Streaming Text Orientated Message Protocol)是流文本定向消息协议,是一种为MOM(Message Oriented Middleware,面向消息的中间件)设计的简单文本协议。STOMP提供一个可互操作的连接格式,允许客户端与任意STOMP消息代理(Broker)进行交互。

STOMP协议的前身是TTMP协议(一个简单的基于文本的协议),专为消息中间件设计。

STOMP是一个非常简单和容易实现的协议,其设计灵感源自于HTTP的简单性。尽管STOMP协议在服务器端的实现可能有一定的难度,但客户端的实现却很容易。例如,可以使用Telnet登录到任何的STOMP代理,并与STOMP代理进行交互。

5.5 XMPP

XMPP(可扩展消息处理现场协议,Extensible Messaging and Presence Protocol)是基于可扩展标记语言(XML)的协议,多用于即时消息(IM)以及在线现场探测。适用于服务器之间的准即时操作。核心是基于XML流传输,这个协议可能最终允许因特网用户向因特网上的其他任何人发送即时消息,即使其操作系统和浏览器不同。特点:通用公开、兼容性强、可扩展、安全性高,但XML编码格式占用带宽大,是一种历史悠久的协议。

5.6 自定义协议

有些特殊框架(如:Redis、Kafka、ZeroMq 等)根据自身需要未严格遵循MQ规范,而是基于TCP/IP自行封装了一套协议,通过网络socket接口进行传输,实现了MQ的功能。

5.7 各协议简单对比

图7

6 总结

当前,消息中间件技术已经成为构建分布式互联网应用的基础设施。越来越多的系统使用消息中间件解决异步、解耦、削峰等难题。消息中间件不是一项新技术,但新的实现方案层出不穷,引入消息中间件时还需要根据自身的业务特性与需求选择适合的方案。

参考文献

[1] 面向消息的中间件 (Message-Oriented Middleware, MOM):https://docs.oracle.com/cd/E19148-01/820-0533/6nc927vst/index.html.

[2] 凤凰架构:http://icyfenix.cn/.​