记一次生产事故-消息积压分析

1,812 阅读6分钟

背景

在5月20日这天,出了一个生产事故,导致消息队列中积压了大量消息,峰值时期达到 200w+。

下面主要就这个生产事故,顺带梳理一下消息积压问题以及常见的解决方案。

问题描述

  • 0点到6点期间,消息积压量一直呈现上升趋势,峰值达到 200w+
  • 6点到9点期间,消息积压量维持稳定
  • 9点到9点半期间,消费能力大幅度提升,消息积压量迅速减少
  • 9点半到下午3点期间,消息积压情况几乎不存在
  • 3点以后,消息积压问题彻底解决

问题原因

背景:系统重构,涉及新旧系统的切换,过渡阶段新旧系统同时存在,所以就涉及新旧系统数据的双向同步。

整体流程如下:

  • 旧写新:旧系统通过 otter 监听数据库变更,将变更内容发送到消息队列,消费者消费变更消息后调用新系统的服务A将变更写入新系统
  • 新写旧:新系统在数据变更后,会同步往旧系统的数据库中写一份

该流程中存在两个核心问题,

问题一:在新系统往旧系统同步数据时,因为代码问题,原本只应该变更一条数据的操作,变成了全量数据的变更操作,短时间内旧数据库存在大量数据变更写入,进而导致监听数据库 binlog 的 otter 往消息队列中写入大量消息

问题二:旧系统从消息队列中消费到数据变更的消息,会将变更同步至新系统,因为新系统没有做流程控制,存在循环写入的问题,在数据持久化完毕后,又会往旧系统写一份,因此又会触发“问题一”

问题解决

定位到问题后,

  • 第一步先把存在问题的新写旧流程关闭,防止大量数据变更,进而产生大量消息
  • 第二步对旧系统中消费者流程进行降级,即暂时关闭旧写新流程,消费速度大幅度提升,消息积压问题得到显著解决
  • 第三步和DBA沟通,将otter中历史积压的大量数据库变更消息废弃(通过重置otter消费位点的方式实现)
  • 第四步把消费者的消费位点置于最新位置,将前面积压的变更消息都抛弃,然后通过数据迁移工具进行新老系统的数据同步

消息积压

本文的重点是生产环境出现消息积压问题后的处理方案,至于前期如何预防消息积压,本文暂时不涉及。

出现消息积压的问题,大多数情况是以下原因:

  • 生产者生产速度过快,可能是业务量正常激增,也可能是因为代码问题导致产生大量消息
  • 消费者消费速度过慢,可能是消费者性能问题导致消费速度跟不上生产速度,也可能是消费流程出现问题,导致消息一直没法被成功消费,不断触发重试

针对上述的问题,主要有如下解决方案:

  • 关闭失败重试机制,主要针对大量消费失败的消息不断触发重试的情况,这部分消息即使重试之后还是会失败,因此直接关闭失败重试机制,防止消费失败的消息一直被重复消费
  • 扩容消费者,提升消费能力,快速将积压消息消费完
  • 如果历史消息允许抛弃,则可以通过重置消费位点的方式将前面积压的消息都抛弃

关闭失败重试

首先查看是否存在大量因消费失败,而不断重试消费的消息,如果存在,可以考虑先把失败重试机制关闭,对于消费失败的消息先都记录下来,后续统一做补偿。

以 RocketMQ 的消费者为例:

无论实际消费成功还是失败,统一返回消费成功,就不会触发后续的重试机制了。

把消费失败的原因找到并修复之后,再统一补偿这些消费失败的记录。

扩容消费者

主流的消息队列 Kafka,RocketMQ 都是基于分区模型存储,消费消息的,因此单纯增加消费者实例数量,并不能解决问题。

如下展示了正常消息消费场景:

如果TopicA出现了消息积压情况,且3个分区都存在消息积压,这个时候去增加消费者实例数,试图通过这个方式提升消费能力是没有效果的。

如下图,可以看到新增了Consumer4之后,消费者数量大于分区数,而一个分区最多只能被一个消费者消费,导致新增的Consumer4无法正常消费消息。

因此在增加消费者实例的同时,也要同步增加Topic的分区数。

但仅仅新增分区还是不够的,因为积压的消息存放在历史创建的分区中,无法被新增的消费者实例消费到,因此还需要将积压的消息均衡投递至新的分区。

整体流程如下:

  • 创建一个临时主题TempTopicA,将该主题分区数依据实际情况扩容至原来的数倍
  • 写一个临时消费程序,消费原主题中积压的消息,其中只做转发逻辑,将消费到的消息均匀投递至临时主题TempTopicA的各分区中
  • 新增消费者实例,并且改为消费临时主题TempTopicA中的消息

当消息积压问题解决后,再恢复原来的架构,恢复流程如下:

  • 关闭临时消费程序
  • 将部分消费者(原消费者数量)改回消费原主题TopicA中的消息
  • 待临时主题TempTopicA中的消息都被消费完毕之后,下线消费该主题的消费者实例

消息抛弃

如果积压的消息允许丢弃,可以通过重置消费位点的方式,把之前积压的消息都抛弃。

同样以阿里云提供的 RocketMQ 服务为例,首先找到需要重置消费位点的Group。

它提供了两种重置策略:

  • 清除所有堆积消息,从最新位点开始消费:若选择此项,该Group ID在消费该Topic下的消息时会跳过当前堆积(未被消费)的所有消息,从这之后发送的最新消息开始消费
  • 按时间点进行消费位点重置:选择该选项后会出现时间点选择的控件。请选择一个时间点,这个时间点之后发送的消息才会被消费