背景介绍背景如下:某线上系统,当MQ中间件在高峰期出现故障时,触发降级机制。结果降级机制触发后,降级机制运行了一小会儿,突然系统完全死机,无法响应任何请求。下面简单介绍一下这个系统的整体架构。简单来说,这个系统有一个很核心的行为,就是往MQ里面写入数据,但是写入MQ里面的数据是很核心很关键的,绝对不能有损失。因此,最初设计了降级机制。如果MQ中间件出现故障,系统会立即将核心数据写入本地磁盘文件。但是如果在高峰期并发比较高,如果接收到一条数据,马上同步写入本地磁盘文件,那性能绝对是极差的,系统本身的吞吐量会瞬间急剧下降.这种降级机制是绝对不能在生产环境中运行的,因为它会被高并发请求压垮。所以当时设计的时候,降级机制是经过精心设计的。我们的核心思想是,一旦MQ中间件出现故障,触发降级机制,系统收到请求时,不会立即写入本地磁盘,而是采用内存双缓冲+批量刷盘的机制。简单的说,当系统收到消息后,会立即写入内存缓冲区,然后启动一个后台线程,将内存缓冲区中的数据刷新到磁盘中。您可以通过查看下图了解整个过程。这个内存缓冲区在设计的时候其实分为两个区域。一个是当前区,用于系统写入数据,一个是就绪区,用于后台线程刷新数据到磁盘。每个内存区设置的buffer大小为512kb,系统收到请求时写入当前buffer,但是当前buffer总共有512kb的内存空间,所以一定是满的。同样,让我们??看看下面的图片。当前缓冲区满后,交换当前缓冲区和就绪缓冲区。exchange之后,readybuffer中携带着之前填充的512kb数据。那么此时currentbuffer是空的,系统可以在exchange之后继续往新的currentbuffer中写入新的数据。整个过程如下图所示:此时,后台线程可以通过JavaNIOAPI以高性能追加方式直接将readybuffer中的数据写入本地磁盘文件。当然这里的后台线程会有一整套完善的机制。例如,磁盘文件具有固定大小。如果达到一定大小,会自动打开一个新的磁盘文件写入数据。隐患不错!通过上面的一套机制,即使在高峰期,也能成功抵抗高并发请求,一切看起来都很好!但是,当时在开发这个降级机制的时候,我们采用的思路给以后埋下了隐患!当时采用的思路是:如果当前缓冲区满了,所有线程都会陷入while循环,无限等待。我们要等到什么时候?需要等到readybuffer中的数据被flush到磁盘文件中,清空readybuffer,再与currentbuffer进行交换。这样,当前缓冲区必须再次变成空缓冲区,工作线程才能继续写入数据。但是你有没有想过可能会出现异常情况呢?即使后台线程将就绪缓冲区中的数据刷新到磁盘文件中,其实也需要一点时间。如果在刷新数据到磁盘文件的过程中,当前缓冲区突然满了怎么办?此时系统所有工作线程都无法写入当前缓冲区,所有线程都卡住了。给大家一张图看看这个问题!这就是系统降级机制的双缓冲机制最根本的问题。开发了这个降级机制后,在正常的请求压力下进行了测试。发现这两个buffer在设置为512kb时效果很好。问题是什么。请求高峰期,问题爆发但问题出在高峰期。高峰期,系统请求压力达到平时的10倍以上。当然,正常流程下,在高峰期,其实所有的写请求都是直接写到MQ中间件集群的,所以就算你高峰期流量增加10倍也没关系,MQ集群自然能抗高并发。但不幸的是当时正值高峰期,MQ中间件集群突然出现临时故障,一年很少遇到几次。这导致系统突然触发降级机制,然后开始向内存双缓冲区写入数据。要知道,现在是高峰期,请求量是平时的10倍!于是10倍的请求压力瞬间就出问题了。问题是瞬时涌入的高并发请求将当前缓冲区填满,然后两个缓冲区交换,后台线程开始将就绪缓冲区中的数据刷新到磁盘文件中。结果,由于高峰期请求的快速涌入,readybuffer中的数据还没来得及刷新到磁盘文件中,此时currentbuffer突然又满了。这就尴尬了,在线系统突然开始出现异常。一个典型的表现就是部署在所有机器上的实例的所有线程都卡住了,处于等待状态。定位问题对症下药结果,系统开始在高峰时段无法响应任何请求。后来经过紧急故障排除,在线定位修复,问题得到解决。其实解决方法也很简单。我们使用jvmdump进行快照分析,看系统线程卡在什么地方,然后发现有大量线程卡在等待当前buffer。显然是这个原因。解决办法是将线上系统的双段缓冲区大小从512kb扩大到10mb的缓冲区。这样,在在线高峰期的情况下,降级机制的双缓冲机制也能顺利运行,不会说瞬时高峰涌入的请求占满了两个缓冲区。因为buffer越大,readybuffer可以flush到磁盘文件中,当前buffer不会那么快被填满。但是,从这次线上故障反馈中得到的教训是,任何更复杂的系统设计和开发机制,都必须参照线上高峰时段的最大流量进行压力测试。只有这样,才能保证系统上线的任何复杂机制都能经受住线上高峰流量的考验。
