本文主要讲解MQ的常识,让大家先搞清楚:如果让你设计一个MQ,应该如何入手?需要考虑哪些问题?技术挑战是什么?有了这些基础之后,相信在接下来的几篇文章中谈到Kafka和RocketMQ这两个具体的消息中间件时,大家可以快速掌握主要脉络,区分出各自的特点。对于MQ来说,不管是RocketMQ、Kafka还是其他消息队列,它们的本质就是:发送、存储、消费。下面就以这个本质为根,由浅入深的讲MQ。01从MQ的本质出发,把MQ拆散,拆成碎片,都是“一邮一存一消费”,更直白一点,就是一个“转发器”。生产者先把消息投递到一个叫“队列”的容器中,然后从容器中取出消息,最后转发给消费者,如此而已。上图是消息队列最原始的模型,包含两个关键字:message和queue。1、消息:是要传输的数据,可以是最简单的文本字符串,也可以是自定义的复杂格式(只要能按照预定格式解析即可)。2.队列:大家应该都不陌生了。它是一种先进先出的数据结构。它是一个用于存储消息的容器。消息从队尾入队,队头出队。入队时发送消息的过程,出队时接收消息的过程。02原始模型的演进纵观当今最常用的消息队列产品(RocketMQ、Kafka等),你会发现它们都在最原始的消息模型上进行了扩展,同时提出了一些新的术语,如:主题(topic)、分区(partition)、队列(queue)等。为了彻底理解这些各种各样的新概念,让我们简化复杂性,从消息模型的演进开始(原因就像:架构从来没有设计过,而是演进过)2.1队列模型最初的消息队列就是中提到的原始模型第一部分是严格队列(Queue)。消息写入的顺序将按该顺序读出。但是队列没有“读”的操作。读取意味着离开队列并从队列头部“删除”消息。这就是队列模型:它允许多个生产者向同一个队列发送消息。但是,如果有多个消费者,其实是一种竞争关系,即一条消息只能被其中一个消费者接收,读完就删除。2.2发布-订阅模型如果一条消息数据需要分发给多个消费者,要求每个消费者接收到全量的消息。显然,队列模型不能满足这个要求。一种可能的解决方案是为每个消费者创建一个单独的队列,让生产者发送多个副本。这种做法很愚蠢,同样的数据会被复制多次,也是一种空间的浪费。为了解决这个问题,又演化出另一种消息模型:发布-订阅模型。在发布-订阅模型中,存储消息的容器变成了一个“主题”,订阅者在接收消息之前需要“订阅一个主题”。最终,每个订阅者都可以收到关于同一主题的全部消息。仔细比较一下它和“队列模式”的异同:生产者是发布者,队列是主题,消费者是订阅者,没有本质区别。唯一不同的是:一条消息数据是否可以被多次消费。2.3总结最后,让我做一个总结。以上两种模式简单来说就是:单播和广播的区别。而且,当发布-订阅模型只有一个订阅者时,它和队列模型是一样的,所以在功能上完全兼容队列模型。这也解释了为什么现代主流的RocketMQ和Kafka都是直接基于发布-订阅模型实现的?另外,为什么RabbitMQ中会有Exchange模块?其实也是为了解决消息传递的问题,变相实现发布订阅模型。包括大家接触到的“消费组”、“集群消费”、“广播消费”等概念,都与上述两种模型相关,应用层面最常见的情况:组间广播,组内单播,也属于这一类。因此,先掌握一些通用的理论,当你学习了各个消息中间件的具体实现原理时,其实可以更好地抓住本质,区分概念。03通过模型看MQ的应用场景目前MQ的应用场景有很多,大家可以背诵的有:系统解耦、异步通信、流量调峰。此外,还有延迟通知、最终一致性保证、顺序消息、流处理等。是先有消息模型,还是先有应用场景?答案一定是:先有应用场景(即先有问题),再有消息模型,因为消息模型只是解决方案的抽象。经过30多年的发展,MQ已经能够从最原始的队列模型发展到今天百花齐放的各种消息中间件(平台级解决方案)。感觉什么都一样,感谢:消息模型的适配性很广。让我们试着重新认识一下消息队列的模型。它实际解决的是:生产者和消费者之间的沟通问题。那么它和RPC有什么联系和区别呢?通过对比,可以明显看出两个区别:1.引入MQ后,之前的RPC变成了现在的两个RPC,生产者只与队列耦合,不需要知道消费者的存在根本。2.消息转储多了一个中间节点“queue”,相当于把同步改成了异步。回头想想MQ的所有应用场景,就不难理解为什么MQ适用了?因为这些应用场景都利用了以上两个特性。举个实际的例子,比如电商业务中最常见的“订单支付”场景:订单支付成功后,需要更新订单状态,更新用户积分,通知商家有新订单、更新推荐系统中的用户画像等。引入MQ后,订单支付现在只需要关注最重要的流程:更新订单状态。其他不重要的事情都交给MQ去通知。这就是MQ解决的核心问题:系统解耦。改造前,订单系统依赖3个外部系统。改造后仅依赖MQ,后续业务拓展(例如:营销系统拟对付费用户打赏优惠券),不涉及订单系统的修改,从而保证了核心流程性能的稳定性,降低维护成本。这种改造还带来了另一个好处:因为引入了MQ,更新用户积分、通知商户、更新用户画像等步骤全部异步执行,可以减少整体的订单支??付耗时,提高吞吐量订单系统。这就是MQ的另一个典型应用场景:异步通信。另外,由于队列可以转储消息,对于超出系统承载能力的场景,也就是所谓的流量削峰,MQ可以作为限流保护的“漏斗”。我们也可以利用队列本身的顺序性来满足必须按顺序传递消息的场景;使用队列+定时任务实现消息的延迟消费...MQ的其他应用场景基本类似,可以回到消息模型的特点去寻找其应用的原因,就不一一分析了这里。总之,建议大家从复杂多变的实际场景中回归到理论层面进行思考和抽象,这样才能理解得更透彻。04如何设计一个MQ?了解了以上的理论知识和应用场景后,我们一起来看看:如何设计一个MQ?4.1MQ的雏形我们先从简单版的MQ说起。如果我们只是实现一个很粗略的MQ,根本没有考虑生产环境的要求。如何设计?正如文章开头所说,任何MQ无非就是:发送、存储、消费。这是MQ的核心功能需求。另外,从技术角度看MQ通信模型,可以理解为:两次RPC+消息转储。有了这些理解,相信只要有一定的编程基础,不到一个小时就可以写出MQ的原型:1.直接使用成熟的RPC框架(Dubbo或Thrift)实现两个接口:发送消息和阅读信息。2.消息可以放在本地内存中,数据结构可以使用JDK自带的ArrayBlockingQueue。4.2编写一个适用于生产环境的MQ当然,我们的目标并不局限于一个MQ的原型,而是要实现一个可以在生产环境中使用的消息中间件。难度绝对不是一个数量级的。我们应该如何开始?什么?1、先抓住这个问题的重点。如果我们还是只考虑最基本的功能:发送消息、存储消息、消费消息(支持发布订阅模型)。这些基础功能在生产环境中会面临哪些挑战?我们可以很快想到:1、高并发场景下如何保证消息收发的性能?2、如何保证消息服务的高可用和可靠?3、如何保证服务可以横向扩展?4、如何保证消息存储也是可扩展的?5、如何管理各种元数据(如集群中的各个节点、主题、消费者关系等)考虑数据的一致性?可见,在设计MQ的时候,高并发场景下的三高问题都会遇到。“如何满足高性能、高可靠性等非功能性需求”是解决这个问题的关键。2.整体设计思路先来看一下整体架构,会涉及到三类角色:另外,进一步细化“一帖一存一消费”的核心流程后,相对完整的数据流为如下:基于以上两张图,我们可以快速的理清三类角色的作用,分别如下:1.Broker(服务器):MQ的核心部分是MQ的服务器。核心逻辑几乎都在这里了。它适用于生产者和消费者。提供RPC接口,负责消息的存储、备份和删除,以及消费者关系的维护。2、Producer:MQ的客户端之一,调用Broker提供的RPC接口发送消息。3、Consumer(消费者):MQ的另一个客户端,调用Broker提供的RPC接口接收消息,同时完成消费确认。3.在详细设计之后,讨论一些具体的技术难点和可行的解决方案。难点一:RPC通信解决了Broker、Producer和Consumer之间的通信问题。如果不重新发明轮子,可以直接使用成熟的RPC框架Dubbo或者Thrift来实现,这样就不需要考虑服务注册与发现、负载均衡、通信协议、以及序列化方法。当然你也可以基于Netty做底层通信,使用Zookeeper、Euraka等作为注册中心,然后自定义一个新的通信协议(类似Kafka),也可以基于一个标准化的MQ协议来实现,比如作为AMQP(类似于RabbitMQ)。与直接使用RPC框架相比,该方案具有更大的定制能力和优化空间。难点二:高可用设计高可用主要涉及两个方面:broker服务的高可用和存储方案的高可用。可以单独讨论。Broker服务的高可用只需要保证Broker可以水平扩展进行集群部署,而这又是通过服务自动注册和发现、负载均衡、超时重试机制、发送和消费消息时的ack机制来进一步保证的。存储方案的高可用有两种思路:1)参考Kafka的分区+多副本模式,但需要考虑分布式场景下的数据复制和一致性方案(类似Zab、Raft等协议),实现自动故障转移;2)也可以使用主流的DB、分布式文件系统、具有持久化能力的KV系统,这些都有自己的高可用方案。难点三:存储设计消息的存储方案是MQ的核心部分。在高可用设计中已经讨论了可靠性保证。如果可靠性要求不高,可以直接使用内存或者分布式缓存。这里我们重点说一下如何保证存储的高性能?这个问题的决定性因素在于存储结构的设计。目前主流的方案是:增加日志文件(数据部分)+索引文件(很多主流的开源MQ都是这种方式),索引设计可以考虑密集索引或者稀疏索引,可以使用跳表来查找消息,二级查找等,还可以通过页面缓存、操作系统的零拷贝等技术提高磁盘文件的读写性能。如果不追求高性能,也可以考虑现成的分布式文件系统、KV存储或数据库方案。难点四:消费者关系管理为了支持发布-订阅广播模式,Broker需要知道每个主题有哪些消费者订阅,并根据这种关系传递消息。由于Broker部署在集群中,消费者关系通常维护在公共存储上,可以基于Zookeeper、Apollo等配置中心进行管理和通知变更。难点五:高性能设计上面已经讨论了存储的高性能,当然还可以从其他方面进一步优化性能。比如Reactor网络IO模型,业务线程池的设计,生产端的批量发送,Broker端的异步刷机,消费者端的批量拉取等。4.3小结综上所述,需要回答得好:如何设计一个MQ?1、我们需要从两个方面入手:功能需求(消息收发)和非功能需求(高性能、高可用、高扩展等)。2、功能需求不是重点。足以涵盖MQ最基本的功能。至于延迟消息、事务消息、重试队列等高级特性,只是锦上添花。3、最重要的是:能够结合功能需求,理清整体的数据流向,然后按照这个思路去考虑如何满足非功能需求。这就是技术难点所在。05写在最后,本文从MQ的本质,一次发送,一次存储,一次消费出发,阐述了消息模型的演进过程,这是MQ的核心理论基础。基于此,大家更容易了解MQ的各种新名词和应用场景。最后通过回答:如何设计一个MQ?目的是让大家对MQ的核心组件和技术难点有一个清晰的认识。另外,带着这道题的答案去学习具体的消息中间件,比如Kafka、RocketMQ等,会有更多的侧重点。本文转载自微信公众号“五哥谈IT”,可通过以下二维码关注。转载本文请联系吴哥聊IT公众号。
