消息队列选型
大家好,我是易安!今天我们聊下消息队列常见选型。
消息队列作用
谈选型之前我们先讲下我们为什么需要消息队列。
消息队列是一种很流行的技术,自从系统间开始通信时,消息队列就出现了。然而,对消息队列给出一个精确的定义却颇为困难。我们知道,消息队列的核心功能是收发消息,但它的作用远不止于解决应用间通信问题这么简单。
让我们通过一个例子来阐述消息队列的作用。假设小明拥有一家巧克力工厂,制作美味巧克力需要经历三个阶段:首先将可可豆研磨成可可粉,接着把可可粉加热并加入糖制成巧克力浆,最后将巧克力浆倒入模具,撒上碎坚果,经过冷却便成为巧克力产品。
起初,工人们在研磨出一桶可可粉后,便将其送至下一阶段的工人手中,然后回去继续研磨。小明很快意识到,其实工人不必亲自运送半成品。于是,他在各个阶段之间设置了传送带,使得工人只需将研磨好的可可粉放在传送带上,便可继续处理下一批可可豆。传送带解决了不同阶段工序间的“通信”问题。
尽管传送带提高了生产效率,但也带来了新的问题:各个阶段的生产速度并不一致。当一桶可可粉传送至巧克力浆车间时,工人可能正在处理上一批可可粉,无法立即接收。各个阶段的工人需要协调何时将半成品放置在传送带上。如果上下游阶段处理速度不匹配,工人需要互相等待,确保传送带上的半成品不被遗漏。
为解决这一问题,小明在每组传送带的下游设置了一个暂存半成品的仓库。这样一来,上游工人无需等待下游工人,随时可将处理好的半成品放置在传送带上。无法及时接收的半成品将暂存在仓库中,供下游工人随时取用。这些仓库实际上在“通信”过程中发挥了“缓存”的作用。
传送带解决了半成品运输问题,仓库可以暂存一些半成品,解决了上下游生产速度不一致的问题,小明在不知不觉中实现了一个巧克力工厂版的消息队列。
消息队列使用场景
我们日常开发中,哪些问题适合使用消息队列解决呢?
1. 异步处理
大多数程序员在面试中,应该都问过或被问过一个经典却没有标准答案的问题:如何设计一个秒杀系统?这个问题可以有一百个版本的合理答案,但大多数答案中都离不开消息队列。
秒杀系统需要解决的核心问题是,如何利用有限的服务器资源,尽可能多地处理短时间内的海量请求。我们知道,处理一个秒杀请求包含了很多步骤,例如:
-
风险控制; -
库存锁定; -
生成订单; -
短信通知; -
更新统计数据。
如果没有任何优化,正常的处理流程是:App将请求发送给网关,依次调用上述5个流程,然后将结果返回给APP。
对于这5个步骤来说,能否决定秒杀成功,实际上只有风险控制和库存锁定这2个步骤。只要用户的秒杀请求通过风险控制,并在服务端完成库存锁定,就可以给用户返回秒杀结果了,对于后续的生成订单、短信通知和更新统计数据等步骤,并不一定要在秒杀请求中处理完成。
所以当服务端完成前面2个步骤,确定本次请求的秒杀结果后,就可以马上给用户返回响应,然后把请求的数据放入消息队列中,由消息队列异步地进行后续的操作。
处理一个秒杀请求,从5个步骤减少为2个步骤,这样不仅响应速度更快,并且在秒杀期间,我们可以把大量的服务器资源用来处理秒杀请求。秒杀结束后再把资源用于处理后面的步骤,充分利用有限的服务器资源处理更多的秒杀请求。
可以看到,在这个场景中,消息队列被用于实现服务的异步处理。 这样做的好处是:
-
可以更快地返回结果; -
减少等待,自然实现了步骤之间的并发,提升系统总体的性能。
2. 流量控制
继续说我们的秒杀系统,我们已经使用消息队列实现了部分工作的异步处理,但我们还面临一个问题:如何避免过多的请求压垮我们的秒杀系统?
一个设计健壮的程序有自我保护的能力,也就是说,它应该可以在海量的请求下,还能在自身能力范围内尽可能多地处理请求,拒绝处理不了的请求并且保证自身运行正常。不幸的是,现实中很多程序并没有那么“健壮”,而直接拒绝请求返回错误对于用户来说也是不怎么好的体验。
因此,我们需要设计一套足够健壮的架构来将后端的服务保护起来。 我们的设计思路是,使用消息队列隔离网关和后端服务,以达到流量控制和保护后端服务的目的。
加入消息队列后,整个秒杀流程变为:
-
网关在收到请求后,将请求放入请求消息队列; -
后端服务从请求消息队列中获取APP请求,完成后续秒杀处理过程,然后返回结果。
秒杀开始后,当短时间内大量的秒杀请求到达网关时,不会直接冲击到后端的秒杀服务,而是先堆积在消息队列中,后端服务按照自己的最大处理能力,从消息队列中消费请求进行处理。
对于超时的请求可以直接丢弃,APP将超时无响应的请求处理为秒杀失败即可。运维人员还可以随时增加秒杀服务的实例数量进行水平扩容,而不用对系统的其他部分做任何更改。
这种设计的优点是:能根据下游的处理能力自动调节流量,达到“削峰填谷”的作用。但这样做同样是有代价的:
-
增加了系统调用链环节,导致总体的响应时延变长。 -
上下游系统都要将同步调用改为异步消息,增加了系统的复杂度。
那还有没有更简单一点儿的流量控制方法呢?如果我们能预估出秒杀服务的处理能力,就可以用消息队列实现一个令牌桶,更简单地进行流量控制。
令牌桶控制流量的原理是:单位时间内只发放固定数量的令牌到令牌桶中,规定服务在处理请求之前必须先从令牌桶中拿出一个令牌,如果令牌桶中没有令牌,则拒绝请求。这样就保证单位时间内,能处理的请求不超过发放令牌的数量,起到了流量控制的作用。
实现的方式也很简单,不需要破坏原有的调用链,只要网关在处理APP请求时增加一个获取令牌的逻辑。
令牌桶可以简单地用一个有固定容量的消息队列加一个“令牌发生器”来实现:令牌发生器按照预估的处理能力,匀速生产令牌并放入令牌队列(如果队列满了则丢弃令牌),网关在收到请求时去令牌队列消费一个令牌,获取到令牌则继续调用后端秒杀服务,如果获取不到令牌则直接返回秒杀失败。
以上是常用的使用消息队列两种进行流量控制的设计方法,你可以根据各自的优缺点和不同的适用场景进行合理选择。
3. 服务解耦
消息队列的另外一个作用,就是实现系统应用之间的解耦。再举一个电商的例子来说明解耦的作用和必要性。
我们知道订单是电商系统中比较核心的数据,当一个新订单创建时:
-
支付系统需要发起支付流程; -
风控系统需要审核订单的合法性; -
客服系统需要给用户发短信告知用户; -
经营分析系统需要更新统计数据;
这些订单下游的系统都需要实时获得订单数据。随着业务不断发展,这些订单下游系统不断的增加,不断变化,并且每个系统可能只需要订单数据的一个子集,负责订单服务的开发团队不得不花费很大的精力,应对不断增加变化的下游系统,不停地修改调试订单系统与这些下游系统的接口。任何一个下游系统接口变更,都需要订单模块重新进行一次上线,对于一个电商的核心服务来说,这几乎是不可接受的。
所有的电商都选择用消息队列来解决类似的系统耦合过于紧密的问题。引入消息队列后,订单服务在订单变化时发送一条消息到消息队列的一个主题Order中,所有下游系统都订阅主题Order,这样每个下游系统都可以获得一份实时完整的订单数据。
无论增加、减少下游系统或是下游系统需求如何变化,订单服务都无需做任何更改,实现了订单服务与下游服务的解耦。
上述,我们介绍了消息队列的三种使用场景:异步处理、流量控制和服务解耦。当然,消息队列的适用范围不仅仅局限于这些场景,还有包括:
-
作为发布/订阅系统实现一个微服务级系统间的观察者模式; -
连接流计算任务和数据; -
用于将消息广播给大量接收者。
简单的说,我们在单体应用里面需要用队列解决的问题,在分布式系统中大多都可以用消息队列来解决。
同时我们也要认识到,消息队列也有它自身的一些问题和局限性,包括:
-
引入消息队列带来的延迟问题; -
增加了系统的复杂度; -
可能产生数据不一致的问题。
所以我们说没有最好的架构,只有最适合的架构,根据目标业务的特点和自身条件选择合适的架构,才是体现一个架构师功力的地方。
在消息队列的技术选型这个问题上,也是同样的道理。并不存在说,哪个消息队列就是“最好的”。常用的这几个消息队列,每一个产品都有自己的优势和劣势,你需要根据现有系统的情况,选择最适合你的那款产品,那么我们如何来选择消息队列呢?
选型原则
虽然这些消息队列产品在功能和特性方面各有优劣,但我们在选择的时候要有一个基本原则,保证入选的产品至少是及格的。
接下来我们先说一下这及格的标准是什么样的。
首先,必须是开源的产品,这个非常重要。开源意味着,如果有一天你使用的消息队列遇到了一个影响你系统业务的Bug,你至少还有机会通过修改源代码来迅速修复或规避这个Bug,解决你的系统火烧眉毛的问题,而不是束手无策地等待开发者不一定什么时候发布的下一个版本来解决。
其次,这个产品必须是近年来比较流行并且有一定社区活跃度的产品。流行的好处是,只要你的使用场景不太冷门,你遇到Bug的概率会非常低,因为大部分你可能遇到的Bug,其他人早就遇到并且修复了。你在使用过程中遇到的一些问题,也比较容易在网上搜索到类似的问题,然后很快的找到解决方案。
还有一个优势就是,流行的产品与周边生态系统会有一个比较好的集成和兼容,比如,Kafka和Flink就有比较好的兼容性,Flink内置了Kafka的Data Source,使用Kafka就很容易作为Flink的数据源开发流计算应用,如果你用一个比较小众的消息队列产品,在进行流计算的时候,你就不得不自己开发一个Flink的Data Source。
最后,作为一款及格的消息队列产品,必须具备的几个特性包括:
-
消息的可靠传递:确保不丢消息; -
Cluster:支持集群,确保不会因为某个节点宕机导致服务不可用,当然也不能丢消息; -
性能:具备足够好的性能,能满足绝大多数场景的性能要求。
接下来我们一起看一下有哪些符合上面这些条件,可供选择的开源消息队列产品。
主流产品
1. RabbitMQ
首先,我们说一下老牌儿消息队列RabbitMQ。RabbitMQ是使用一种比较小众的编程语言:Erlang语言编写的,它最早是为电信行业系统之间的可靠通信设计的,也是少数几个支持AMQP协议的消息队列之一。
RabbitMQ就像它的名字中的兔子一样:轻量级、迅捷,它的Slogan,也就是宣传口号,也很明确地表明了RabbitMQ的特点:Messaging that just works,“开箱即用的消息队列”。也就是说,RabbitMQ是一个相当轻量级的消息队列,非常容易部署和使用。
另外RabbitMQ还号称是世界上使用最广泛的开源消息队列,是不是真的使用率世界第一,我们没有办法统计,但至少是“最流行的消息中间之一”,这是没有问题的。
RabbitMQ一个比较有特色的功能是支持非常灵活的路由配置,和其他消息队列不同的是,它在生产者(Producer)和队列(Queue)之间增加了一个Exchange模块,你可以理解为交换机。
这个Exchange模块的作用和交换机也非常相似,根据配置的路由规则将生产者发出的消息分发到不同的队列中。路由的规则也非常灵活,甚至你可以自己来实现路由规则。基于这个Exchange,可以产生很多的玩儿法,如果你正好需要这个功能,RabbitMQ是个不错的选择。
RabbitMQ的客户端支持的编程语言大概是所有消息队列中最多的,如果你的系统是用某种冷门语言开发的,那你多半可以找到对应的RabbitMQ客户端。
接下来说下RabbitMQ的几个问题。
-
RabbitMQ对消息堆积的支持并不好,在它的设计理念里面,消息队列是一个管道,大量的消息积压是一种不正常的情况,应当尽量去避免。当大量消息积压的时候,会导致RabbitMQ的性能急剧下降。
-
RabbitMQ的性能是我介绍的这几个消息队列中最差的,根据官方给出的测试数据综合我们日常使用的经验,依据硬件配置的不同,它大概每秒钟可以处理几万到十几万条消息。其实,这个性能也足够支撑绝大多数的应用场景了,不过,如果你的应用对消息队列的性能要求非常高,那不要选择RabbitMQ。
-
RabbitMQ使用的编程语言Erlang,这个编程语言不仅是非常小众的语言,更麻烦的是,这个语言的学习曲线非常陡峭。大多数流行的编程语言,比如Java、C/C++、Python和JavaScript,虽然语法、特性有很多的不同,但它们基本的体系结构都是一样的,你只精通一种语言,也很容易学习其他的语言,短时间内即使做不到精通,但至少能达到“会用”的水平。就像一个以英语为母语的人,学习法语、德语都很容易,但是你要是让他去学汉语,那基本上和学习其他这些语言不是一个难度级别的。很不幸的是,Erlang就是编程语言中的“汉语”。所以如果你想基于RabbitMQ做一些扩展和二次开发什么的,建议你慎重考虑一下可持续维护的问题。
2. RocketMQ
RocketMQ是阿里巴巴在2012年开源的消息队列产品,后来捐赠给 Apache 软件基金会,2017正式毕业,成为Apache的顶级项目。阿里内部也是使用RocketMQ作为支撑其业务的消息队列,经历过多次“双十一”考验,它的性能、稳定性和可靠性都是值得信赖的。作为优秀的国产消息队列,近年来越来越多的被国内众多大厂使用。
我在总结RocketMQ的特点时,发现很难找出RocketMQ有什么特别让我印象深刻的特点,也很难找到它有什么缺点。
RocketMQ就像一个品学兼优的好学生,有着不错的性能,稳定性和可靠性,具备一个现代的消息队列应该有的几乎全部功能和特性,并且它还在持续的成长中。
RocketMQ有非常活跃的中文社区,大多数问题你都可以找到中文的答案,也许会成为你选择它的一个原因。另外,RocketMQ使用Java语言开发,它的贡献者大多数都是中国人,源代码相对也比较容易读懂,你很容易对RocketMQ进行扩展或者二次开发。
RocketMQ对在线业务的响应时延做了很多的优化,大多数情况下可以做到毫秒级的响应, 如果你的应用场景很在意响应时延,那应该选择使用RocketMQ。
RocketMQ的性能比RabbitMQ要高一个数量级,每秒钟大概能处理几十万条消息。
RocketMQ的一个劣势是,作为国产的消息队列,相比国外的比较流行的同类产品,在国际上还没有那么流行,与周边生态系统的集成和兼容程度要略逊一筹。
3. Kafka
最后我们聊一聊Kafka。Kafka最早是由LinkedIn开发,目前也是Apache的顶级项目。Kafka最初的设计目的是用于处理海量的日志。
在早期的版本中,为了获得极致的性能,在设计方面做了很多的牺牲,比如不保证消息的可靠性,可能会丢失消息,也不支持集群,功能上也比较简陋,这些牺牲对于处理海量日志这个特定的场景都是可以接受的。这个时期的Kafka甚至不能称之为一个合格的消息队列。
但是,请注意,重点一般都在后面。随后的几年Kafka逐步补齐了这些短板,你在网上搜到的很多消息队列的对比文章还在说Kafka不可靠,其实这种说法早已经过时了。当下的Kafka已经发展为一个非常成熟的消息队列产品,无论在数据可靠性、稳定性和功能特性等方面都可以满足绝大多数场景的需求。
Kafka与周边生态系统的兼容性是最好的没有之一,尤其在大数据和流计算领域,几乎所有的相关开源软件系统都会优先支持Kafka。
Kafka使用Scala和Java语言开发,设计上大量使用了批量和异步的思想,这种设计使得Kafka能做到超高的性能。Kafka的性能,尤其是异步收发的性能,是三者中最好的,但与RocketMQ并没有量级上的差异,大约每秒钟可以处理几十万条消息。
我曾经使用配置比较好的服务器对Kafka进行过压测,在有足够的客户端并发进行异步批量发送,并且开启压缩的情况下,Kafka的极限处理能力可以超过每秒2000万条消息。
但是Kafka这种异步批量的设计带来的问题是,它的同步收发消息的响应时延比较高,因为当客户端发送一条消息的时候,Kafka并不会立即发送出去,而是要等一会儿攒一批再发送,当然你也可以配置按照时间拉取。当你的业务场景中,每秒钟消息数量没有那么多的时候,Kafka的时延反而会比较高。所以, Kafka不太适合在线业务场景。
其他产品
除了上面给你介绍的三大消息队列之外,还有几个产品,我个人的观点是,这些产品之所以没那么流行,或多或少都有着比较明显的短板,不推荐使用。在这儿呢,我简单介绍一下,纯当丰富你的知识广度。
先说ActiveMQ,ActiveMQ是最老牌的开源消息队列,是十年前唯一可供选择的开源消息队列,目前已进入老年期,社区不活跃。无论是功能还是性能方面,ActiveMQ都与现代的消息队列存在明显的差距,它存在的意义仅限于兼容那些还在用的爷爷辈儿的系统。
接下来说说ZeroMQ,严格来说ZeroMQ并不能称之为一个消息队列,而是一个基于消息队列的多线程网络库,如果你的需求是将消息队列的功能集成到你的系统进程中,可以考虑使用ZeroMQ。
最后说一下Pulsar,很多人可能都没听说过这个产品,Pulsar是一个新兴的开源消息队列产品,最早是由Yahoo开发,目前处于成长期,流行度和成熟度相对没有那么高。与其他消息队列最大的不同是,Pulsar采用存储和计算分离的设计,官网号称延时远低于kafka,但是吞吐量超出kafka很多,当然这个统计的方式有待商榷,有兴趣后面我会出文章分析下。
总结
在了解了上面这些开源消息队列各自的特点和优劣势后,我相信你对于消息队列的选择已经可以做到心中有数了。我也总结了几条选择的建议供你参考。
如果说,消息队列并不是你将要构建系统的主角之一,你对消息队列功能和性能都没有很高的要求,只需要一个开箱即用易于维护的产品,我建议你使用RabbitMQ。
如果你的系统使用消息队列主要场景是处理在线业务,比如在交易系统中用消息队列传递订单,那RocketMQ的低延迟和金融级的稳定性是你需要的。
如果你需要处理海量的消息,像收集日志、监控信息或是前端的埋点这类数据,或是你的应用场景大量使用了大数据、流计算相关的开源产品,那Kafka是最适合你的消息队列。
如果我说的这些场景和你的场景都不符合,你看了我之前介绍的这些消息队列的特点后,还是不知道如何选择,那就选你最熟悉的吧。
如果本文对你有帮助的话,欢迎点赞分享,这对我继续分享&创作优质文章非常重要。感谢 !
本文由 mdnice 多平台发布