本文转载自微信公众号「菜鸟飞呀飞」,作者刘进坤。转载本文请联系菜鸟飞呀飞公众号。
一、前言
面大厂时,MQ 这一中间件基本都是必问的,本文是面试时被问到的其中一题的答案。
二、为什么丢消息
一条消息从产生到被消费,中间会经历三个环节:生产者、MQ 内部、消费者,消息在这三个环节中均有可能出现丢失。
1. 在生产者环节丢失
当生产者往 MQ 中写数据时,可能出现网络故障,消息压根就没到达 MQ 内部,生产者端对这个异常没有捕获,不做任何处理,这种场景会导致消息丢失。
当消息达到 MQ 所在的机器,但是 MQ 出现了异常,返回异常给生产者端,生产者对异常没做相应处理,导致消息丢失
2. 在 MQ 环节丢失
当消息达到 MQ 内部后,消息会先存于内存当中,然后再持久化到磁盘。如果在消息处于内存当中,还未来得及刷入磁盘时,MQ 所在机器宕机,此时,消息会丢失。
即使消息持久化到磁盘了,但当前机器的磁盘发生损坏,消息依旧会丢失。
3. 在消费者环节丢失消息
- 当消息达到消费者端时,如果消费者开启了 Auto ACK,那么消费者消费到消息后,就会自动提交 offset 到 MQ,如果此时消费者还没来得及处理消息对应的业务逻辑,机器宕机了或者被新手 kill -9 pid 了,此时消息也就被丢失了。即使机器重新恢复后,由于已经提交了之前消息的 offset,所以 MQ 不会再将之前的消息推送给消费者,因此这条消息丢失。(这也是很多文章都说不准使用 kill -9 pid 的其中原因之一)
- 消费者没有开启 Auto ACK,但是消费者消费到消息后,将消息扔到了线程池,然后提交 offset,让线程池异步去处理消息。如果线程池中的任务还没处理完,机器宕机或者 OOM 等异常,这也将导致消息没被处理,从而丢失。
三、如何保证不丢消息
消息在上述三个环节均有可能出现丢失,因此需要保证上述这三个环节均不出现丢数据的可能,才能完全保证消息不丢失。
1. 生产者
当往 MQ 中写消息出现异常时,采用 try...catch... 捕获异常,在异常代码块中重试。
如果是 RocketMQ,可以直接使用 RokcetMQ 的事务消息,来保证消息不丢失。至于为什么 RocketMQ 为什么能保证消息不丢失。
2. MQ
对于 MQ 而言,要保证消息不丢失,一方面是要保证消息要持久化到磁盘,另一方面是需要保证消息有多个副本。在不同的 MQ 中,对这两点的处理方式均不太一样,下面主要以 kafka 和 RocketMQ 为例说明。
对于 kafka 而言,需要保证如下三点:
- 要求每个 partition 的副本数大于 1(replication factor > 1)
- 要求 kafka 服务端设置 broker.insync.replicas 参数的值大于 1,它的意思是要求至少有一个 flower 在和 leader 同步
- 将 acks=all,在写数据时,要求消息写到所有的 leader 和 flower 之后,才认为消息写成功。
对于 RocketMQ 而言,需要保证以下几点:
- 基于 Dledger 的 broker 主从架构,每个主 broker 需要挂至少 2 个 slave broker。
- 采用同步刷盘策略。
同步刷盘指的是 MQ 接收到生产的消息时,将消息先写入到 OS cache 中,然后再将 OS cache 刷入到磁盘后,才返回 success 给生产者;与之对应的是异步刷盘,异步刷盘指的是将将消息写入到 OS cache 中后就返回 success 给生产者,然后由操作系统决定 OS cache 中的数据什么时候刷入到磁盘。显然,同步刷盘虽然能保证数据不丢失,但是性能会比较低,同步刷盘时,MQ 的吞吐量没有异步刷盘高。
3. 消费者
关闭 Auto ACK。消费到消息后,处理完业务逻辑后再手动提交 offset。
不使用异步线程池处理消息。
四、总结
保证消息不丢失是一个非常苛刻的要求,要保证消息不丢失就需要牺牲系统的性能(生产者的处理逻辑变复杂,MQ 的吞吐量降低,消费者消费速度下降等),所以需要结合具体的业务场景来决定是不是需要百分百保证消息不丢失。通常而言,对于核心链路:如订单、交易等相关的业务,基本都需要保证保证消息百分百不丢失。