RocketMQ设计基于主题的发布与订阅模式,其核心功能包括消息发送、消息存储和消息消费,整体设计追求简单和性能高效,主要体现在如下3个方面:
作为一款消息中间件,RocketMQ需要解决的问题:
1)Broker异常崩溃。
2)操作系统崩溃。
3)机器断电,但是能立即恢复供电。
4)机器无法开机(可能是CPU、主板、内存等关键设备损坏)。
5)磁盘设备损坏。
对于前3种情况,RocketMQ在同步刷盘模式下可以确保不丢失消息,在异步刷盘模式下,会丢失少量消息。后2种情况属于单点故障,一旦发生,该节点上的消息会全部丢失。如果开启了异步复制机制,RoketMQ能保证只丢失少量消息。
消息中间件的设计思路一般是基于主题的订阅发布机制,消息生产者(Producer)发送某一主题的消息到消息服务器,消息服务器负责该消息的持久化存储,消息消费者(Consumer)订阅感兴趣的主题,消息服务器根据订阅信息(路由信息)将消息推送给消费者(推模式)或者消息消费者主动向消息服务器拉取消息(拉模式),从而实现消息生产者与消息消费者的解耦。
RocketMQ架构:
Broker消息服务器在启动时向所有NameServer注册,消息生产者在发送消息之前先从NameServer获取Broker服务器的地址列表,然后根据负载算法从列表中选择一台消息服务器发送消息。NameServer与每台Broker服务器保持长连接,并间隔10s检测Broker是否存活,如果检测到Broker宕机,则从路由注册表中将其移除,但是路由变化不会马上通知消息生产者。为什么要这样设计呢?这是为了降低NameServer实现的复杂性,因此需要在消息发送端提供容错机制来保证消息发送的高可用性。
NameServer本身的高可用性可通过部署多台NameServer服务器来实现,但彼此之间互不通信。虽然NameServer服务器之间在某一时刻的数据并不会完全相同,但对消息发送不会造成重大影响,无非就是短暂造成消息发送不均衡。
NameServer核心架构设计:
消息客户端与NameServer、Broker的交互设计要点:
1、Broker每隔30s向NameServer集群的每一台机器发送心跳包,包含自身创建的topic路由等信息。
2、消息客户端每隔30s向NameServer更新对应topic的路由信息。
3、NameServer收到Broker发送的心跳包时会记录时间戳。
4、NameServer每隔10s会扫描一次brokerLiveTable(存放心跳包的时间戳信息),如果在120s内没有收到心跳包,则认为Broker失效,更新topic的路由信息,将失效的Broker信息移除。
RocketMQ发送普通消息有3种实现方式:可靠同步发送、可靠异步发送和单向发送。
同步
:发送者向RocketMQ执行发送消息API时,同步等待,直到消息服务器返回发送结果。
异步
:发送者向RocketMQ执行发送消息API时,指定消息发送成功后的回调函数,调用消息发送API后,立即返回,消息发送者线程不阻塞,直到运行结束,消息发送成功或失败的回调任务在一个新的线程中执行。
单向
:消息发送者向RocketMQ执行发送消息API时,直接返回,不等待消息服务器的结果,也不注册回调函数。
消息发送者向某一个topic发送消息时,需要查询topic的路由信息。初次发送时会根据topic的名称向NameServer集群查询topic的路由信息,然后将其存储在本地内存缓存中,并且每隔30s依次遍历缓存中的topic,向NameServer查询最新的路由信息。如果成功查询到路由信息,会将这些信息更新至本地缓存,实现topic路由信息的动态感知。
发送端在自动发现主题的路由信息后,RocketMQ默认使用轮询算法进行路由的负载均衡。RocketMQ在消息发送时支持自定义的队列负载算法。需要注意的是,使用自定义的路由负载算法后,RocketMQ的重试机制将失效。
RocketMQ为了实现消息发送高可用,引入了两个非常重要的特性:
1、
消息发送重试机制
。RocketMQ在消息发送时如果出现失败,默认会重试两次。
2、故障规避机制
。当消息第一次发送失败时,如果下一次消息还是发送到刚刚失败的Broker上,其消息发送大概率还是会失败,因此为了保证重试的可靠性,在重试时会尽量避开刚刚接收失败的Broker,而是选择其他Broker上的队列进行发送,从而提高消息发送的成功率。
DefaultMQProducer是默认消息生产者实现类。
DefaultMQProducer的主要方法:
/**
* 创建主题
* key:目前无实际作用,可以与newTopic相同
* newTopic:主题名称
* queueNum:队列数量
* topicSysFlag:主题系统标签,默认为0
**/
void createTopic(String key, String newTopic, int queueNum, int topicSysFlag)
//同步发送消息,具体发送到主题中的哪个消息队列由负载算法决定
SendResult send(Message msg)
//同步发送消息,如果发送超过timeout则抛出超时异常
SendResult send(Message msg, final long timeout)
//异步发送消息,sendCallback参数是消息发送成功后的回调方法
void send(Message msg, SendCallback sendCallback)
//异步发送消息,如果发送超过timeout则抛出超时异常
void send(Message msg, SendCallback sendCallback, long timeout)
//单向消息发送,即不在乎发送结果,消息发送出去后该方法立即返回
void sendOneway(Message msg)
//同步方式发送消息,且发送到指定的消息队列
SendResult send(Message msg, MessageQueue mq, final long timeout)
//异步方式发送消息,且发送到指定的消息队列
void send(final Message msg, final MessageQueue mq, final SendCallback sendCallback, long timeout)
DefaultMQProducer的核心属性:
//生产者所属组,消息服务器在回查事务状态时会随机选择该组中任何一个生产者发起的事务回查请求
private String producerGroup;
//默认topicKey
private String createTopicKey = MixAll.DEFAULT_TOPIC;
//默认主题在每一个Broker队列的数量
private volatile int defaultTopicQueueNums = 4;
//发送消息的超时时间,默认为3s
private int sendMsgTimeout = 3000;
//消息体超过该值则启用压缩,默认为4KB
private int compressMsgBodyOverHowmuch = 1024 * 4;
//同步方式发送消息重试次数,默认为2,总共执行3次
private int retryTimesWhenSendFailed = 2;
//异步方式发送消息的重试次数,默认为2
private int retryTimesWhenSendAsyncFailed = 2;
//消息重试时选择另外一个Broker,是否不等待存储结果就返回,默认为false
private boolean retryAnotherBrokerWhenNotStoreOK = false;
//允许发送的最大消息长度,默认为4MB,最大值为2的32次方 - 1
private int maxMessageSize = 1024 * 1024 * 4;
开启所谓的故障延迟机制,即设置sendLatencyFaultEnable为ture,其实是一种较为悲观的做法。当消息发送者遇到一次消息发送失败后,就会悲观地认为Broker不可用,在接下来的一段时间内就不再向其发送消息,直接避开该Broker。而不开启延迟规避机制,就只会在本次消息发送的重试过程中规避该Broker,下一次消息发送还是会继
续尝试。
1、先获取Broker的网络地址。
2、在为消息分配全局唯一ID,如果消息体默认超过4KB,则对消息体采用zip压缩。
3、如果注册了消息发送钩子函数,则执行消息发送之前的增强逻辑。
4、构建消息发送请求包。主要包含如下重要信息:生产者组、主题名称、默认创建主题key、该主题在单个Broker上的默认队列数、队列ID、消息发送时间等等。
5、根据消息发送方式(同步、异步、单向)进行网络传输。
从存储模型来看,目前MQ中间件分为需要持久化和不需要持久化两种,大多数MQ都支持持久化存储,比如ActiveMQ、RabbitMQ、Kafka、RocketMQ。
RocketMQ存储的文件主要包括CommitLog文件、ConsumeQueue文件、Index文件。RocketMQ将所有主题的消息存储在同一个文件中,确保消息发送时按顺序写文件,尽最大的能力确保消息发送的高性能与高吞吐量。
因为消息中间件一般是基于消息主题的订阅机制,所以给按照消息主题检索消息带来了极大的不便。为了提高消息消费的效率,RocketMQ引入了ConsumeQueue消息消费队列文件,每个消息主题包含多个消息消费队列,每一个消息队列有一个消息文件。Index索引文件的设计理念是为了加速消息的检索性能,根据消息的属性从CommitLog文件中快速检索消息。
CommitLog:消息存储,所有消息主题的消息都存储在CommitLog文件中。
ConsumeQueue:消息消费队列,消息到达CommitLog文件后,将异步转发到ConsumeQuene文件中,供消息消费者消费。
Index:消息索引,主要存储消息key与offset的对应关系。
RocketMQ在消息写入过程中追求极致的磁盘顺序写,所有主题的消息全部写入一个文件,即CommitLog文件。所有消息按抵达顺序依次追加到CommitLog文件中,消息一旦写入,不支持修改。
CommitLog文件的命名方式:使用存储在该文件的第一条消息在整个CommitLog文件组中的偏移量来命名,例如第一个CommitLog文件为0000000000000000000,第二个CommitLog文件为00000000001073741824,依次类推。
这样做的好处是给出任意一个消息的物理偏移量,可以通过二分法进行查找,快速定位这个文件的位置,然后用消息物理偏移量减去所在文件的名称,得到的差值就是在该文件中的绝对地址。
CommitlLog文件的设计理念是追求极致的消息写,但我们知道消息消费模型是基于主题订阅机制的,即一个消费组是消费特定主题的消息。根据主题从CommitlLog文件中检索消息,这绝不是一个好主意,这样只能从文件的第一条消息逐条检索,其性能可想而知,为了解决基于topic的消息检索问题,RocketMQ引入了ConsumeQueue文件:
ConsumeQueue文件是消息消费队列文件,是CommitLog文件基于topic的索引文件,主要用于消费者根据topic消费消息。
RocketMQ与Kafka相比具有一个强大的优势,就是支持按消息属性检索消息,引入ConsumeQueue文件解决了基于topic查找消息的问题,但如果想基于消息的某一个属性进行查找,ConsumeQueue文件就无能为力了。故RocketMQ又引入了Index索引文件,实现基于文件的哈希索引。Index文件的存储结构:
虽然基于磁盘的顺序写消息可以极大提高I/O的写效率,但如果基于文件的存储采用常规的Java文件操作API,例如FileOutputStream等,将性能提升会很有限,故RocketMQ又引入了内存映射,将磁盘文件映射到内存中,以操作内存的方式操作磁盘,将性能又提升了一个档次。
在Java中可通过FileChannel的map方法创建内存映射文件。在Linux服务器中由该方法创建的文件使用的就是操作系统的页缓存(pagecache)。Linux操作系统中内存使用策略时会尽可能地利用机器的物理内存,并常驻内存中,即页缓存。在操作系统的内存不够的情况下,采用缓存置换算法,例如LRU将不常用的页缓存回收,即操作系统会自动管理这部分内存。
如果RocketMQ Broker进程异常退出,存储在页缓存中的数据并不会丢失,操作系统会定时将页缓存中的数据持久化到磁盘,实现数据安全可靠。不过如果是机器断电等异常情况,存储在页缓存中的数据也有可能丢失。
有了顺序写和内存映射的加持,RocketMQ的写入性能得到了极大的保证,但凡事都有利弊,引入了内存映射和页缓存机制,消息会先写入页缓存,此时消息并没有真正持久化到磁盘。
RocketMQ提供了两种策略:同步刷盘、异步刷盘。
RocketMQ的存储与读写是基于JDK NIO的内存映射机制的,消息存储时首先将消息追加到内存中,再根据配置的刷盘策略在不同时间刷盘。如果是同步刷盘,消息追加到内存后,将同步调用MappedByteBuffer的force()方法;如果是异步刷盘,在消息追加到内存后会立刻返回给消息发送端。
RocketMQ使用一个单独的线程按照某一个设定的频率执行刷盘操作。通过在broker配置文件中配置flushDiskType来设定刷盘方式,可选值为ASYNC_FLUSH(异步刷盘)、SYNC_FLUSH(同步刷盘),默认为异步刷盘。
因为RocketMQ操作CommitLog、ConsumeQueue文件是基于内存映射机制并在启动的时候会加载commitlog、consumequeue目录下的所有文件,所以为了避免内存与磁盘的浪费,不可能将消息永久存储在消息服务器上,这就需要引入一种机制来删除已过期的文件。RocketMQ顺序写CommitLog文件、ConsumeQueue文件,所有写操作全部落在最后一个CommitLog或ConsumeQueue文件上,之前的文件在下一个文件创建后
将不会再被更新。RocketMQ清除过期文件的方法:如果非当前写文件在一定时间间隔内没有再次更新,则认为是过期文件,可以被删除,RocketMQ不会关注这个文件上的消息是否全部被消费。默认每个文件的过期时间为72h,通过在broker配置文件中设置fileReservedTime来改变过期时间,单位为小时。
消息消费以组的模式开展,一个消费组可以包含多个消费者,每个消费组可以订阅多个主题,消费组之间有集群模式和广播模式两种消费模式。集群模式是当前主题下的同一条消息只允许被其中一个消费者消费。广播模式是当前主题下的同一条消息将被集群内的所有消费者消费一次。
RocketMQ支持局部顺序消息消费,也就是保证同一个消息队列上的消息按顺序消费。不支持消息全局顺序消费,如果要实现某一主题的全局顺序消息消费,可以将该主题的队列数设置为1,牺牲高可用性。
消息队列主要有三大用途:解耦、异步、削峰。
以电商系统的下单为例:
市场上几大消息队列对比:
RabbitMQ | ActiveMQ | RocketMQ | Kafka | |
---|---|---|---|---|
公司 | Rabbit | Apache | 阿里 | Apache |
语言 | Erlang | Java | Java | Scala&Java |
协议支持 | AMPQ | OpenWire、STOMP、REST、 XMPP、AMQP | 自定义 | 自定义协议,社区封装了http协议支持 |
客户端支持语言 | 官方支持Erlang、Java、Ruby等,社区产出多种API,几乎支持所有语言 | Java、C、C++、Python、PHP、Perl,.net 等 | Java、C++(不成熟) | 官方支持Java,社区产出多种API,如PHP,Python等 |
单击吞吐量 | 万级 | 万级 | 十万级 | 十万级 |
消息延迟 | 微秒级 | 毫秒级 | 毫秒级 | 毫秒以内 |
可用性 | 高,基于主从架构实现可用性 | 高,基于主从架构实现可用性 | 非常高,分布式架构 | 非常高,分布式架构,一个数据多副本 |
消息可靠性 | 有较低的概率丢失数据 | 经过参数优化配置,可以做到零丢失 | 经过参数配置,消息可以做到零丢失 | |
功能支持 | 基于erlang开发,所以并发性能极强,性能极好,延时低 | MQ领域的功能极其完备 | MQ功能较为完备,分布式扩展性好 | 功能较为简单,主要支持加单MQ功能 |
优势 | erlang语言开发,性能极好、延时很低,吞吐量万级、MQ功能完备,管理界面非常好,社区活跃;互联网公司使用较多 | 非常成熟,功能强大,在业内大量公司和项目中都有应用 | 接口简单易用,阿里出品有保障,吞吐量大,分布式扩展方便、社区比较活跃,支持大规模的Topic、支持复杂的业务场景,可以基于源码进行定制开发 | 超高吞吐量,ms级的时延,极高的可用性和可靠性,分布式扩展方便 |
劣势 | 吞吐量较低,erlang语音开发不容易进行定制开发,集群动态扩展麻烦 | 偶尔有较低概率丟失消息,社区活跃度不高 | 接口不是按照标准JMS(Java Message Service,Java消息服务应用程序接口,是一个Java平台中关于面向消息中间件的API,用于在两个应用程序之间,或分布式系统中发送消息,进行异步通信。Java消息服务是一个与具体平台无关的API)规范走的,有的系统迁移要修改大量的代码,技术有被抛弃的风险 | 有可能进行消息的重复消费 |
应用 | 都有使用 | 主要用于解耦和异步,较少用在大规模吞吐的场景中 | 用于大规模吞吐、复杂业务中 | 在大数据的实时计算和日志采集中被大规模使用,是业界的标准 |
总结:选择中间件的可以从这些维度来考虑:可靠性,性能,功能,可运维行,可拓展性,社区活跃度。目前常用的几个中间件,ActiveMQ作为“老古董”,市面上用的已经不多。
如果是面向用户的C端系统,具有一定的并发量,对性能也有比较高的要求,可以选择低延迟、吞吐量比较高,可用性比较好的RocketMQ。
RabbitMQ:基于erlang开发,对消息堆积的支持并不好,当大量消息积压的时候,会导致RabbitMQ的性能急剧下降。每秒钟可以处理几万到十几万条消息。
RocketMQ:基于Java开发,面向互联网集群化功能丰富,对在线业务的响应时延做了很多的优化,大多数情况下可以做到毫秒级的响应,每秒钟大概能处理几十万条消息。
Kafka:基于Scala开发,面向日志功能丰富,性能最高。当你的业务场景中,每秒钟消息数量没有那么多的时候,Kafka 的时延反而会比较高。所以,Kafka 不太适合在线业务场景。
ActiveMQ:基于Java开发,简单,稳定,性能不如前面三个。小型系统用也ok,但是不推荐。
消息队列有两种模型:队列模型和发布/订阅模型。
RocketMQ使用的消息模型是标准的发布-订阅模型,在RocketMQ的术语表中,生产者、消费者和主题,与发布-订阅模型中的概念是完全一样的。
RocketMQ本身的消息是由下面几部分组成:
消息消费模式有两种:Clustering(集群消费)和Broadcasting(广播消费)。
默认情况下就是集群消费,这种模式下一个消费者组共同消费一个主题的多个队列,一个队列只会被一个消费者消费,如果某个消费者挂掉,分组内其它消费者会接替挂掉的消费者继续消费。
而广播消费消息会发给消费者组中的每一个消费者进行消费。
RocketMQ一共有四个部分组成:NameServer、Broker、Producer、Consumer。
- 每个NameServer结点之间是相互独立,彼此没有任何信息交互。
- Nameserver被设计成几乎是无状态的,通过部署多个结点来标识自己是一个伪集群,Producer 在发送消息前从 NameServer 中获取 Topic 的路由信息也就是发往哪个 Broker,Consumer 也会定时从 NameServer 获取 Topic 的路由信息,Broker 在启动时会向 NameServer 注册,并定时进行心跳连接,且定时同步维护的 Topic 到 NameServer。
NameServer功能主要有两个:
1、和Broker 结点保持长连接。
2、维护 Topic 的路由信息。
- 同步发送:同步发送指消息发送方发出数据后会在收到接收方发回响应之后才发下一个数据包。一般用于重要通知消息,例如重要通知邮件、营销短信。
- 异步发送:异步发送指发送方发出数据后,不等接收方发回响应,接着发送下个数据包,一般用于可能链路耗时较长而对响应时间敏感的业务场景,例如用户视频上传后通知启动转码服务。
- 单向发送:单向发送是指只负责发送消息而不等待服务器回应且没有回调函数触发,适用于某些耗时非常短但对可靠性要求并不高的场景,例如日志收集。
消息可能会在这三个阶段发生丢失:生产阶段、存储阶段、消费阶段。所以要从这三个阶段考虑:
对分布式消息队列来说,同时做到确保一定投递和不重复投递是很难的,就是所谓的“有且仅有一次” 。RocketMQ择了确保一定投递,保证消息不丢失,但有可能造成消息重复。
处理消息重复问题,主要由业务端自己保证,主要的方式有两种:业务幂等和消息去重。
发生了消息积压,这时候就得想办法赶紧把积压的消息消费完,就得考虑提高消费能力,一般有两种办法:
消费者扩容:如果当前Topic的Message Queue的数量大于消费者数量,就可以对消费者进行扩容,增加消费者,来提高消费能力,尽快把积压的消息消费玩。
消息迁移Queue扩容:如果当前Topic的Message Queue的数量小于或者等于消费者数量,这种情况,再扩容消费者就没什么用,就得考虑扩容Message Queue。可以新建一个临时的Topic,临时的Topic多设置一些Message Queue,然后先用一些消费者把消费的数据丢到临时的Topic,因为不用业务处理,只是转发一下消息,还是很快的。接下来用扩容的消费者去消费新的Topic里的数据,消费完了之后,恢复原状。
顺序消息是指消息的消费顺序和产生顺序相同,在有些业务逻辑下,必须保证顺序,比如订单的生成、付款、发货,这个消息必须按顺序处理才行。
顺序消息分为全局顺序消息和部分顺序消息:
全局顺序消息:某个Topic下的所有消息都要保证顺序;
部分顺序消息:只要保证每一组消息被顺序消费即可,比如订单消息,只要保证同一个订单 ID 个消息能按顺序消费即可。
有两种方案:
一种是在 Broker 端按照 Consumer 的去重逻辑进行过滤,这样做的好处是避免了无用的消息传输到 Consumer 端,缺点是加重了 Broker 的负担,实现起来相对复杂。
另一种是在 Consumer 端过滤,比如按照消息设置的 tag 去重,这样的好处是实现起来简单,缺点是有大量无用的消息到达了 Consumer 端只能丢弃不处理。
一般采用Cosumer端过滤,如果希望提高吞吐量,可以采用Broker过滤。
对消息的过滤有三种方式:
1、根据Tag过滤:这是最常见的一种,用起来高效简单。
DefaultMQPushConsumer consumer = new DefaultMQPushConsumer("CID_EXAMPLE");
consumer.subscribe("TOPIC", "TAGA || TAGB || TAGC");
2、SQL 表达式过滤:SQL表达式过滤更加灵活。
DefaultMQPushConsumer consumer = new DefaultMQPushConsumer("please_rename_unique_group_name_4");
// 只有订阅的消息有这个属性a, a >=0 and a <= 3
consumer.subscribe("TopicTest", MessageSelector.bySql("a between 0 and 3");
consumer.registerMessageListener(new MessageListenerConcurrently() {
@Override
public ConsumeConcurrentlyStatus consumeMessage(List<MessageExt> msgs, ConsumeConcurrentlyContext context) {
return ConsumeConcurrentlyStatus.CONSUME_SUCCESS;
}
});
consumer.start();
3、Filter Server 方式:最灵活,也是最复杂的一种方式,允许用户自定义函数进行过滤。
电商的订单超时自动取消,就是一个典型的利用延时消息的例子,用户提交了一个订单,就可以发送一个延时消息,1h后去检查这个订单的状态,如果还是未付款就取消订单释放库存。
RocketMQ是支持延时消息的,只需要在生产消息的时候设置消息的延时级别:
// 实例化一个生产者来产生延时消息
DefaultMQProducer producer = new DefaultMQProducer("ExampleProducerGroup");
// 启动生产者
producer.start();
int totalMessagesToSend = 100;
for (int i = 0; i < totalMessagesToSend; i++) {
Message message = new Message("TestTopic", ("Hello scheduled message " + i).getBytes());
// 设置延时等级3,这个消息将在10s之后发送(现在只支持固定的几个时间,详看delayTimeLevel)
message.setDelayTimeLevel(3);
// 发送消息
producer.send(message);
}
目前RocketMQ支持的延时级别是有限的:
private String messageDelayLevel = "1s 5s 10s 30s 1m 2m 3m 4m 5m 6m 7m 8m 9m 10m 20m 30m 1h 2h";
半消息:是指暂时还不能被 Consumer 消费的消息,Producer 成功发送到 Broker 端的消息,但是此消息被标记为 “暂不可投递” 状态,只有等 Producer 端执行完本地事务后经过二次确认了之后,Consumer 才能消费此条消息。
依赖半消息,可以实现分布式消息事务,其中的关键在于二次确认以及消息回查:
死信队列用于处理无法被正常消费的消息,即死信消息。
当一条消息初次消费失败,消息队列 RocketMQ 会自动进行消息重试;达到最大重试次数后,若消费依然失败,则表明消费者在正常情况下无法正确地消费该消息,此时,消息队列 RocketMQ 不会立刻将消息丢弃,而是将其发送到该消费者对应的特殊队列中,该特殊队列称为死信队列。
死信消息的特点:
不会再被消费者正常消费。
有效期与正常消息相同,均为 3 天。3 天后会被自动删除。因此,需要在死信消息产生后的 3 天内及时处理。
死信队列的特点:
一个死信队列对应一个 Group ID, 而不是对应单个消费者实例。
如果一个 Group ID 未产生死信消息,消息队列 RocketMQ 不会为其创建相应的死信队列。
一个死信队列包含了对应 Group ID 产生的所有死信消息,不论该消息属于哪个 Topic。
RocketMQ 控制台提供对死信消息的查询、导出和重发的功能。
NameServer因为是无状态,且不相互通信的,所以只要集群部署就可以保证高可用。
RocketMQ的高可用主要是在体现在Broker的读和写的高可用,Broker的高可用是通过集群和主从实现的。
Broker可以配置两种角色:Master和Slave,Master角色的Broker支持读和写,Slave角色的Broker只支持读,Master会向Slave同步消息。也就是说Producer只能向Master角色的Broker写入消息,Cosumer可以从Master和Slave角色的Broker读取消息。
Consumer 的配置文件中,并不需要设置是从 Master 读还是从 Slave读,当 Master 不可用或者繁忙的时候, Consumer 的读请求会被自动切换到从 Slave。有了自动切换 Consumer 这种机制,当一个 Master 角色的机器出现故障后,Consumer 仍然可以从 Slave 读取消息,不影响 Consumer 读取消息,这就实现了读的高可用。
如何达到发送端写的高可用性呢?在创建 Topic 的时候,把 Topic 的多个Message Queue 创建在多个 Broker 组上(相同 Broker 名称,不同 brokerId机器组成 Broker 组),这样当 Broker 组的 Master 不可用后,其他组Master 仍然可用, Producer 仍然可以发送消息 RocketMQ 目前还不支持把Slave自动转成 Master ,如果机器资源不足,需要把 Slave 转成 Master ,则要手动停止 Slave 色的 Broker ,更改配置文件,用新的配置文件启动 Broker。
简单来说,RocketMQ是一个分布式消息队列,也就是消息队列+分布式系统。
作为消息队列,它是发-存-收的一个模型,对应的就是Producer、Broker、Cosumer;作为分布式系统,它要有服务端、客户端、注册中心,对应的就是Broker、Producer/Consumer、NameServer。
CAP理论,指的是在一个分布式系统中,Consistency(一致性)、Availability(可用性)、Partition Tolerance(分区容错性),不能同时成立。
Kafka采用Zookeeper作为注册中心——当然也开始逐渐去Zookeeper,RocketMQ不使用Zookeeper其实主要可能从这几方面来考虑:
RocketMQ主要的存储文件包括CommitLog文件、ConsumeQueue文件、Indexfile文件。
RocketMQ对文件的读写巧妙地利用了操作系统的一些高效文件读写方式——PageCache、顺序读写、零拷贝。
- 从磁盘复制数据到内核态内存;
- 从内核态内存复制到用户态内存;
- 然后从用户态内存复制到网络驱动的内核态内存;
- 最后是从网络驱动的内核态内存复制到网卡中进行传输。
可以通过零拷贝的方式,减少用户态与内核态的上下文切换和内存拷贝的次数,用来提升I/O的性能。零拷贝比较常见的实现方式是mmap,这种机制在Java中是通过MappedByteBuffer实现的。
RocketMQ提供了两种刷盘策略:
同步刷盘:在消息达到Broker的内存之后,必须刷到commitLog日志文件中才算成功,然后返回Producer数据已经发送成功。
异步刷盘:异步刷盘是指消息达到Broker内存后就返回Producer数据已经发送成功,会唤醒一个线程去将数据持久化到CommitLog日志文件中。
Broker 在消息的存取时直接操作的是内存(内存映射文件),这可以提供系统的吞吐量,但是无法避免机器掉电时数据丢失,所以需要持久化到磁盘中。
刷盘的最终实现都是使用NIO中的 MappedByteBuffer.force() 将映射区的数据写入到磁盘,如果是同步刷盘的话,在Broker把消息写到CommitLog映射区后,就会等待写入完成。
异步而言,只是唤醒对应的线程,不保证执行的时机:
RocketMQ和Kafka都是利用“长轮询”来实现拉模式。
长轮询,就是Consumer拉取消息,如果对应的Queue如果没有数据,Broker 不会立即返回,而是把PullReuqest hold起来,等待queue有了消息后,或者长轮询阻塞时间到了,再重新处理该queue上的所有PullRequest。
4.6版本默认48(默认是72,但是broker配置文件默认改成了48,所以新版本都是48)小时后会删除不再使用的CommitLog文件。
检查这个文件最后访问时间。
判断是否大于过期时间。
指定时间删除,默认凌晨4点。
RocketMQ是分布式消息服务,负载均衡是在生产和消费的客户端完成的。
推模式难以根据消费者的状态控制推送速率,适用于消息量不大、消费能力强要求实时性高的情况
。消息延迟
。毕竟是消费者去拉取消息,但是消费者怎么知道消息到了呢?所以它只能不断地拉取,但是又不能很频繁地请求,太频繁了就变成消费者在攻击 Broker 了。因此需要降低请求的频率,比如隔个2 秒请求一次,你看着消息就很有可能延迟 2 秒了。消息忙请求
,忙请求就是比如消息隔了几个小时才有,那么在几个小时之内消费者的请求都是无效的,在做无用功。 需要明确地提出消息中间件的几个重要角色,分别是生产者、消费者、Broker、注册中心。
简述下消息中间件数据流转过程:无非就是生产者生成消息,发送至Broker,Broker可以暂缓消息,然后消费者再从Broker获取消息,用于消费。
注册中心用于服务的发现包括:Broker的发现、生产者的发现、消费者的发现,当然还包括下线,可以说服务的高可用离不开注册中心。
然后开始简述实现要点,可以同通信讲起:各模块的通信可以基于Netty然后自定义协议来实现。注册中心可以利用zookeeper、consul、eureka、nacos等等,也可以像RocketMQ自己实现简单的nameserver。
为了考虑扩容和整体的性能,采用分布式的思想,像Kafka一样采取分区理念,一个Topic分为多个partition。并且为保证数据可靠性,采取多副本存储,即Leader和follower,根据性能和数据可靠的权衡提供异步和同步的刷盘存储。
并且利用选举算法保证Leader挂了之后Follower可以顶上,保证消息队列的高可用。
也同样为了提高消息队列的可靠性利用本地文件系统来存储消息,并且采用顺序写的方式来提高性能。也可以根据消息队列的特性利用内存映射、零拷贝进一步的提升性能。