线上RocktMQ重复投递半事务消息故障排查

1. 故障现象

    2020-11-18 10:40开始,业务线反馈线上收到大量的重复MQ半事务消息,导致容器资源消耗急剧攀升,经查看MQ日志,发现broker-b的Master服务,报出大量半事务消息回查日志,且每次回查的起始offset不变化,但opOffset不断迅速增大,且HALF_TOPIC队列急速膨胀,查看RocketMQ console监控web后台,发现出现大量消息堆积,且都在broker-B。offset日志如下:

2. 原因分析

2.1. MQ半事务消息回查机制

      1、producer提交半事务消息,会先存储在RMQ_SYS_TRANS_HALF_TOPIC队列(以下简称HALF队列)

      2、如果producer在MQ回查前,主动确认了本次事务结果,不管是提交还是回滚,MQ都会把该消息转存至RMQ_SYS_TRANS_OP_HALF_TOPIC队列(以下简称OP队列),且如果事务是成功提交的,同时把消息转存至真实的topic,让消费者进行消费。

      3、如果producer未能及时确认事务结果,则MQ会定时消费HALF队列,回查对应事务的结果,根据回查结果进行跟上述第二点一样的处理。

      4、如果当前事务消息尚未超时,则本次回查终止。

      5、更新HALF队列的消费进度offset,等待下一次定时回查,从最新进度offset开始。

2.2. 故障原因

      1、MQ默认对超出4K的消息进行压缩存储,并设置sysFlag为已压缩

2、半事务消息需要回查时,从HALF获取消息,如果消息是被压缩过的,此处会进行解压处理

3、把解压后的消息renew一份,追加到HALF队列后,sysFlag标记仍为已压缩,但实际msgBody已经解压

4、下次回查时,该事务消息仍未有结果,对其进行回查,但将其从HALF里读出时出现异常,因为根据sysFlag,消息进行了压缩,但实际进行解压又失败

5、NPE异常未被捕获,直接抛到TransactionalMessageServiceImpl.check()方法,该方法仅打印出来,未作其他业务处理,导致未能更新HALF的回查offset,导致下次回查,仍然从上次的offset开始,陷入死循环

3. 解决方案

1、临时解决方案:

  • a)故障当天通过手动修改HALF的offset,让其跳过异常消息,得以恢复。
  • b)可以暂时修改transactionTimeout参数,加大半事务消息的事务超时时间,降低MQ回查的概率,规避出现故障。
  • c)可以暂时修改Producer端的compressMsgBodyOverHowmuch参数,加大启用压缩的阈值(不推荐)。         

 2、最终解决方案:

  MQ官方4.6.0+以后的版本已经修复该问题,对新版MQ进行稳定性测试后,升级到生产。

原文地址:https://www.cnblogs.com/dennyzhangdd/p/14028635.html