Fork me on GitHub

【转】10倍请求压力来袭,你的系统会被击垮吗?

本文转自:https://shishan100.gitee.io/docs/#/./docs/page/page6

这篇文章,我们继续给大家聊聊另外一个线上系统在生产环境遇到的问题。

一、背景介绍
背景情况是这样:线上一个系统,在某次高峰期间 MQ 中间件故障的情况下,触发了降级机制,结果降级机制触发之后运行了一小会儿,突然系统就完全卡死,无法响应任何请求。

给大家简单介绍一下这个系统的整体架构,这个系统简单来说就是有一个非常核心的行为,就是往 MQ 里写入数据,但是这个往 MQ 里写入的数据是非常核心及关键的,绝对不容许有丢失。

所以最初就设计了一个降级机制,如果一旦 MQ 中间件故障,那么这个系统立马就会把核心数据写入本地磁盘文件。

额外提一句,如果有同学不太清楚 MQ 中间件的概念,建议看一下之前发的一篇文章《哥们,你们的系统架构中为什么要引入消息中间件?》,先对 MQ 中间件这个东西做一个基本的了解。

但是如果说在高峰期并发量比较高的情况下,接收到一条数据立马同步写本地磁盘文件,这个性能绝对是极其差的,会导致系统自身的吞吐量瞬间大幅度下降,这个降级机制是绝对无法在生产环境运行的,因为自己就会被高并发请求压垮。

因此当时设计的时候,对降级机制进行了一番精心的设计。

我们的核心思路是一旦 MQ 中间件故障,触发降级机制之后,系统接收到一条请求不是立马写本地磁盘,而是采用内存双缓冲 + 批量刷磁盘的机制。

简单来说,系统接收到一条消息就会立马写内存缓冲,然后开启一个后台线程把内存缓冲的数据刷新到磁盘上去。

整个过程,大家看看下面的图,就知道了。

avatar

这个内存缓冲实际在设计的时候,分为了两个区域。

一个是 current 区域,用来供系统写入数据,另外一个是 ready 区域,用来供后台线程刷新数据到磁盘里去。

每一块内存区域设置的缓冲大小是 512kb,系统接收到请求就写 current 缓冲区,但是 current 缓冲区总共就 512kb 的内存空间,因此一定会写满。

同样,大家结合下面的图,一起来看看。

avatar

current 缓冲区写满之后,就会交换 current 缓冲区和 ready 缓冲区。交换过后,ready 缓冲区承载了之前写满的 512kb 的数据。

然后 current 缓冲区此时是空的,可以继续接着系统继续将新来的数据写入交换后的新的 current 缓冲区。

整个过程如下图所示:

avatar

此时,后台线程就可以将 ready 缓冲区中的数据通过 Java NIO 的 API,直接高性能 append 方式的写入到本地磁盘文件里。

当然,这里后台线程会有一整套完善的机制,比如说一个磁盘文件有固定大小,如果达到了一定大小,自动开启一个新的磁盘文件来写入数据。

二、埋下隐患
好!通过上面一套机制,即使是高峰期,也能顺利的抗住高并发的请求,一切看起来都很美好!

但是,当时这个降级机制在开发时,我们采取的思路,为后面埋下了隐患!

当时采取的思路是:如果 current 缓冲区写满了之后,所有的线程全部陷入一个 while 循环无限等待。

等到什么时候呢?一直需要等到 ready 缓冲区的数据被刷到磁盘文件之后,清空掉 ready 缓冲区,然后跟 current 缓冲区进行交换。

这样 current 缓冲区要再次变为空的缓冲区,才可以让工作线程继续写入数据。

但是大家有没有考虑过一个异常的情况有可能会发生?

就是后台线程刷新 ready 缓冲区的数据到磁盘文件,实际上也是需要一点时间的。

万一在他刷新数据到磁盘文件的过程中,current 缓冲区突然也被写满了呢?

此时就会导致系统的所有工作线程无法写入 current 缓冲区,线程全部卡死。

给大家上一张图,看看这个问题!

avatar

这个就是系统的降级机制的双缓冲机制最根本的问题了,在开发好这套降级机制之后,采用正常的请求压力测试过,发现两块缓冲区在设置为 512kb 的情况下,运作良好,没有什么问题。

三、高峰请求,问题爆发
但是问题就出在高峰期上了。某一次高峰期,系统请求压力达到了平时的 10 倍以上。

当然正常流程下,高峰期的时候,写请求其实也是直接全部写到 MQ 中间件集群去的,所以哪怕你高峰期流量增加 10 倍也无所谓,MQ 集群是可以天然抗高并发的。

但是当时不幸的是,在高峰期的时候,MQ 中间件集群突然临时故障,这也是一年遇不到几次的。

这就导致这个系统突然触发了降级机制,然后就开始写入数据到内存双缓冲里面去。

要知道,此时是高峰期啊,请求量是平时正常的 10 倍!因此 10 倍的请求压力瞬间导致了一个问题的发生。

这个问题就是瞬时涌入的高并发请求一下将 current 缓冲区写满,然后两个缓冲区交换,后台线程开始刷新 ready 缓冲区的数据到磁盘文件里去。

结果因为高峰期请求涌入过快,导致 ready 缓冲区的数据还没来得及刷新到磁盘文件,此时 current 缓冲区又突然写满了。。。

这就尴尬了,线上系统瞬间开始出现异常。。。

典型的表现就是,所有机器上部署的实例全部线程都卡死,处于 wait 的状态。

四、定位问题,对症下药

于是,这套系统开始在高峰期无法响应任何请求。后来经过线上故障紧急排查、定位和抢修,才解决了这个问题。

其实说来解决方法也很简单,我们通过 jvm dump 出来快照进行分析,查看系统的线程具体是卡在哪个环节,然后发现大量线程卡死在等待 current 缓冲区的地方。

这就很明显知道原因了,解决方法就是对线上系统扩容双段缓冲的大小,从 512kb 扩容到一个缓冲区 10mb。

这样在线上高峰期的情况下,也可以稳稳的让降级机制的双缓冲机制流畅的运行,不会说瞬间高峰涌入的请求打满两块缓冲区。

因为缓冲区越大,就可以让 ready 缓冲区被 flush 到磁盘文件的过程中,current 缓冲区没那么快被打满。

但是这个线上故障反馈出来的一个教训,就是对系统设计和开发的任何较为复杂的机制,都必须要参照线上高峰期的最大流量来压力测试。只有这样,才能确保任何在系统上线的复杂机制可以经得起线上高峰期的流量的考验。

posted @ 2021-05-10 10:28  秋楓  阅读(94)  评论(0编辑  收藏  举报