APP下载

架构实战(10)——讯息处理中的死循环

消息来源:baojiabao.com 作者: 发布时间:2024-05-19

报价宝综合消息架构实战(10)——讯息处理中的死循环

讯息中介软件有很多种,在使用讯息伫列时,消费讯息一般有两种模式,推送模式(Push)和拉取模式(Pull)。有些中介软件会支援两种,例如RabbitMq;有些支援一种,例如Kafka只支援Pull。在专案中,应用了Aws SQS服务,只支援Pull模式,以此为出发点,谈谈讯息处理中的死循环。

大胆的使用死循环

在code review时,发现同事写了如下的这段程式码。基本逻辑没有问题,读资料-->处理资料-->ACK。请注意用到了Thread.sleep()在每次处理完成后,执行绪暂停100ms。他的解释是,如果不用sleep,会导致死CPU占用率100%。

示例程式码-原始版

他的这种解释正确吗?首先,read和ack过程,都是包含网络IO操作的。其次,process函式也有读库、写库的程式码,同样包含网络IO操作。网络IO操作和CPU的执行速度相比,是非常慢的,即使没有sleep函式,网络IO过程中,CPU存在等待状况,不会出现CPU占用率100%的情况。另一方面,这种写法限定了单执行绪的处理能,因为每次最多可以从queue中读取10条资料,每秒最多能读取100条,考虑到处理各环节的时间损耗,处理能力更弱。

通过分析,做了第一版的修改。读到资料正常处理,读取不到资料执行绪暂停100ms,这样可以避免空伫列造成大量的读。

示例程式码-修改1

事实证明,这样的修改是合适的,在压力测试过程中,同时启动8个执行绪,在2核心16G内存的服务器上,CUP使用率达到70%左右,单例项的处理能力1200/s,达到设计预期。测试结果和具体的业务逻辑有关,具体实践中,根据实际执行情况

单讯息的死循环

首先弄明白ack的作用。通过read函式从伫列中pull资料,资料并没有在queue中删除,而是设定了讯息对其他讯息在时效期内,其他执行绪不可见;超过时效期,讯息就可以被其他执行绪可见。呼叫ack函式,则是从伫列中删除讯息,其他执行绪任何时间都不可见了。(这是SQS的实现,其他讯息伫列可能有些差异,但是ack的作用是一样的)。

上述程式码中,如果prcess过程发生异常,会跳过ack函式,直接跳转到异常处理逻辑。结果就是讯息无法被删除,其他执行绪或本执行绪在时效期过后,再次读到这条讯息,但同样会处理失败,这样就陷入了同一个讯息处理的死循环。上述程式码的另一个问题,每次读取10条,其中一条失败导致所有的讯息无法ack,这其中可能包含已经处理过的讯息,导致讯息的重复消费。

针对这两个个问题,有两个方面可以优化:

1、死信伫列

在伫列中存放太久的讯息,被称为是死信,存放的时间是可以配置的,并可以转发到另外的伫列,称之为死信伫列。假设死信时间为30min,讯息可见性的时效期配置为5s,一个失败的讯息最多可能被处理360次,必然会拖累讯息处理能力。

是否可以把死信伫列的时间设定的很短呢?比如10ms。讯息伫列的一大作用就是消除波峰,在高峰期可能存在讯息积压,超过10s的讯息积压就不会被处理,显然有违讯息伫列的设计初衷。能否把讯息的可见性设定的很长呢?比方说10min。讯息被处理失败后,10分钟后才能再次被处理,这样导致讯息的时效性太差。显然,仅仅通过死信伫列,是无法优雅完美的解决问题的,还需要对程式码进行再次优化。

2、程式码优化

结合死信伫列的作用,再来完善程式码。逐条处理讯息,处理出现异常,尝试把讯息存库,然后呼叫ack。这样只有在process和save同时失败时,才会走到死信伫列的逻辑,大大减少了系统的压力。

示例程式码-最终版

推送模式中的隐藏死循环

以上讲述的是pull模式的死循环问题,其实是非常明显的,因为看到了while关键字的存在。推送模式下,死循环隐藏的就有点深了。

push模式中,一般通过listener实现,以RabbitMQ为例,示例程式码如下。

示例程式码-推送模式

Spring 的RebbitMQ Template,隐藏了ack的过程,这样的程式码逻辑上并没有问题。但正是这样简洁的一段程式码,发生过一次严重的线上故障,已经过去了2年多,但至今仍然记忆犹新。

RebbitMQ Template在实现的过程中,要考虑讯息被正确的ack,策略就是只有被正确处理的讯息才能被ack。简化一下,程式码逻辑如下:

示例程式码-推送实现原理

虽然在自己写的程式码中没有whilie,但是在Template中存在。如果在处理的过程中出现异常,讯息不能被ack,rabbitMq会再次推送此讯息,这样就造成了单条讯息的处理死循环。更严重是,RabbitMQ并没有时效期的概念,失败的讯息立即就会被推送回来。

两年前的那次线上故障是因为处理讯息是发生空指标错误,讯息不能被处理,讯息不停地被推送到处理例项。单条讯息的处理,并没有其他效能损失,导致CPU使用率100%,并且输出非常多的日志,每小时几十个G。而处理讯息的服务例项,在服务丛集中对外提供界面供其他服务呼叫,拖累其他服务的长时间等待,进而引发雪崩效应,导致整个丛集的不可用。事后对日志的分析发现,在一小时内对单条讯息的处理次数超过40万次,非常夸张。

这个问题的处理也非常简单,解决掉空指标异常,并且优化处理逻辑。在以后的工作中,时刻有了这个紧绷的弦,防止讯息处理的死循环。

示例程式码-最终版本

今天讲述了讯息处理过程中的死循环问题,希望对大家有所帮助。欢迎关注我,持续更新IT互联网相关技术,原创不易,多多支援。

2020-02-01 03:58:00

相关文章