作为一个高性能中间件,RocketMQ是如何保证高可用的?
RocketMQ的高可用设计
山中何事?松花酿酒,春水煎茶。
哈喽大家好啊,我是际遇,继续再跟大家聊一聊RocketMQ的高可用设计,咱们话不多说,直接开干吧!
我们通常说一个计算机系统的可用性,会用一个标准来衡量:平均无故障时间,系统的可用性越高,平均无故障时间就越长。
RocketMQ作为一个消息中间件,高可用当然也是它的一个重要且必须要保证的特性。
我们通过四个方面来分析一下RocketMQ的高可用设计
消息发送的高可用
在发送消息时,我们可能会遇到网络问题,Broker宕机等情况,但是NameServer检测Broker是有延迟的,NameServer会每隔10秒扫描一下Broker,那这么说的话,10秒也不算是很慢啊,10秒钟扫一下,有问题的话,及时更新,还不算是很难接受吧。
咱们这么想当然没有问题!
但关键在于,NameServer对Broker进行扫描的时候,需要保证Broker的最后心跳已经过了有120秒的情况下,才会认为该Broker下线!
所以,这个时间就从10秒变成120秒,所以导致Producer不能马上感知到Producer下线了,还在苦苦等消息。
那为什么不设计成及时检查和及时通知呢?
因为这样会让整个NamerServer的通信系统变得复杂,之前我们也说过,NameServer的设计就是尽可能的简单,所以呢,高可用这块儿就交给Producer端来实现了。
其中最重要的两个设计:
- 重试机制
- 故障延迟机制
消息发送重试机制
重试机制比较简单,就是在消息发送出现异常时会尝试再次发送,默认最多会重试三次。
注意了,重试机制只支持同步发送,不支持异步和单向发送方式。
还有就是根据发送失败的异常处理策略略有不同,如果是网络异常(RemotingException)和客户端异常(MQClientException)会重试,而Broker服务端异常(MQBrokerException)和线程终端异常(InterruptedException)则不会再重试,直接抛出异常。
这部分我们就不过多叙述了,然后我们可以在源码中发现下面的代码:
故障规避机制
之前我们在说NameServer的时候说过,NameServer为了简化和客户端的通信,发现Broker故障并不会立马通知客户端。
那么故障规避的机制就是用来解决这个问题的,这个机制默认是不开启的,如果在开启的情况下,消息发送失败的时候会将失败的Broker暂时排除在队列的选择外。
另外,规避的时间也是衰减的,但是如果Broker一直不可用,那么就会被NameServer检测到并在Producer更新路由信息的时候进行剔除!
这个也比较好理解,就好比你给你女朋友打电话,她那边提示无法接通,那你肯定过一会儿再打呀,如果她在乎你,过一个小时就打得通了呀,对吧,如果十来年了还没打通,那肯定就表示你被拉黑了呀(下线!)
在查找路由的时候,选择路由也是有一些关键的步骤:
-
先按照轮询算法选择一个消息队列
-
从故障列表中判断该消息队列是否可用
- 判断其是否在故障列表中,不在故障列表中则代表可用
- 在故障列表中的话,还需要判断当前时间是否大于等于故障规避的开始时间。
这部分的重点其实在于在什么条件下会进入故障列表,这个问题RocketMQ有一套自己的标准,前提是,故障规避机制需要打开,否则,一切都白说。
或许这里有人会说,既然你都说到这了,那你再详细描述一下呗。
对此我的回答是:知道那么多干嘛?你要考研啊?
消息存储的高可用
其实这个顺序相信大家也是能明白,从消息中间件的整个工作流程,我们来逐个分析,那么下个内容大家也能够想得到了,那必然是消息接收了。
在RocketMQ中消息存储的高可用体现在发送成功的消息不能丢,Broker不能发生单点故障,出现Broker异常宕机,操作系统Crash,机房断电或者断网的情况下数据不丢失,当然,什么小行星撞击地球,三体人降临这种就不能纳入考虑范围了哈。
RocketMQ主要是通过下面三个设计来保证消息存储的高可用
- 消息持久化(刷盘)
- 主从复制
- 读写分离机制
同步刷盘和异步刷盘
此刷盘非彼刷盘哈,家庭煮夫别来凑热闹。
这里说的是指消息数据发送到Broker之后,写入磁盘中做持久化,保障在Broker出现故障重启时,数据不会丢失。众所周知哈,内存断电是会丢数据的,但是硬盘是不会的。
RocketMQ也贴心的提供了两种刷盘的机制:同步刷盘和异步刷盘。
同步刷盘
在同步刷盘的模式下,当消息写入到内存后,会等待数据写入到磁盘的CommitLog文件(不记得的翻一番上一篇文章哈),提交刷盘任务后,会在刷盘队列中等待刷盘,而刷盘线程每隔10毫秒写一批数据到磁盘。
那为什么不直接全部写呢?
主要原因还是磁盘压力过大,写入的性能低,每隔10毫秒可以提升刷盘的性能。
异步刷盘
RocketMQ默认是采用的异步刷盘,异步刷盘有两种策略:开启缓冲池和不开启缓冲池,默认是不开启的。
咱们可以稍微了解一下,这玩意了解一下就行了,看多了对身体不好。
- 不开启缓冲池:刷盘线程会每间隔500毫秒尝试去刷盘,这间隔500毫秒仅仅是尝试,实际去刷盘还得满足一些前提条件的,比如距离上一次刷盘时间超过了10秒,或者写入内存的数据超过了4页(16KB),这样即使服务器挂了,丢失的数据也是在10秒内的,或者大小是在16KB以内的,减小损失,止损。
- 开启缓冲池:RocketMQ会申请一块儿和CommitLog文件相同大小的堆外内存用来做缓冲池,数据会先写入缓冲池,提交线程也是每500毫秒尝试提交到文件通道等待刷盘,这个和不开启缓冲池的处理方法一样。使用缓冲池的目的是将多条消息合并写入,提高I/O性能。
了解到这里已经够了,我查资料查到这里都查吐了,你还没够啊?
主从复制
单点故障我们之前也提到过,如果发生了单调故障导致存储在Broker上的消息无法及时消费,或者出现数据损失,RocketMQ采用Broker主从复制,当消息发送到Master服务器后会将消息同步到Slave服务器,如果Master挂了,消费者还可以继续从Slave拉取消息。
主从复制,也就是从Master服务器复制到Slave服务器,RocketMQ也提供有两种服务方式,怎么样,贴心吧。
我们可以通过/conf/broker.conf里的brokerRole参数进行设置。
- 同步复制:Master服务器和Slave服务器都成功写入之后才返回给客户端写成功的状态,优点是如果Master服务器出现故障,Slave服务器上有全部的数据备份,很容易恢复到Master服务器。缺点也很明显,同步写入会增加耗时,增加数据写入的延迟,降低系统的吞入量。
- 异步复制:仅仅需要Master写入成功就可以返回成功的状态。优点就是同步复制的缺点了呗,减少数据写入延迟,增加吞吐量。缺点就是如果Master挂了,数据没有写入Slave,那么没有同步的数据就会丢失。
从上面我们可以看出,同步和异步是在复制数据给Slave的数据的时候的同步异步,并不是一开始写入Master的时候就是同步异步。
读写分离:
读写分离也是高性能,高可用架构中常见的设计,例如MySQL对吧,Client只能从Master服务器写数据,但是可以从Master和Slave读数据。
RocketMQ的Consumer在拉取消息时,Broker会判断Master服务器的消息堆积量以决定Consumer是否从Slave服务器拉取消息消费。默认一开始从Master上拉取消息,如果Master的消息堆积超过了物理内存的40%,则会在Consumer的消息结果里告知Consumer下次需要从Slave服务器拉取。
消息消费高可用
前面消息发送,消息存储都说完了,就到了消息消费了。
在实际业务场景中消息消费失败的情况是无法避免的,消费失败也有多种原因,某些情况下,消费失败不代表消息一定不能被消费,比如网络原因对吧,网络卡了导致我消费失败了,那这种情况是可以通过重复消费来解决的。
话赶话都说到这了,那接下来就说说RocketMQ的重试队列和死信队列:
消费重试机制
- 重试队列:重试队列的消息存在单独的一个Topic中,不在原消息的Topic中,Consumer会自动订阅该Topic。每个业务Topic都会有多个ConsumerGroup,每个ConsumerGroup消费失败的情况都不一样,所以各对应一个重试队列的Topic。
- 死信队列:由于业务逻辑BUG等等原因,导致一直重试失败,为了保证这个消息不丢失,同时也不能阻塞其他能重试消费成功的消息,超过了最大重试次数的消息将会进入死信队列,这种情况下就不要想着还怎么消费了,需要人工干预了。(死信队列当然也是存在一个单独的Topic中,和ConsumerGroup的关系也和重试队列一致)
这里说一下,RocketMQ的重试并不是固定时间间隔的重试,每次重试的时间都会加长,第一次10秒,第二次30秒,以此类推,直到次错超过最大重试次数(默认16次)。
还有就是,RocketMQ也支持定时消息,也叫做延迟消息,在异步的场景下比较适用,比如调用某个异步服务,需要在一分钟内返回结果,此时就可以发送一个1分钟的延迟消息,等一分钟后收到该消息再去查询调用的结果。
Tips:RocketMQ不支持任意时间精确的延迟消息,只有:1s,5s,10s...(剩下的自己去查哈,乖!)
ACK机制
TCP/IP协议的ACK相信大家都了解,没错,这个也是类似的消息确认机制。
广播模式的消费进度是保存在客户端本地的,而集群模式下的消费进度是保存在Broker上的。集群模式中,RocketMQ就使用了ACK机制来确保消息一定被消费。
在消息投递的过程中,不是消息从Broker发送到Consumer就算是消息消费成功,这仅仅是刚开始而已,我们需要Consumer明确给Broker返回消费成功的状态,这才算消费成功。
但是有一种情况,Broker发送了消息,Consumer接收到了,消费完成之后网络中断了,或者消费者挂了,那么Broker未收到反馈,那这种情况下,在Consumer重启之后,消息会重新投递,会出现重复消费的情况,那这种情况下的消息幂等性就需要Consumer自行保证了。
Broker集群部署
Broker集群是消息存储高可用的保障,如果集群都没有,那么消息存储的高可用也没有的基本的保障。
这里我们也简单说一下Broker集群的搭建方式吧,不同的搭建方式有不同的优缺点:
-
单Master模式:仅仅部署一台Broker服务器作为Master,当然这种情况下就是非集群的方式了。至于缺点,就不用再细说了,单点故障的风险会导致整个服务不可用。
-
多Master模式:一个集群全是Master机器,没有Slave机器,属于不配置主从复制的场景,不建议线上使用。
- 优点:配置简单,单个Master宕机或者重启对服务没有影响。
- 缺点:单台机器宕机期间,这台机器上未被消费的消息在机器恢复之前不可订阅,消息实时性收到影响,违背了高可用的原则。
-
异步复制的多Master多Slave:每个Master配置一个Slave,有多对Master-Slave,主从复制采用异步复制的方式。
- 优点:即使磁盘损坏,消息丢失少,实时性不受影响。
- 缺点:Master宕机且磁盘损坏的情况下,会丢失消息,概率很低。
-
同步复制的多Master多Slave:每个Master配置一个Slave,有多对Master-Slave,主从复制采用同步复制的方式。
- 优点:消息无延迟,数据可用性和服务可用性都非常高
- 缺点:性能比异步复制略低(10%左右),发送单个消息的RT(响应时间)会略高。
后记
终于!RocketMQ的部分(甚至是从Spring Cloud Stream开始的)就到此结束了。
完结撒花!
这期间从工作变动,然后熟悉新的东家,再到现在也拖了很长时间了,大概有一年半了。
接下来写点什么呢?(暗示评论留言)
喂,不是吧,动辄三千多大字,好歹给个赞呗!
转载自:https://juejin.cn/post/7294171458031960074