RocketMQ 简介

引言

本系列文章详细地介绍了 RocketMQ 的设计思想和实现细节,本文作为开篇,着重介绍了 RocketMQ 的设计思想已经常见使用场景,更多关于 RocketMQ 的文章均收录于<RocketMQ系列文章>;

功能介绍

简单来说,消息队列就是基础数据结构课程里“先进先出”的一种数据结构,但是如果要消除单点故障,保证消息传输的可靠性,并且还能应对大流量的冲击,对消息队列的要求就很高了。现在互联网“微架构”模式兴起,原有大型集中式的IT服务因为各种弊端,通常被分拆成细粒度的多个“微服务”,这些微服务可以在一个局域网内,也可能跨机房部署。一方面对服务之间松耦合的要求越来越高,另一方面,服务之间的联系却越来越紧密,对通信质量的要求也越来越高。分布式消息队列可以提供应用解耦、流量削峰、消息分发等功能,已经成为大型互联网服务架构里标配的中间件。

应用解耦

复杂的应用里会存在多个子系统,比如在电商应用中有订单系统、库存系统、物流系统、支付系统等。这个时候如果各个子系统之间的耦合性太高,整体系统的可用性就会大幅降低。多个低错误率的子系统强耦合在一起,得到的是一个高错误率的整体系统。

以电商应用为例,用户创建订单后,如果耦合调用库存系统、物流系统、支付系统,任何一个子系统出了故障或者因为升级等原因暂时不可用,都会造成下单操作异常,影响用户使用体验。

如下图所示,当转变成基于消息队列的方式后,系统可用性就高多了,比如物流系统因为发生故障,需要几分钟的时间来修复,在这几分钟的时间里,物流系统要处理的内容被缓存在消息队列里,用户的下单操作可以正常完成。当物流系统恢复后,补充处理存储在消息队列里的订单信息即可,终端用户感知不到物流系统发生过几分钟的故障。
decoupling

流量削峰

电商服务中一般都会有大促活动,在大促时,大部分应用系统流量会在瞬间猛增,这个时候如果没有缓冲机制,不可能承受住短时大流量的冲击。通过利用消息队列,把大量的请求暂存起来,分散到相对长的一段时间内处理,能大大提高系统的稳定性和用户体验。

举个例子,如果订单系统每秒最多能处理一万次下单,这个处理能力应对正常时段的下单是绰绰有余的,正常时段我们下单后一秒内就能返回结果。在大促活动时,如果没有消息队列这种缓冲机制,为了保证系统稳定,只能在订单超过一万次后就不允许用户下单了。如果有消息队列做缓冲,我们可以取消这个限制,把一秒内下的订单分散成一段时间来处理,这时有些用户可能在下单后十几秒才能收到下单成功的状态,但是也比不能下单的体验要好。

使用消息队列进行流量削峰,很多时候不是因为能力不够,而是出于经济性的考量。

消息分发

在大数据时代,数据对很多公司来说就像金矿,公司需要依赖对数据的分析,进行用户画像、精准推送、流程优化等各种操作,并且对处理的实时性要求越来越高。数据是不断产生的,各个分析团队、算法团队都要依赖这些数据来进行工作,这个时候有个可持久化的消息队列就非常重要。数据的产生方只需要把各自的数据写入一个消息队列即可,数据使用方根据各自需求订阅感兴趣的数据,不同数据团队所订阅的数据可以重复也可以不重复,互不干扰,也不必和数据产生方关联。

如下图所示,各个子系统将日志数据不停地写入消息队列,不同的数据处理系统有各自的Offset,互不影响。甚至某个团队处理完的结果数据也可以写入消息队列,作为数据的产生方,供其他团队使用,避免重复计算。在大数据时代,消息队列已经成为数据处理系统不可或缺的一部分。
message-dispatch

方便动态扩容

我们已经知道,消息队列可以帮我们缓存用户的请求,让我们有更加宽裕的时间来处理这些请求,那么对于请求越积压越多的情况,显然只通过现存下游服务持续消费是无法满足的。这时候,就需要根据消息队列中数据积压的情况动态的增加下游服务的节点数,避免消息越积压越多,最后到无法控制的地步。

保证最终一致性

既然前面已经引入了”微服务”的概念,必然就会牵扯出分布式事务的问题,而业内解决分布式事务问题基本是采用如下两套方案:

  • 基于TCC的事务框架
  • 消息队列

如果服务双方是同步调用,即要么一起成功,要么一起失败,则此时应该选用TCC的事务框架,这部分内容我们以后会写一篇文章介绍分布式事务框架————seata,那时再专门进行说明。

如果服务双方是异步调用,即上游服务落库后立即返回,不等待下游服务的执行结果时,一般都会采用消息队列来实现。下面的例子虽然不是RocketMQ的方案,但是是使用消息队列解决最终一致性问题的一个通用方案,我们不妨先来看一看。
finally-consistent
图中的过程表述的是将支付宝中的钱转入余额宝,操作步骤如下:

  1. 支付宝将zhansan的余额扣除100并将分布式事务消息记录写入本地库中
    • 借助了本地数据库的ACID属性,保证余额落库和消息记录落库的ACID
  2. 后台有一个定时程序,将本地库中未成功发送到消息队列的消息进行发送,消息中既包含余额宝要处理的账户的金额,也包含分布式事务的id
    • 引入本地消息记录表的意义:如果步骤1中直接余额落库,并发送消息,无法保证其原子性。可能消息发送成功了,但是最终本地事务提交失败。如果先提交本地事务,再发送消息,也可能本地提交成功,但是消息未发送。所以在步骤1中,将余额和消息记录同时落库,最后让定时任务去扫描未发送的消息,并进行消息发送,这时候发送成功后再将消息记录标记为已发送
    • 这样虽然保证了,消息一定会被发送到消息队列,但是没办法保证消息只发送1份。因为可能消息发送成功后还没来得及修改本地消息记录的状态,就停机了。这时,重启服务器后,会产生重复的消息,这就需要下游服务提供幂等性支持(重复处理同一条消息,不会造成数据错误)
  3. 余额宝从消息队列中拿到消息后,在同一个事务中将消息中存储的分布式事务id存储在本地的消息处理表中,然后修改zhansan的余额,最后提交事务
    • 这样,如果收到一条重复的消息,在将消息插入到本地消息处理表时,就会发生事务id重复的错误,让事务回滚,从而保证了幂等性

至此,大家应该已经了解了通过消息队列来处理分布式事务的通用解决方案了,在这个例子中我们可以看出,消息的Provider需要在自己的服务中添加一个消息发送表,并维护一个循环任务来发送消息。这对其来说有很大的服务侵入性,在本文的后段,我会介绍RocketMQ的分布式事务方案,它通过自己的一些机制,降低了对消息Provider的侵入性。

设计理念与目标

设计理念

RocketMQ 设计基于主题的发布与订阅模式,其核心功能包括消息发送,消息存储(Broker),消息消费,整体设计追求简单与性能第一,主要体现在如下三个方面。

首先,NameServer设计极其简单,摒弃了业界常用的使用Zookeeper充当信息管理的“注册中心”,而是自研NameServer来实现元数据的管理(Topic路由信息等)。从实际需求出发,因为Topic路由信息无须在集群之间保持强一致,追求最终一致性,并且能容忍分钟级的不一致。正是基于此种情况,RocketMQ的NameServer集群之间互不通信,极大地降低了NameServer实现的复杂程度,对网络的要求也降低了不少,但是性能相比较Zookeeper有了极大的提升。

其次是高效的IO存储机制。RocketMQ追求消息发送的高吞吐量,RocketMQ的消息存储文件设计成文件组的概念,组内单个文件大小固定,方便引人内存映射机制,所有主题的消息存储基于顺序写,极大地提供了消息写性能,同时为了兼顾消 息消费与消息查找,引入了消息消费队列文件与索引文件。

最后是容忍存在设计缺陷,适当将某些工作下放给RocketMQ使用者。消息中间件的实现者经常会遇到一个难题:如何保证消息一定能被消息消费者消费,并且保证只消费一次。RocketMQ的设计者给出的解决办法是不解决这个难题,而是退而求其次,只保证消息被消费者消费,但设计上允许消息被重复消费,这样极大地简化了消息中间件的内核,使得实现消息发送高可用变得非常简单与高效消息重复问题由消费者在消息消费时实现幂等。

设计目标

RocketMQ作为一款消息中间件,需要解决如下问题。
架构模式
RocketMQ与大部分消息中间件一样,采用发布订阅模式,基本的参与组件主要包括:消息发送者、消息服务器(消息存储)、消息消费、路由发现。
顺序消费
所谓顺序消息,就是消息消费者按照消息达到消息存储服务器的顺序消费。RocketMQ可以严格保证消息有序,但是相较于无序队列来说,性能上会有很大的损失,不过这也是在所难免的。
消息过滤
消息过滤是指在消息消费时,消息消费者可以对同一主题下的消息按照规则只消费自己感兴趣的消息。RocketMQ消息过滤支持在服务端与消费端的消息过滤机制。

  • 消息在Broker端过滤。Broker只将消息消费者感兴趣的消息发送给消息消费者。
  • 消息在消息消费端过滤,消息过滤方式完全由消息消费者自定义,但缺点是有很多无用的消息会从Broker传输到消费端。

消息存储
消息中间件的一个核心实现是消息的存储对消息存储一般有如下两个维度的考量: 消息堆积能力和消息存储性能。RocketMQ追求消息存储的高性能,引人内存映射机制,所有主题的消息顺序存储在同一个文件中。同时为了避免消息无限在消息存储服务器中累积,引入了消息文件过期机制与文件存储空间报警机制。
消息高可用性
通常影响消息可靠性的有以下几种情况。

  1. Broker正常关机。
  2. Broker异常 Crash。
  3. OS Crash。
  4. 机器断电,但是能立即恢复供电情况 。
  5. 机器无法开机(可能是CPU、主板、内存等关键设备损坏)。
  6. 磁盘设备损坏。

针对上述情况,情况1~4的RocketMQ在同步刷盘机制下可以确保不丢失消息,在异步刷盘模式下,会丢失少量消息。情况5-6属于单点故障,一旦发生,该节点上的消息全部丢失,如果开启了异步复制机制,RoketMQ能保证只丢失少量消息,如果使用Master Slave双写机制,可以保证不丢失消息,从而满足消息可靠性要求极高的场合。
消费低延迟
RocketMQ在消息不发生消息堆积时,以长轮询模式实现准实时的消息推送模式。
确保消息必须被消费一次
RocketMQ通过消息消费确认机制(ACK)来确保消息至少被消费一次,但由于ACK消息有可能丢失等其他原因,RocketMQ无法做到消息只被消费一次,有重复消费的可能。
回溯消息
回溯消息是指消息消费端已经消费成功的消息,由于业务要求需要重新消费消息。RocketMQ支持按时间回溯消息,时间维度可精确到毫秒,可以向前或向后回溯。
消息堆积
消息中间件的主要功能是异步解耦,必须具备应对前端的数据洪峰,提高后端系统的可用性,必然要求消息中间件具备一定的消息堆积能力。RocketMQ消息存储使用磁盘文件(内存映射机制),并且在物理布局上为多个大小相等的文件组成逻辑文件组,可以无限循环使用。RocketMQ消息存储文件并不是永久存储在消息服务器端,而是提供了过期机制,默认保留3天。
定时消息
定时消息是指消息发送到Broker后,不能被消息消费端立即消费,要到特定的时间点或者等待特定的时间后才能被消费。如果要支持任意精度的定时消息消费,必须在消息服务端对消息进行排序,势必带来很大的性能损耗,故RocketMQ不支持任意进度的定时消息,而只支持特定延迟级别。
消息重试机制
消息重试是指消息在消费时,如果发送异常,消息中间件需要支持消息重新投递,RocketMQ支持消息重试机制。

架构

architecture
RocketMQ集群中包含4个模块:Namesrv,Broker,Producer,Consumer。

  • Namesrv: 存储当前集群所有Brokers信息、Topic跟Broker的对应关系。
  • Broker: 集群最核心模块,主要负责Topic消息存储、消费者的消费位点管理(消费进度)。
  • Producer: 消息生产者,每个生产者都有一个ID(编号),多个生产者实例可以共用同一个ID。同一个ID下所有实例组成一个生产者集群。
  • Consumer: 消息消费者,每个订阅者也有一个ID(编号),多个消费者实例可以共用同一个ID。同一个ID下所有实例组成一个消费者集群。

接下来,我们将按照RocketMQ中的模块,挨个介绍其实现方案。

参考内容

[1]《RocketMQ技术内幕》
[2]《RocketMQ实战与原理解析》
[3] 老生常谈——利用消息队列处理分布式事务
[4] RocketMQ架构解析
[5] MappedByteBuffer VS FileChannel 孰强孰弱?
[6] 文件 IO 操作的一些最佳实践
[7] 海量数据处理之Bloom Filter详解
[8] rocketmq GitHub Wiki

贝克街的流浪猫 wechat
您的打赏将鼓励我继续分享!
  • 本文作者: 贝克街的流浪猫
  • 本文链接: https://www.beikejiedeliulangmao.top/middleware/rocketmq/introduction/
  • 版权声明: 本博客所有文章除特别声明外,均采用 BY-NC-SA 许可协议。转载请注明出处!
  • 创作声明: 本文基于上述所有参考内容进行创作,其中可能涉及复制、修改或者转换,图片均来自网络,如有侵权请联系我,我会第一时间进行删除。