面向面试编程:消息队列——可靠性

281 阅读9分钟

持续创作,加速成长!这是我参与「掘金日新计划 · 10 月更文挑战」的第21天,点击查看活动详情

面试官:如何保证消息的可靠性传输?

使用mq有个基本原则,数据不能多一条,也不能少一条。

RabbitMQ

生产者弄丢了数据

生产者在发送数据到MQ的过程中,可能由于网络问题,在半路就搞丢了。

方法一

此时可以选用RabbitMQ提供的事务功能,生产者发送消息之前打开一个事务,channel.txSelect,如果mq没接到消息,那么生产者会报错,回滚事务,再去重试,再去发送。

事务机制是同步的,生产者发送消息会等待成功或失败,导致吞吐量下降。

方法二

先把channel设置成confirm模式,然后发送消息给mq,rabbitmq如果接收到这条消息,就会回调生产者本地接口,告诉生产者已经收到了。

如果在接收消息时报错,回调接口告诉生产者接收失败了,可以再次重试。

生产者需要提供一个供回调的回调接口的实现,如ack/nack,在nack中再重发一次这个消息。

异步模式,不会阻塞,吞吐量会高一些。

RabbitMQ弄丢了数据

持久化也会有可能丢数据,刚收到的消息还没写到磁盘,MQ挂了。

步骤一

第一个是创建queue是将其设置成持久化的,这样保证rabbitmq持久化queue的元数据,但不会持久化queue里的数据。

步骤二

第二个是发送消息时将消息的deliveryMode设置为2,就是将消息设置为持久化的。

必须同时设置这两个持久化才行,而且持久化可以跟生产者那边的confirm机制配合使用,当消息被写入磁盘后在返回ack。

消费端弄丢了数据

出现这个问题,是因为打开了消息消费者的autoack机制,接到消息之后,自动通知rabbitmq说已经处理完了,消费者层面需要将autoack关闭,手动发送ack,防止宕机。

Kafka

消费者弄丢数据

原因,消费到这个消息,自动提交offset,需要手动提交offset,避免挂掉。
此时只要将自动offset关闭,在处理完成后手动提交offset,保证数据不丢失。

kafka自己弄丢数据

场景:kafka某个broker宕机,然后重新选举partition的leader是,此时其他follower刚好还有些数据没有同步,然后leader挂了,选举某个follower之后,他就少了一些数据,造成数据丢失。
此时一般是要求起码设置如下四个参数:

  • 给topic设置replication.factor参数,这个值必须大于1,要求每个partition必须至少有2个副本;
  • 在kafka服务端设置min.insync.replicas参数,这个值必须大于1,要求一个leader至少感知到有至少一个follower还能跟自己保持联系,这样才能保证leader挂了,还有一个follower;
  • 在producer端设置ack= all,要求每条数据,必须是写入所有的replica之后,才能认为是成功了; - 在producer端设置retries=MAX,这个要求一旦写入失败,就无限重试,卡在这里了;
    这样配置之后,至少在kafka broker端就可以保证在leader所在的broker发生故障进行leader切换时数据不会丢失。

生产者不会弄丢数据

ack=all,一定不会丢,leader收到并且所有follower都收到了才会确认返回。

RocketMQ

生产者发送消息零丢失方案

在RocketMQ中,有一个非常强悍有力的功能,就是事务消息的功能,凭借这个事务级的消息机制,就可以让我们确保生产者系统推送给出去的消息一定会成功写入MQ里,绝对不会半路就搞丢了。

Broker消息零丢失方案

即使消息已经进入磁盘文件了,但是这个时候消费者系统还没来得及消费这条消息,然后此时这台机器的磁盘突然就坏了,就会一样导致消息丢失,而且可能消息再也找不回来了,同样会丢失数据。

同步刷盘

解决这个问题的第一个关键点,就是将异步刷盘调整为同步刷盘。

所谓的异步刷盘,就是之前我们一直说的那种模式。也就是说,你的消息即使成功写入了MQ,他也就在机器的os cache中,没有进入磁盘里,要过一会儿等操作系统自己把os cache里的数据实际刷入磁盘文件中去。所以在异步刷盘的模式下,我们的写入消息的吞吐量肯定是极高的,毕竟消息只要进入os cache这个内存就可以了,写消息的性能就是写内存的性能,那每秒钟可以写入的消息数量肯定更多了,但是这个情况下,可能就会导致数据的丢失。

所以如果一定要确保数据零丢失的话,可以调整MQ的刷盘策略,我们需要调整broker的配置文件,将其中的flushDiskType配置设置为:SYNC_FLUSH,默认他的值是ASYNC_FLUSH,即默认是异步刷盘的。如果调整为同步刷盘之后,我们写入MQ的每条消息,只要MQ告诉我们写入成功了,那么他们就是已经进入了磁盘文件了!比如我们发送half消息的时候,只要MQ返回响应是half消息发送成功了,那么就说明消息已经进入磁盘文件了,不会停留在os cache里。

主从架构

我们必须要对Broker使用主从架构的模式,也就是说,必须让一个Master Broker有一个Slave Broker去同步他的数据,而且你一条消息写入成功,必须是让Slave Broker也写入成功,保证数据有多个副本的冗余。这样一来,你一条消息但凡写入成功了,此时主从两个Broker上都有这条数据了,此时如果你的Master Broker的磁盘坏了,但是Slave Broker上至少还是有数据的,数据是不会因为磁盘故障而丢失的。

对于主从同步的架构,是基于DLedger技术和Raft协议的主从同步架构,如果采用了这套架构,对于所有的消息写入,只要写入成功,那就一定会通过Raft协议同步给其他的Broker机器。此时任何一台机器的磁盘故障,数据也是不会丢失的。

Consumer消息零丢失方案

如果消费者系统已经拿到了这条消息,但是消息目前还在他的内存里,还没执行消费的逻辑,此时他就直接提交了这条消息的offset到broker去说自己已经处理过了。接着消费者系统在这个时候直接崩溃了,内存里的消息就没了,消息也没消费成功,结果Broker已经收到他提交的消息offset了,还以为他已经处理完这条消息了。等消费者系统重启的时候,就不会再次消费这条消息了。

RocketMQ消费者

RocketMQ的消费者中会注册一个监听器,就是MessageListenerConcurrently这个东西,当你的消费者获取到一批消息之后,就会回调你的这个监听器函数,让你来处理这一批消息。

然后当你处理完毕之后,你才会返ConsumeConcurrentlyStatus.CONSUME_SUCCESS作为消费成功的示意,告诉RocketMQ,这批消息我已经处理完毕了。所以对于RocketMQ而言,其实只要你的消费者系统是在这个监听器的函数中先处理一批消息,基于这批消息都处理完成,然后返回了那个消费成功的状态,接着才会去提交这批消息的offset到broker去。所以在这个情况下,如果你对一批消息都处理完毕了,然后再提交消息的offset给broker,接着消费者系统崩溃了,此时是不会丢失消息的。

那么如果是消费者系统获取到一批消息之后,还没处理完,也就没返回ConsumeConcurrentlyStatus.CONSUME_SUCCESS这个状态呢,自然没提交这批消息的offset给broker呢,此时消费者系统突然挂了,会怎么样?其实在这种情况下,你对一批消息都没提交他的offset给broker的话,broker不会认为你已经处理完了这批消息,此时你突然消费者系统的一台机器宕机了,他其实会感知到你的消费者系统的一台机器作为一个Consumer挂了。接着他会把你没处理完的那批消息交给消费者系统的其他机器去进行处理,所以在这种情况下,消息也绝对是不会丢失的。

不能异步消费消息

在默认的Consumer的消费模式之下,必须是你处理完一批消息了,才会返回ConsumeConcurrentlyStatus.CONSUME_SUCCESS这个状态标识消息都处理结束了,去提交offset到broker去。在这种情况下,正常来说是不会丢失消息的,即使你一个Consumer宕机了,他会把你没处理完的消息交给其他Consumer去处理。

但是这里我们要警惕一点,就是我们不能在代码中对消息进行异步的处理,如果我们开启了一个子线程去处理这批消息,然后启动线程之后,就直接返回ConsumeConcurrentlyStatus.CONSUME_SUCCESS状态了,如果要是用这种方式来处理消息的话,那可能就会出现你开启的子线程还没处理完消息呢,你已经返回ConsumeConcurrentlyStatus.CONSUME_SUCCESS状态了,就可能提交这批消息的offset给broker了,认为已经处理结束了。然后此时你消费者系统突然宕机,必然会导致你的消息丢失了!

因此在RocketMQ的场景下,我们如果要保证消费数据的时候别丢消息,你就老老实实的在回调函数里处理消息,处理完了你再返回ConsumeConcurrentlyStatus.CONSUME_SUCCESS状态表明你处理完毕了!

消息处理失败场景下的方案

消费者底层的一些依赖可能有故障了,比如数据库宕机,缓存宕机之类的,此时你就没办法完成消息的处理了,那么可以通过一些返回状态去让消息进入RocketMQ自带的重试队列,同时如果反复重试还是不行,可以让消息进入RocketMQ自带的死信队列,后续针对死信队列中的消息进行单独的处理就可以了。