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

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

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

消息队列之所以能够解耦,本质上是因为它在生产者和消费者之间增加了一个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 并不能保证消息绝对不会丢。

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

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

为什么消息会重复消费?

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

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

相关推荐
此时不提桶,更待何时2 天前
06-09-A-Kafka架构与存储原理详解
架构·kafka·linq
醉颜凉2 天前
Kafka 与 RabbitMQ/RocketMQ 选型对比:场景匹配、性能基准与迁移成本
kafka·消息队列·rabbitmq·rocketmq·中间件选型
筑梦之路3 天前
K8S yaml文件部署kafka集群(Kraft模式)——筑梦之路
容器·kafka·kubernetes
筑梦之路3 天前
Kafka KRaft 模式 Kubernetes 部署手册(StatefulSet + apache/kafka 官方镜像)——筑梦之路
kafka·kubernetes·apache
智码看视界4 天前
技术选型指南:Pulsar vs Kafka:消息队列选型终极对比与压测
kafka·消息队列·pulsar·消息中间件·流处理·高吞吐·架构选型
俊哥大数据4 天前
Flink1.20.3 实时消费 Kafka 数据并解析入湖 Paimon1.4.2 全流程实战
分布式·flink·kafka·数据湖·paimon
今年下半年5 天前
记录一次IBMS物联网项目的架构设计及技术栈应用
物联网·websocket·网络协议·tcp/ip·微服务·udp·kafka
Nano叶落11 天前
Kafka 消费积压排查入门:Docker 搭环境亲手制造一次 ‘消息堵死‘,10 分钟看懂 Lag
kafka
吉甫作诵12 天前
Kafka 集群安装与运维实战:消费组排查、Offset 重置与副本重分配
大数据·运维·分布式·kafka·消息队列
imDwAaY12 天前
消息队列四大核心问题:顺序性、幂等性、可靠性与一致性
学习·kafka·rabbitmq