第三篇:消息队列如何保证消息可靠性?

消息队列如何保证消息可靠性?

上一篇我们聊了《消息为什么能够解耦系统?》

消息队列之所以能够解耦,本质上是因为它在生产者和消费者之间增加了一个Broker,让双方不再直接调用,而是通过消息进行通信。

但是,新的问题也来了。

以前:

订单服务

↓

短信服务

订单服务调用成功,就说明短信服务已经收到了请求。

现在:

订单服务

↓

消息队列

↓

短信服务

消息需要先发送到 Broker,再由消费者处理,中间多了很多环节,如果其中某个环节出问题,消息会不会丢?消息到底会在哪里丢?

很多人认为,消息丢失只有一种情况:Broker 宕机,其实,一条消息从发送到消费,要经历完整的一条链路。

Producer

↓

发送消息

↓

Broker

↓

持久化

↓

Consumer

↓

业务处理

↓

消费确认

每一个阶段,都有可能发生异常。

例如:

生产者发送时网络断开;Broker 收到消息,还没来得及写入磁盘就宕机;消费者处理成功,但是确认消息失败。所以讨论消息可靠性,不能只看 Broker。而是要看整条消息链路。

第一关:生产者如何知道消息发送成功?

来看一段最简单的代码。

kafkaTemplate.send("order-topic", order);

或者:

rocketMQTemplate.syncSend("order-topic", order);

看到这里会觉得:send() 方法执行结束,消息应该已经发送成功了。其实并不一定。

例如:

Producer

↓

发送消息

↓

网络异常

这时候生产者根本不知道:消息到底有没有到达 Broker。如果直接认为发送成功,就可能造成消息丢失。所以几乎所有 MQ 都会设计:发送确认(ACK)机制。只有 Broker 明确告诉生产者:我已经收到消息。生产者才认为发送成功。

如果没有收到确认:

Producer

↓

超时

↓

重新发送

这也是为什么很多 MQ 客户端都支持:

同步发送

异步发送

重试机制

因为第一步,就要尽可能保证:消息能够成功进入 Broker。

第二关:Broker 收到消息,就安全吗?

很多人会回答当然,Broker 都已经收到消息了。

其实依然不安全。

来看下面这个场景:

Producer

↓

Broker(内存)

↓

服务器突然断电

如果消息只是放在内存里,服务器一旦宕机,消息依然会消失,所以 Broker 收到消息以后,并不会立即返回成功。

真正重要的是:消息有没有成功持久化。这里不同 MQ 实现略有区别。

例如 Kafka,会把消息顺序追加到日志文件,RocketMQ,则会先写入 CommitLog。虽然实现不同,但目标一致:尽快把消息保存到磁盘。

为什么不是每收到一条消息就立刻刷盘?

很多人第一次看到这里,会想到:既然刷盘最安全。为什么不每条消息都立即刷盘?

原因和 Redis 很像,上一篇 Redis 我们讲过:每次修改都刷盘,性能会急剧下降,MQ 也是一样。

假设:

收到消息

↓

立即刷盘

↓

返回成功

一次消息,就对应一次磁盘 IO。高并发下,Broker 很快就成为瓶颈。所以很多 MQ 都提供两种策略:

同步刷盘:

收到消息

↓

写磁盘

↓

返回 ACK

优点:可靠

缺点:延迟更高

异步刷盘:

收到消息

↓

先返回 ACK

↓

后台统一刷盘

优点:吞吐量更高

缺点:如果服务器在刷盘之前宕机,可能丢失少量消息。

看到这里是不是很熟悉?Redis 的 AOF,也是类似的设计。本质上都是可靠性和性能之间的权衡。

第三关:Broker 宕机怎么办?

假设:消息已经成功写入 Broker,结果下一秒:

Broker

↓

宕机

如果整个集群只有这一台 Broker,消息依然会丢。所以真正的 MQ,都会设计:

副本机制。

例如:

Producer

↓

Leader Broker

├─────────┐

│ │

▼ ▼

Follower Follower

Leader 保存消息,Follower 同步数据,当 Leader 宕机时,其他副本仍然可以继续提供服务。

Kafka 有 ISR 副本机制。RocketMQ 也支持主从复制。虽然实现不同,但都是为了提高可靠性。

第四关:消费者收到消息,就结束了吗?

还没有。来看下面这个流程:

Consumer

↓

收到消息

↓

更新数据库

↓

程序崩溃

数据库已经更新成功。但是消费者还没有告诉 Broker:这条消息我已经处理完了。

Broker 会认为:这条消息还没有消费。于是:重新投递同一条消息。再次消费。

这就是为什么:可靠性和重复消费,往往是一起出现的。为了避免消息丢失。

MQ 宁愿:多投一次,也不会少投一次。所以大多数 MQ 保证的是:至少投递一次(At Least Once),至于重复消费的问题,我们下一篇专门讨论。

为什么不存在绝对可靠的 MQ?

看到这里,很多人可能会问有没有一种 MQ:既不会丢消息,又没有重复消费,还能保持高性能。

答案是:几乎没有。原因很简单,如果:每一条消息:

发送

↓

同步刷盘

↓

等待所有副本同步

↓

等待消费者确认

↓

返回成功

那么可靠性确实最高。但是性能一定下降。

如果:

收到消息

↓

立即返回

性能很高。

但是:极端情况下,又可能丢失数据。所以 MQ 的设计,从来不是追求绝对可靠。

而是在:可靠性、性能、延迟之间寻找平衡。不同 MQ,只是选择了不同的侧重点。

总结:消息队列如何保证消息可靠性?

不是依靠某一个功能,而是整条消息链路共同完成。

Producer

↓

发送确认

↓

Broker 持久化

↓

副本同步

↓

Consumer 消费

↓

消费确认

每一个环节,都有对应的保护机制,也正因为如此。MQ 并不能保证消息绝对不会丢。

它真正做到的是:即使某个环节发生故障,也尽可能发现问题、恢复消息,并降低消息丢失的概率。这也是为什么:可靠性越高,系统需要付出的性能成本也越高。

下一篇,我们继续讨论另一个很多人都遇到过的问题:

为什么消息会重复消费?

上一篇:《消息为什么能够解耦系统?》

下一篇:《消息为什么会重复消费?》

相关推荐
孤狼GPT6 小时前
ChatGPT、Codex排查实录:Kafka消费者日志明明显示处理成功,为什么同一条消息还是被重复消费?
kafka·springboot·codex·chatgptplus·chatgptpro
jyOverQ6 小时前
从零开始学 RocketMQ(四):集群部署、消息存储与高可用【保姆级教程】
java·rocketmq·java-rocketmq
starzy19909 小时前
KafkaProducer 源码深度剖析:batch 发送的条件
大数据·kafka
SL-staff1 天前
JVS私有化部署运维避坑指南:Kafka容灾、ES冷热分离、RabbitMQ可观测性三实战检查项
elasticsearch·kafka·jvs运维·es冷热分离·rabbitmq监控·中间件巡检·sre实践
starzy19901 天前
KafkaProducer 源码深度剖析:支撑高吞吐的数据结构设计
大数据·数据结构·kafka
starzy19902 天前
KafkaProducer 源码深度剖析:RecordAccumulator 封装消息流程
大数据·kafka
starzy19903 天前
KafkaProducer 加载元数据:一条 MetadataRequest 的完整旅程
大数据·kafka
RobinXYuan3 天前
【Kafka 源码手记】04 客户端如何找到 Broker
kafka·源码阅读
Linux实战笔记3 天前
记一次消息队列选型对比线上排查:Kafka/RabbitMQ/Pulsar 技术选型
kafka·rabbitmq·高并发·pulsar·消息队列选型