消息队列如何保证消息可靠性?
上一篇我们聊了《消息为什么能够解耦系统?》
消息队列之所以能够解耦,本质上是因为它在生产者和消费者之间增加了一个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 并不能保证消息绝对不会丢。
它真正做到的是:即使某个环节发生故障,也尽可能发现问题、恢复消息,并降低消息丢失的概率。这也是为什么:可靠性越高,系统需要付出的性能成本也越高。
下一篇,我们继续讨论另一个很多人都遇到过的问题:
为什么消息会重复消费?
上一篇:《消息为什么能够解耦系统?》
下一篇:《消息为什么会重复消费?》