19.RocketMQ之消息丢失的场景以及解决方案


highlight: arduino-light

如果我们的项目中引入了MQ,势必要面对的一个问题,就是消息丢失问题,今天我们就来聊聊消息是怎么丢失的。

现在假设我们的业务是这样的,用户通过订单系统下了一个订单,订单系统完成支付扣减余额后会发送消息给RocketMQ,然后积分系统会从RocketMQ中消费消息,去给用户增加积分。

但是突然有一天有用户反映,支付订单扣减余额之后,自己的积分并没有增长,这是为什么呢? 经过排查日志,我们只发现了推送消息给MQ的日志,而没有发现积分系统消费这条消息的日志,这就导致了积分系统并没有给用户发放积分。 也就是说,消息在传输过程中丢失了。

在系统的核心链路中,如果发生消息丢失的问题,可能会产生恶劣的后果,为了解决此类问题,我们必须弄明白什么时候会发生消息丢失。

推送消息消息丢失:消息补偿解决或者事务消息

生产者发送消息时由于网络故障或broker的master节点宕机导致消息丢失。比如订单系统完成支付后会发送消息给RocketMQ,此时由于网络故障或broker的master节点宕机导致消息丢失。

同步发送消息+消息补偿

所以此时我们可以根据消息发送的异常将消息保存在消息表,定时扫描消息表重试发送消息。

伪代码如下: java 1.订单系统完成支付,扣减余额 try{ 2.发送消息 }catch{ 3.消息发送失败保存消息表 }

但是假如第1步和第2步执行完毕,系统宕机了,此时数据库数据回滚了消息发送成功了 咋办? 此时可以将扣减余额和保存消息放在1个事务里。如果扣减余额失败就不保存消息,如果扣减余额成功就保存消息,此时由于数据库的事务特性可以保证2者的一致性。最后启动1个定时器定时扫描消息表发送消息即可,发送消息的时候还要做好幂等,否则可能会出现消息的重复消费。

伪代码如下: 1.订单系统完成支付,扣减余额 2.保存消息表

事务消息

上面的第一种方案伪代码如下: java 1.订单系统完成支付,扣减余额 try{ 2.发送消息 }catch{ 3.消息发送失败保存消息表 } 但是假如第1步和第2步执行完毕,系统宕机了,此时数据库数据回滚了消息发送成功了 咋办?

基于rocketmq的事务消息,首先发送half消息给mq。

发送或响应失败不执行本地事务。

成功执行本地事务根据本地事务结果进行rollback或commit则直接结束。

如果回调失败等待rocketmq后续定时任务扫描half消息来询问订单系统,整个过程即使某个服务宕机也不影响业务!

异步刷盘导致消息丢失:改为同步刷盘

接下来假设我们订单系统推送到MQ这一过程没有任何问题,消息成功到达了MQ中,此时订单系统会认为消息写入成功了,那么这时候消息就一定不会丢失了吗?

答案是否定的,这个时候也不能保证消息的不丢失,我们来分析一下。 通过之前文章的了解,相信大家都还记得,当消息写入到MQ后,MQ会把消息先写入到os cache,也就是操作系统的缓存区中,本质也是内存。

也就是说,你认为发送成功的消息,可能只存在于内存中,还没到磁盘中。 那么如果这个时候机器宕机了,os cache中的消息数据将会跟着丢失掉,是不是这个理。

消息到达rocketmq,当使用异步刷盘时,可能消息对应的commit log还在page cache中未刷新到磁盘,此时broker的物理机宕机了重启导致page cache中数据丢失。

解决方案是异步刷盘改为同步刷盘。解决的方式就是把异步刷盘改为同步刷盘,具体操作就是修改一下broker的配置文件,将其中的flushDiskType配置设置为:SYNCFLUSH,默认它的值是ASYNCFLUSH,即异步刷盘。 调整为同步刷盘后,只要MQ告诉我们消息发送成功了,那么就说明消息已经在磁盘中了。

同步刷盘,磁盘故障:主从同步+磁盘备份

那么现在假设消息已经刷新到磁盘上了,是不是就可以保证万无一失了呢?

显然这个时候也是不能完全保证的,因为虽然你把数据保存到了磁盘中,但是如果磁盘发生了故障,数据还是会丢失掉。

如果选择了同步刷盘消息存储到磁盘后也可能存在丢失,当磁盘故障时,此时我们可以通过主从同步+冗余备份磁盘的方式保证尽量丢少的消息。

只要我们使用RockerMQ的高可用集群模式就可以了,也就是说如果返回消息发送成功的响应,那就代表Master Broker已经把数据同步到了Slave Broker中,保证数据有多个备份。

自动ack导致消息丢失:手动ack

消息保存到mq,消费者消费消息时未进行ack,让mq以为消息消费成功了,跳到了下一个消息的offset,此时通过手动ack机制来保证消息不丢失。

对于Kafka和RabbitMQ来讲,默认的消费模式就是上边这种自动提交的模式,所以是有可能导致消息丢失掉的。

*而RocketMQ的消费者有点不一样,它本身就是需要手动返回消息处理成功的响应的。 *

所以其实Consumer的消息丢失解决方案也很简单,就是将自动提交改为手动提交。

订阅关系不一致,同一消费组下不同消费者订阅关系不同:保证订阅关系一致

出现问题的地方在于两个消费者订阅不同的topic,但是他们的GroupName 相同,由于每个consumer都会向broker上报自己的订阅信息,groupName相同会导致两者相互覆盖的情况导致消息丢失。 org.apache.rocketmq.broker.client.ConsumerGroupInfo#updateSubscription方法导致的。

https://blog.csdn.net/zchdjb/article/details/97722500?utm_medium=distribute.pc_feed_404.none-task-blog-2~default~OPENSEARCH~default-3.control404&depth_1-utm_source=distribute.pc_feed_404.none-task-blog-2~default~OPENSEARCH~default-3.control40 订阅关系一致 (aliyun.com)

读写队列缩容导致消息丢失:先缩写后缩读

1.读写队列,是在路由时使用 在消息发送时,根据写队列个数返回路由信息,而消息消费时按照读队列个数返回路由信息。

2.在物理文件层面,只有写队列才会创建文件。

举个例子:写队列个数是8,设置的读队列个数是4. 这个时候,会创建8个文件夹,代表0 1 2 3 4 5 6 7,但在消息消费时,路由信息只返回4,在具体拉取消息时,就只会消费0 1 2 3 这4个队列中的消息,4 5 6 7中的信息压根就不会被消费。

反过来,如果写队列个数是4,读队列个数是8,在生产消息时只会往0 1 2 3中生产消息,消费消息时则会从0 1 2 3 4 5 6 7所有的队列中消费,当然 4 5 6 7中压根就没有消息,假设ConsumerGroup有两个消费者,事实上只有第一个消费者在真正的消费消息(0 1 2 3),第二个消费者压根就消费不到消息。

  1. 只有readQueueNums>=writeQueueNums,程序才能正常进行

最佳实践是readQueueNums=writeQueueNums。

那rocketmq为什么要区分读写队列呢?直接强制readQueueNums=writeQueueNums,不就没有问题了吗?rocketmq设置读写队列数的目的在于方便队列的缩容和扩容。

【RocketMQ】读写队列 https://blog.51cto.com/u_15080014/4201415

一个topic在每个broker上创建了128个队列,现在需要将队列缩容到64个,怎么做才能100%不会丢失消息,并且无需重启应用程序?

1.先缩容写队列128->64,写队列由0 1 2 ......127 缩至 0 1 2 ........63。等到64 65 66......127中的消息全部消费完后

2.再缩容读队列128->64(同时缩容写队列和读队列可能会导致部分消息未被消费,写队列还有消息,读队列已经被缩容,此时写队列中的消息永远无法被消费到了)

缩容与扩容相反,在扩容时,首先增加可读队列个数,保证Consumer先完成监听,再增加可写队列个数,使得Producer可以往新增加的队列发消息。

消息零丢失解决方案

事务消息或者消息表解决发送消息消息丢失

基于rocketmq的事物首先发送half给mq,发送或响应失败不执行本地事物,成功执行本地事物根据本地事物结果进行rollback或commit则直接结束,如果回调失败等待rocketmq后续定时任务扫描half消息来询问订单系统,整个过程即使某个服务宕机也不影响业务

主从同步+同步刷盘解决Broker的消息丢失

首先解决临时存在os cache,而未刷新到磁盘导致的消息丢失问题,那么如何解决呢?

看过之前系列文章的小伙伴都知道,Broker是有两种刷盘机制的,同步刷盘和异步刷盘。

解决的方式就是把异步刷盘改为同步刷盘,具体操作就是修改一下broker的配置文件,将其中的flushDiskType配置设置为:SYNCFLUSH,默认它的值是ASYNCFLUSH,即异步刷盘。

调整为同步刷盘后,只要MQ告诉我们消息发送成功了,那么就说明消息已经在磁盘中了。

接下来就要解决磁盘坏了导致的消息丢失问题了。

这个问题其实也很好解决,只要我们使用RockerMQ的高可用集群模式就可以了,也就是说如果返回消息发送成功的响应,那就代表Master Broker已经把数据同步到了Slave Broker中,保证数据有多个备份。

这样一来就算是Master Broker突然宕机 ,也可以通过Dledger技术进行主从的自动切换,使用我们备份的数据。

手动ack解决Consumer的消息丢失

我们已经确保了生产者和Broker的消息不会丢失了,那么消费者处理消息的时候会不会导致消息丢失呢?

答案是肯定的。

比如说我们的积分系统拿到了消息,还未执行该执行的操作,先返回给broker这条消息的offset,说这条消息已经处理过了。然后突然宕机了,这就导致mq认为这条消息已经处理过了,而实际并没有处理,所以这条消息就丢失掉了。

对于Kafka和RabbitMQ来讲,默认的消费模式就是上边这种自动提交的模式,所以是有可能导致消息丢失掉的。

而RocketMQ的消费者有点不一样,它本身就是需要手动返回消息处理成功的响应的。

所以其实Consumer的消息丢失解决方案也很简单,就是将自动提交改为手动提交

消费者不要使用异步消费

尽量避免异步调用回调函数consumeMessage。采用异步调用的结果,就可能会出现业务处理失败,但是返回了消息消费成功的异常情况。

比如下面的骚操作!

java //注册消息监听器处理消息 consumer.registerMessageListener(new MessageListenerConcurrently() { @Override public ConsumeConcurrentlyStatus consumeMessage (List msgs, ConsumeConcurrentlyContext context){ //开启子线程异步处理消息 new Thread() { public void run() { //对消息进行处理 }.start(); return ConsumeConcurrentlyStatus.CONSUME_SUCCESS; } });

消息零丢失方案的优缺点分析

如果在系统中落地一套消息零丢失的方案,无论什么场景都保证消息的可靠性,这似乎听起来不错,这也是它的优点所在,保证系统的数据都是正确的,不会有丢失的情况。

但它有什么缺点呢?

首先,引入了这套解决方案之后,系统的复杂度变高了,想想事务消息的实现方式你肯定会这么觉得。

而且比较严重的缺点是,它会导致系统的性能严重的下降,比如原来每秒可以处理好几万条的消息,结果在引入消息零丢失这套方案之后,可能每秒就只能处理几千条消息了。

事务消息的复杂性导致生产消息的过程耗时更久了,同步刷盘的策略导致写入磁盘后才返回消息,自然也会增加耗时,而消费者如果异步的处理消息,直接返回成功,整个流程的速度会更快。

所以说引入这么一套消息零丢失的方案,对于性能的影响还是很大的。

一般对于跟金钱、交易以及核心数据相关的系统和核心链路,可以采用这套方案。

比如说文章中举的例子:支付系统、订单系统、积分系统。

而对于其他的没那么核心的场景,丢失一些数据问题也不大,就可以不采用这套方案了,或者说可以做一些简化,比如事务消息改成失败重试几次的机制,刷盘策略改为异步刷盘。

参考:http://www.360doc.com/content/20/1013/17/59401483_940272690.shtml

  • 0
    点赞
  • 6
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
这个错误是由于连接到 `<172.19.0.1:10909>` 失败导致的。根据引用,在RocketMQ中手动添加一个新的topic可能会解决此问题。您可以尝试使用以下命令在RocketMQ中添加一个新的topic: ``` sh mqadmin updateTopic -n XXX:9876 -b XXX:10911 -t ESOrderListTopic ``` 引用提到,该错误通常是因为您无法访问指定的IP和端口。请确保您在`application.yml`中正确配置了RocketMQ的服务器地址。此外,还需要确保您的程序和RocketMQ不在同一台机器上,您的本地机器无法访问该IP。在这种情况下,您可以尝试在`config/broker.conf`配置文件中添加以下内容来解决此错误: ``` brokerIP1=服务器地址 namesrvAddr = 服务器地址:9876 ``` 通过这些步骤,您可以尝试解决该错误并重新连接到RocketMQ服务器。<span class="em">1</span><span class="em">2</span><span class="em">3</span> #### 引用[.reference_title] - *1* *3* [rocketMQ 踩坑解决No route info of this topic和rocketmq:connect to 172.17.42.1:10911 failed](https://blog.csdn.net/weixin_44607885/article/details/103137280)[target="_blank" data-report-click={"spm":"1018.2226.3001.9630","extra":{"utm_source":"vip_chatgpt_common_search_pc_result","utm_medium":"distribute.pc_search_result.none-task-cask-2~all~insert_cask~default-1-null.142^v93^chatsearchT3_1"}}] [.reference_item style="max-width: 50%"] - *2* [Caused by: org.apache.rocketmq.remoting.exception.RemotingConnectException: connect to 10.244.2.0:10](https://blog.csdn.net/weixin_44379605/article/details/128135104)[target="_blank" data-report-click={"spm":"1018.2226.3001.9630","extra":{"utm_source":"vip_chatgpt_common_search_pc_result","utm_medium":"distribute.pc_search_result.none-task-cask-2~all~insert_cask~default-1-null.142^v93^chatsearchT3_1"}}] [.reference_item style="max-width: 50%"] [ .reference_list ]
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值