MQ的定义
MQ是英文Message Queue的缩写,翻译成中文是消息队列。所谓队列,是基础数据结构中“先进先出”的一种数据结构。而消息队列,是指在不同服务之间传播消息。
MQ存在的意义
为什么需要MQ?
技术上任何东西存在要有价值,就必须有使用的场景。我们考虑一个日常的场景,你在网上A商城买东西,下单付款,这时候A给你推送了一条短信,告之你付费购买了XX;并且A还给你发了邮件,告之你付费购买了XX。
在上述情形中,假设A的交易体量很大,所以下单付款和发短信、发邮件属于不同的服务。每当下单成功了,订单服务就要向外发消息,让发短信服务和发邮件服务去把短信和邮件发出去。
这时候就需要在服务之间传播消息,所以需要MQ。
传播消息直接HTTP不行么?为什么要用MQ?
服务之间直接使用HTTP协议传播消息,只能选择RESTful的接口来实现。这种接口方式,对于消息队列的传输来说,太重了。所以并不是说这种办法做不了,而是面向API的接口开发,要么每一次接口改动就去修改代码,这很不方便。而且,你如果想知道短信有没有发出去,就必须让短信服务再给你回一个接口,代码量就很夸张。
这个时候,不如专门抽出来一个中间件去关心短信有没有发出去、订单服务发出的消息是否重复、订单服务发出的消息是否顺序正确这种事情。此时业务服务本身只需要关心发和接受消息就好了。
所以,为了方便企业IT系统内部服务之间的通信,我们就需要单独把这个事情拿出来做成一个中间件——即消息队列,一方面接受消息,一方面把消息发出去。这样的好处是能把与消息相关的技术一并处理,还能降低服务之间的耦合性。
MQ只是单纯的服务之间传播消息么?
MQ的核心功能是服务之间传播消息,但是为了保障消息传播的可靠,我们需要考虑诸如RPC、高可用、顺序和重复消息、可靠投递、消费关系解析等等这类问题。
MQ的实际使用
当你需要使用消息队列时,首先需要考虑它的必要性。可以使用mq的场景有很多,最常用的几种,是做业务解耦/最终一致性/广播/错峰流控等。反之,如果需要强一致性,关注业务逻辑的处理结果,则RPC显得更为合适。
解耦
解耦是消息队列要解决的最本质问题。所谓解耦,简单点讲就是一个事务,只关心核心的流程。而需要依赖其他系统但不那么重要的事情,有通知即可,无需等待结果。换句话说,基于消息的模型,关心的是“通知”,而非“处理”。
比如上面的例子中,上游的订单服务其实只关心Orders表里有没有正确的插入数据,根本不关心下游的短信、邮件服务的消息处理结果。
正如刚才分析的,如果下游的短信、邮件服务定时来拉取数据,也能保证数据的更新,只是实时性没有那么强。但使用接口方式去发短信、邮件,显然对于这些服务来说太过于“重量级”了,只需要发布一个订单创建的通知,由下游系统来处理,可能更为合理。
再举一个例子,公司的运营肯定希望能看到每天的订单数据,从而进行分析。但是订单系统明显也不关心这种业务,这时候选择发一个通知给消息中心让下游的展示系统让他们去完成自己的业务,是最好的选择了。
最终一致性
最终一致性指的是两个系统的状态保持一致,要么都成功,要么都失败。当然有个时间限制,理论上越快越好,但实际上在各种异常的情况下,可能会有一定延迟达到最终一致状态,但最后两个系统的状态是一样的。
业界有一些为“最终一致性”而生的消息队列,如Notify(阿里)、QMQ(去哪儿)等,其设计初衷,就是为了交易系统中的高可靠通知。
以一个银行的转账过程来理解最终一致性,转账的需求很简单,如果A系统扣钱成功,则B系统加钱一定成功。反之则一起回滚,像什么都没发生一样。
然而,这个过程中存在很多可能的意外:
- A扣钱成功,调用B加钱接口失败。
- A扣钱成功,调用B加钱接口虽然成功,但获取最终结果时网络异常引起超时。
- A扣钱成功,B加钱失败,A想回滚扣的钱,但A机器down机。
可见,想把这件看似简单的事真正做成,真的不那么容易。所有跨VM的一致性问题,从技术的角度讲通用的解决方案是:
- 强一致性,分布式事务,但落地太难且成本太高,后文会具体提到。
- 最终一致性,主要是用“记录”和“补偿”的方式。在做所有的不确定的事情之前,先把事情记录下来,然后去做不确定的事情,结果可能是:成功、失败或是不确定,“不确定”(例如超时等)可以等价为失败。成功就可以把记录的东西清理掉了,对于失败和不确定,可以依靠定时任务等方式把所有失败的事情重新搞一遍,直到成功为止。
- 回到刚才的例子,系统在A扣钱成功的情况下,把要给B“通知”这件事记录在库里(为了保证最高的可靠性可以把通知B系统加钱和扣钱成功这两件事维护在一个本地事务里),通知成功则删除这条记录,通知失败或不确定则依靠定时任务补偿性地通知我们,直到我们把状态更新成正确的为止。
- 整个这个模型依然可以基于RPC来做,但可以抽象成一个统一的模型,基于消息队列来做一个“企业总线”。
- 具体来说,本地事务维护业务变化和通知消息,一起落地(失败则一起回滚),然后RPC到达broker,在broker成功落地后,RPC返回成功,本地消息可以删除。否则本地消息一直靠定时任务轮询不断重发,这样就保证了消息可靠落地broker。
- broker往consumer发送消息的过程类似,一直发送消息,直到consumer发送消费成功确认。
- 我们先不理会重复消息的问题,通过两次消息落地加补偿,下游是一定可以收到消息的。然后依赖状态机版本号等方式做判重,更新自己的业务,就实现了最终一致性。
最终一致性不是消息队列的必备特性,但确实可以依靠消息队列来做最终一致性的事情。另外,所有不保证100%不丢消息的消息队列,理论上无法实现最终一致性。好吧,应该说理论上的100%,排除系统严重故障和bug。
像Kafka一类的设计,在设计层面上就有丢消息的可能(比如定时刷盘,如果掉电就会丢消息)。哪怕只丢千分之一的消息,业务也必须用其他的手段来保证结果正确。
广播
消息队列的基本功能之一是进行广播。如果没有消息队列,每当一个新的业务方接入,我们都要联调一次新接口。有了消息队列,我们只需要关心消息是否送达了队列,至于谁希望订阅,是下游的事情,无疑极大地减少了开发和联调的工作量。
比如本文开始提到的产品中心发布产品变更的消息,以及景点库很多去重更新的消息,可能“关心”方有很多个,但产品中心和景点库只需要发布变更消息即可,谁关心谁接入。
错峰与流控
试想上下游对于事情的处理能力是不同的。比如,Web前端每秒承受上千万的请求,并不是什么神奇的事情,只需要加多一点机器,再搭建一些LVS负载均衡设备和Nginx等即可。但数据库的处理能力却十分有限,即使使用SSD加分库分表,单机的处理能力仍然在万级。由于成本的考虑,我们不能奢求数据库的机器数量追上前端。
这种问题同样存在于系统和系统之间,如短信系统可能由于短板效应,速度卡在网关上(每秒几百次请求),跟前端的并发量不是一个数量级。但用户晚上个半分钟左右收到短信,一般是不会有太大问题的。如果没有消息队列,两个系统之间通过协商、滑动窗口等复杂的方案也不是说不能实现。但系统复杂性指数级增长,势必在上游或者下游做存储,并且要处理定时、拥塞等一系列问题。而且每当有处理能力有差距的时候,都需要单独开发一套逻辑来维护这套逻辑。所以,利用中间系统转储两个系统的通信内容,并在下游系统有能力处理这些消息的时候,再处理这些消息,是一套相对较通用的方式。
总而言之,消息队列不是万能的。对于需要强事务保证而且延迟敏感的,RPC是优于消息队列的。
对于一些无关痛痒,或者对于别人非常重要但是对于自己不是那么关心的事情,可以利用消息队列去做。
支持最终一致性的消息队列,能够用来处理延迟不那么敏感的“分布式事务”场景,而且相对于笨重的分布式事务,可能是更优的处理方式。
当上下游系统处理能力存在差距的时候,利用消息队列做一个通用的“漏斗”。在下游有能力处理的时候,再进行分发。
如果下游有很多系统关心你的系统发出的通知的时候,果断地使用消息队列吧。
MQ的流派划分
MQ的主要流派分为两种,一种是有Broker的MQ,一种是没有Broker的MQ。没有Broker的MQ可以看成一次RPC,有Broker的MQ可以看成是把一次RPC变成2次或者3次RPC。
有Broker的MQ
这个流派通常有一台服务器作为 Broker,所有的消息都通过它中转。生产者把消息发送给它就结束自己的任务了,Broker 则把消息主动推送给消费者或者让消费者从Broker拉取。
一个消息队列配备Broker,无外乎要做两件事情:
- 消息的转储,在更合适的时间点投递,或者通过一系列手段辅助消息最终能送达消费机。
- 规范一种范式和通用的模式,以满足解耦、最终一致性、错峰等需求。
这种MQ的代表有kafka、RabbitMQ、JMS。由于本篇文章并不具体讲某个MQ,所以仅简单说一下最火的kafka,如下图:
kafka是一个发布订阅消息系统,由topic区分消息种类,每个topic中可以有多个partition,每个kafka集群有一个多个broker服务器组成,producer可以发布消息到kafka中,consumer可以消费kafka中的数据。kafka就是生产者和消费者中间的一个暂存区,可以保存一段时间的数据保证使用。zk在kafka中做元数据存储以及partition的failover。在下一个大的kafka版本中,zk将会被去除。
无Broker的MQ
这种流派的MQ就是单纯的把消息发送看成是一次RPC了,主要的代表有ZeroMQ。
文章首发于:
九神带你理解消息队列MQ