暂无图片
暂无图片
暂无图片
暂无图片
暂无图片

面试官:RocketMQ的高可靠是怎么设计与实现的?

编码师兄 2022-04-15
757

大家好,这里是技术头条,同时活跃开源社区及王者荣耀的大厂架构师,今天给大家分享一下RocketMQ的高可靠机制。全文阅读大约15分钟,技术原创不易,希望大家多点赞、收藏。

RocketMQ 的高可靠机制是我在面试候选人时最常问的问题之一,也是实际工作中经常用到的知识点,所以我们今天花点时间探究一下 RocketMQ 是如何保证消息可靠性的。

从业务上讲,一个大型的互联网系统,消息的可靠性比性能更为重要,一般情况下,我们都指望消息中间件承担消息削峰或保障最终一致性的职责,如果消息到了中间件后并没有一个高可靠的保障,那么从业务方的角度看来就是消息丢失,这样一来,你前期进行的所有架构设计都是徒然。

所以,无论是从夯实功力、从容应对面试的角度出发,还是从实际业务的角度出发,了解“RocketMQ 怎么保证消息可靠性”对你来讲都尤为必要。

消息中间件需要从哪些环节实现高可靠?

开始讲 RocektMQ 怎么做高可靠之前,我们先思考一个问题:如果让你去设计一个消息高可靠的系统,都需要准备什么?你会面临哪些挑战?

要知道,一个消息从消息生产到消息消费,大体会经历三个环节,在这三个环节中会遇到各种各样的异常,比如——

  • 消息发送到消息中间件:可能因为发送到消息中间件的过程中遇到网络故障、broker 不可用等问题导致消息发送失败。

  • 消息中间件做好存储等工作:可能是消息并没有完成刷盘持久化的工作,就宕机了或者磁盘坏了,导致消息丢失。

  • 消息中间件把消息投递给消费者:可能是消息投递给消费者成功后,但是实际上没有接收到消费响应,broker 就已经把消息删除了。

这些方方面面的异常情况数不胜数,每个小问题都可能破坏我们对消息中间件消息可靠的承诺,

那么要做到一个消息的高可靠,从我的经验出发,要从三个环节入手,如果能做到以下三点,就认为消息是可靠的。

  • 保证发送的高可靠:如果生产者发送消息到了消息中间件,消息中间件告诉生产者成功,就可以认为消息的存储和消息的消费都是可靠的,即后续的高可靠的工作无需生产者再介入。

  • 保证消息本身存储的高可靠:如果消息存储下来了,那么 broker 能保证消息在消费成功之前,不会因为任何异常而导致消息丢失。

  • 保证消息消费的高可靠:消费者可能因为各种异常导致没有消费或者消费没成功,broker 能保证消息总能至少消费一次。

以上三个环节,我们先来讲头尾两个环节,消息的生产、消息的消费。

消息生产的高可靠

要做到消息的可靠,你得先可靠地把消息投递到消息中间件。听起来好像很容易,不就是调一个API 吗?这样说既对也不对,虽然它的确是一个 API 的调用,但这中间存在很多问题。

我们来看一个实际例子,假设你现在正在处理一个支付的消息,这个支付消息消费成功后,需要发送一个支付成功的事件,好让积分系统增加用户的积分。你的代码可能是这样的:

update t_pay_order set order_status='SUCCESS' where order_no='12345678';
sendMQMessage(myMsg);

这里可能会存在几个常见的问题:

  • 如果我们数据库执行成功了,这时候服务重启了;

  • 服务正常,但是发送消息到 broker 时,那一台 broker 挂了;

  • 服务正常,但是发送消息到 broker 时,消息超时、或者网络出现了异常无法发送。

针对第一个问题,常见的解决思路是:用消息表去存储待发送的消息,直到消息发送成功后才删除/更新消息表数据(RocketMQ 还提供了很强大的事务消息功能去解决这个场景)。

而第二、三个问题,实际上都是在说“发送消息的指令已经发出去了,但是由于各种异常没有得到一个明确的成功响应”。针对这类问题,我们首先要意识到:没有收到明确成功的响应,都不能认为消息发送成功,解决办法就是重试。当然,重试意味着同样的消息可能被重复发送,但你有去重的手段去处理消息重复,而消息丢失就没办法了。

不知道你有没有留心过类似 retryTimesWhenSendFailed 的生产者参数,实际上 RocketMQ 的生产者客户端在内置上已经帮我们做了失败重试的操作了,如果对应用来说还是得到了一个失败、超时等的响应,这说明 RocketMQ 内置的重试操作也无法“拯救”这个失败。这时就需要应用自行处理了,常见的解决思路有

  • 保存到数据库后延迟再重试;

  • 开启一个异步线程不断重试等。

总而言之一句话:对应用来说,要做到消息的高可靠,就是想尽方法得到一个成功的响应

消息消费的高可靠

对于消息消费的高可靠,消息中间件要做的就是:没有得到消费者的 ack,这条消息就不会认为是消费成功的。

这样一来,只要消费者在线,这条消息还能被继续投递。只不过,这个继续投递在 RocketMQ的机制里,还带有延迟消费的特性,这是由于很多情况下消费失败都是外部资源的获取失败导致的。比如数据库访问失败了、Redis 失败了、下游系统报错了……这类错误通常都需要一段时间才能恢复,所以 RocketMQ 在重新投递下一个消息时,会带上一个延迟级别,以便给予这些外部资源恢复的时间。

虽然 RocketMQ 已经对消息消费的高可靠做了很好的支持,但我依旧能看到很多人为原因导致的“消息丢失”,最常见的就是很多同学采取这样的消费模板:

consumer.registerMessageListener(new MessageListenerConcurrently() {
@Override
public ConsumeConcurrentlyStatus consumeMessage(List<MessageExt> msgs,ConsumeConcurrentlyContext context) {
try{
。。。。。//处理消息
return ConsumeConcurrentlyStatus.CONSUME_SUCCESS;//返回消息消费状态
} catch (Exeption ex) {
return ConsumeConcurrentlyStatus.CONSUME_SUCCESS;
}
}
});

这类开发模板很可能是从类似 RabbitMQ 消息中间件迁移到 RocketMQ 时顺手带过来的。

在 RabbitMQ 的消费代码里,我们需要手动调用 ack 方法的,否则消息会处于 unack 状态,达到一定程度,消费者就无法再消费消息了。所以,开发者可能会 catch 住异常,也把消息给 ack 掉。

实际上,RabbitMQ 的这种消费方式并不好,相当于消息消费失败了但是直接当作成功处理。在 RabbitMQ 的场景下,要很好地处理这类错误,我们的确需要 catch 异常,但是针对异常的消息是要做更多的错误处理,比如把消息暂存到某个地方再后面消费等。

而 RocketMQ 把错误处理内置到了 API 级别中,也就是说,RocketMQ 在各种消费失败的场景都能贴心地帮你做到重新消费,但是前提是你能正确地告诉 RocketMQ,这条消息消费失败了。

所以上面的 catch 代码后直接 return
ConsumeConcurrentlyStatus.CONSUME_SUCCESS 的处理就变成画蛇添足了。实际上,我们仍由这个异常往外抛出即可,当然了,如果你觉得这个异常不加处理就抛出不是很好,也可以 catch 住这个异常返回 ConsumeConcurrentlyStatus.RECONSUME_LATER,效果也是一样的。

以上就是消息生产、消费的高可靠,接下来我们聊一聊 RocketMQ broker 在存储方面要做什么样的高可靠工作。

消息刷盘策略

你应该知道,RocketMQ 是由 commitLog 文件去存储消息的。也就是说,万一 broker 所在的机器发生重启之类的故障,实际上消息因为被持久化过,是不会丢失的。

但是消息的持久化实际上是依赖操作系统的。RocketMQ 写消息是依赖文件映射的方式去操作,这时写入消息成功实际上不能百分百保证消息被刷到了磁盘,只能保证消息被写到了操作系统的 page cache 中。

这时候,RocketMQ 提供了两个刷盘策略:SYNC_FLUSH(同步刷盘)、ASYNC_FLUSH(异步刷盘)。

  • 同步刷盘:消息写入成功后(到 page cache 后),会立刻触发操作系统的 fsync 操作,把消息刷到磁盘中,这时候表现上就是消息发送需要等到真正刷到磁盘才会返回;如果这时,生产者得到一个成功的响应,生产者可以认为该消息被持久化成功了。

  • 异步刷盘(写完 page cache 就结束了):也就是说,这时生产者得到的成功响应,消息是“大概率会持久化”,并不能百分百保证消息一定被持久化成功;换一句话说,如果这时 broker 发生重启,有机会造成消息丢失,那何时才刷盘?一来是由操作系统决定,二来 RocketMQ 也支持在异步刷盘的情况下,隔一段时间就强行触发一次刷盘。

刷盘策略是影响消息可靠性的重要一环,一般情况下,选择异步刷盘能有更好的性能表现

消息同步策略

解决了消息的持久化问题,也就解决 broker 单节点消息可靠性问题。但在一个大型分布式系统里,这还远远不够,我们希望去除所有节点的单点问题。也就是说,假设 broker 的磁盘都坏了,我们还能不能维持 RocketMQ 的消息高可靠?

答案是可以的。

RocketMQ 支持主从的架构部署,也就是说,一个 master 下可以挂多个 slave。master 和 slave 的数据完全一致,只是角色表现上有所区别:只有 master 允许处理写入请求;slave 只能同步 master 数据或者处理读请求。

在讲消息的读写高可用之前,我们先了解一下这个消息的同步策略。

在消息写入 master 之后,slave 会不断地往 master 同步数据,这时 RocketMQ 也提供了两个同步策略。

  • ASYNC_MASTER:意味着这个 master 启用的是异步复制策略。

  • SYNC_MASTER:意味着 master 启动的是同步双写策略。

异步复制是指消息写入 master 之后,就算成功,slave 自己会不断地同步 master 数据。注意,slave 同步数据不是以消息为维度的,而是以 commitLog 文件同步的方式去顺序同步的,也就是说如果 slave 落后很多的话,slave 没有同步完成前面的消息是不会同步这个最新的消息的。

而同步双写与异步复制的区别在于,消息写入 master 之后,写入的线程会等待 slave 的数据同步。由于 slave 同步的过程中会上报自己的同步的最大进度(消息文件里面的物理offset),当发现至少有一个 slave 的进度达到了和 master 一致,这条写入的线程才会返回。

所以在同步双写策略之下,如果生产者得到一个成功的响应,这意味着消息至少在两个 broker 上存在了。当然了,这里的存在也只能保证写入到了 page cache,是否肯定能持久化到磁盘,还取决于刚刚我们提到的刷盘策略。

一般而言,我们会建议采取 SYNC_MASTER+ASYNC_FLUSH 的方式,在消息的可靠性和性能间有一个较好的平衡。

消息的读写高可用

做到了消息的多副本后,理论上我们就具备了读的高可用。因为如果 master 不可用了,slave是可以继续提供读服务的。所以一旦消息发送成功了,即便出现 master 的单点故障,slave 也能可靠地把消息投递到消费者之间,从而完成 RocketMQ 消息高可靠的任务。

回答在这里,实际上一个 RocketMQ 是如何完成消息的高可靠这个问题已经回答完毕了,如果这是一个面试题,你可以简单地这样回答——

  1. 首先 RocketMQ 生产者端会有消息重试机制,保证出现如 broker 单点故障或者网络异常等情况去做重试。当我们接收到 broker 端成功的响应为止,我们可以认为消息存储和消息的投递都是高可靠的。

  2. 其次,RocketMQ 提供了主从的策略,我们可以部署 slave 去同步数据以避免单点的问题。同时,RocketMQ 还给予了刷盘策略和同步策略中同步、异步两种方式让我们在性能和可靠性上自行抉择。

  3. 最后,在消息消费上,RocketMQ 会提供 at least once 的消费模式,除非我们主动的告诉 broker 消费是成功的,否则 RocketMQ 不会放弃投递这个消息(进入死信主题除外)。

多副本的Deledger

上述我们讲到了RocketMQ 是如何实现一个消息的高可靠的。来到这里,或许你会问,如果 master 挂了,消息的读服务是可以延续了,那写服务呢?

这是一个好问题,在很长时间里,这个问题 RocketMQ 是这样解决的。

  1. master 和 slave 不会有主备切换的能力,这意味着如果 master 挂了,那么 slave 不会自动升级为主,只会继续维持 slave 这个角色,接管读服务。

  2. 如何解决写的高可用呢?答案是部署多个 master。RocketMQ 允许多个不同的 broker 共同形成一组而对外提供读写服务。意味着你同样的 topic 在 broker-1 和 broker-2 都存在,那么它们都可以对外提供读写服务,如果 broker-1 的 master 挂了,那么生产者是会自动切换到 broker-2 中的,所以写服务自然也是可以做到高可用的。

在 4.5.0 之后,RocketMQ 提供了一个新的多副本架构,这套实现称为 Dledger。新的多副本架构本质上可以说就是解决一个问题:自动选主的问题。

而 DLedger 其实是一个基于 raft 协议的 commitlog 存储库。如果你选择了使用 Dledger 模式,DLedger commitLog 就会代替了原来的 commitLog,使得 commitLog 拥有选举复制能力。

关于 DLedger 的具体实现,我们不详细展开,有兴趣的读者可以参考阿里云发表的《DLedger —基于 raft 协议的 commitlog 存储库》这篇文章。

小结

最后我们针对本讲做一个简单的总结:

  1. 要做到消息的高可靠,RocketMQ 从消息生产到消息存储和消息副本,再到消息消费三个环节都做出了不同程度的设计

  2. 在消息生产的环节,主要的设计工作是消息重试。开发者需要自己处理 RocketMQ 内置重试后仍然失败的场景。

  3. 消息存储上 RocketMQ 会把消息做持久化和多副本存储,这两个设计都支持同步和异步两个策略。

  4. 消息消费的环节上,RocketMQ 支持 at least once 消费模式,同时内置消息消费失败重投递的设计,但是开发者需要谨慎处理异常消费的场景,避免吞掉了异常。

最后是给大家一道思考题目:RocketMQ 强同步策略采取是同步双写。你能对比一下其他消息中间件或者数据库等产品,说说哪些是采取类似的策略,又有哪些是采取不一样策略的?可以分享你的想法到评论区中。

本期内容到此结束,这里是技术头条,每天带你了解进大厂必须掌握的编程技术和架构设计,关注起来,进大厂不再是梦想。


文章转载自编码师兄,如果涉嫌侵权,请发送邮件至:contact@modb.pro进行举报,并提供相关证据,一经查实,墨天轮将立刻删除相关内容。

评论