降級(jí)機(jī)制設(shè)計(jì)不當(dāng),線上系統(tǒng)瞬間崩潰
背景介紹
背景情況是這樣:線上一個(gè)系統(tǒng),在某次高峰期間MQ中間件故障的情況下,觸發(fā)了降級(jí)機(jī)制,結(jié)果降級(jí)機(jī)制觸發(fā)之后運(yùn)行了一小會(huì)兒,突然系統(tǒng)就完全卡死,無(wú)法響應(yīng)任何請(qǐng)求。
給大家簡(jiǎn)單介紹一下這個(gè)系統(tǒng)的整體架構(gòu),這個(gè)系統(tǒng)簡(jiǎn)單來(lái)說(shuō)就是有一個(gè)非常核心的行為,就是往MQ里寫(xiě)入數(shù)據(jù),但是這個(gè)往MQ里寫(xiě)入的數(shù)據(jù)是非常核心及關(guān)鍵的,絕對(duì)不容許有丟失。
所以最初就設(shè)計(jì)了一個(gè)降級(jí)機(jī)制,如果一旦MQ中間件故障,那么這個(gè)系統(tǒng)立馬就會(huì)把核心數(shù)據(jù)寫(xiě)入本地磁盤(pán)文件。
但是如果說(shuō)在高峰期并發(fā)量比較高的情況下,接收到一條數(shù)據(jù)立馬同步寫(xiě)本地磁盤(pán)文件,這個(gè)性能絕對(duì)是極其差的,會(huì)導(dǎo)致系統(tǒng)自身的吞吐量瞬間大幅度下降,這個(gè)降級(jí)機(jī)制是絕對(duì)無(wú)法在生產(chǎn)環(huán)境運(yùn)行的,因?yàn)樽约壕蜁?huì)被高并發(fā)請(qǐng)求壓垮。
因此當(dāng)時(shí)設(shè)計(jì)的時(shí)候,對(duì)降級(jí)機(jī)制進(jìn)行了一番精心的設(shè)計(jì)。
我們的核心思路是一旦MQ中間件故障,觸發(fā)降級(jí)機(jī)制之后,系統(tǒng)接收到一條請(qǐng)求不是立馬寫(xiě)本地磁盤(pán),而是采用內(nèi)存雙緩沖 + 批量刷磁盤(pán)的機(jī)制。
簡(jiǎn)單來(lái)說(shuō),系統(tǒng)接收到一條消息就會(huì)立馬寫(xiě)內(nèi)存緩沖,然后開(kāi)啟一個(gè)后臺(tái)線程把內(nèi)存緩沖的數(shù)據(jù)刷新到磁盤(pán)上去。
整個(gè)過(guò)程,大家看看下面的圖,就知道了。
這個(gè)內(nèi)存緩沖實(shí)際在設(shè)計(jì)的時(shí)候,分為了兩個(gè)區(qū)域。
一個(gè)是current區(qū)域,用來(lái)供系統(tǒng)寫(xiě)入數(shù)據(jù),另外一個(gè)是ready區(qū)域,用來(lái)供后臺(tái)線程刷新數(shù)據(jù)到磁盤(pán)里去。
每一塊內(nèi)存區(qū)域設(shè)置的緩沖大小是512kb,系統(tǒng)接收到請(qǐng)求就寫(xiě)current緩沖區(qū),但是current緩沖區(qū)總共就512kb的內(nèi)存空間,因此一定會(huì)寫(xiě)滿。
同樣,大家結(jié)合下面的圖,一起來(lái)看看。
current緩沖區(qū)寫(xiě)滿之后,就會(huì)交換current緩沖區(qū)和ready緩沖區(qū)。交換過(guò)后,ready緩沖區(qū)承載了之前寫(xiě)滿的512kb的數(shù)據(jù)。
然后current緩沖區(qū)此時(shí)是空的,可以繼續(xù)接著系統(tǒng)繼續(xù)將新來(lái)的數(shù)據(jù)寫(xiě)入交換后的新的current緩沖區(qū)。
整個(gè)過(guò)程如下圖所示:
此時(shí),后臺(tái)線程就可以將ready緩沖區(qū)中的數(shù)據(jù)通過(guò)Java NIO的API,直接高性能append方式的寫(xiě)入到本地磁盤(pán)文件里。
當(dāng)然,這里后臺(tái)線程會(huì)有一整套完善的機(jī)制,比如說(shuō)一個(gè)磁盤(pán)文件有固定大小,如果達(dá)到了一定大小,自動(dòng)開(kāi)啟一個(gè)新的磁盤(pán)文件來(lái)寫(xiě)入數(shù)據(jù)。
埋下隱患
好!通過(guò)上面一套機(jī)制,即使是高峰期,也能順利的抗住高并發(fā)的請(qǐng)求,一切看起來(lái)都很美好!
但是,當(dāng)時(shí)這個(gè)降級(jí)機(jī)制在開(kāi)發(fā)時(shí),我們采取的思路,為后面埋下了隱患!
當(dāng)時(shí)采取的思路是:如果current緩沖區(qū)寫(xiě)滿了之后,所有的線程全部陷入一個(gè)while循環(huán)無(wú)限等待。
等到什么時(shí)候呢?一直需要等到ready緩沖區(qū)的數(shù)據(jù)被刷到磁盤(pán)文件之后,清空掉ready緩沖區(qū),然后跟current緩沖區(qū)進(jìn)行交換。
這樣current緩沖區(qū)要再次變?yōu)榭盏木彌_區(qū),才可以讓工作線程繼續(xù)寫(xiě)入數(shù)據(jù)。
但是大家有沒(méi)有考慮過(guò)一個(gè)異常的情況有可能會(huì)發(fā)生?
就是后臺(tái)線程刷新ready緩沖區(qū)的數(shù)據(jù)到磁盤(pán)文件,實(shí)際上也是需要一點(diǎn)時(shí)間的。
萬(wàn)一在他刷新數(shù)據(jù)到磁盤(pán)文件的過(guò)程中,current緩沖區(qū)突然也被寫(xiě)滿了呢?
此時(shí)就會(huì)導(dǎo)致系統(tǒng)的所有工作線程無(wú)法寫(xiě)入current緩沖區(qū),線程全部卡死。
給大家上一張圖,看看這個(gè)問(wèn)題!
這個(gè)就是系統(tǒng)的降級(jí)機(jī)制的雙緩沖機(jī)制最根本的問(wèn)題了,在開(kāi)發(fā)好這套降級(jí)機(jī)制之后,采用正常的請(qǐng)求壓力測(cè)試過(guò),發(fā)現(xiàn)兩塊緩沖區(qū)在設(shè)置為512kb的情況下,運(yùn)作良好,沒(méi)有什么問(wèn)題。
高峰請(qǐng)求,問(wèn)題爆發(fā)
但是問(wèn)題就出在高峰期上了。某一次高峰期,系統(tǒng)請(qǐng)求壓力達(dá)到了平時(shí)的10倍以上。
當(dāng)然正常流程下,高峰期的時(shí)候,寫(xiě)請(qǐng)求其實(shí)也是直接全部寫(xiě)到MQ中間件集群去的,所以哪怕你高峰期流量增加10倍也無(wú)所謂,MQ集群是可以天然抗高并發(fā)的。
但是當(dāng)時(shí)不幸的是,在高峰期的時(shí)候,MQ中間件集群突然臨時(shí)故障,這也是一年遇不到幾次的。
這就導(dǎo)致這個(gè)系統(tǒng)突然觸發(fā)了降級(jí)機(jī)制,然后就開(kāi)始寫(xiě)入數(shù)據(jù)到內(nèi)存雙緩沖里面去。
要知道,此時(shí)是高峰期啊,請(qǐng)求量是平時(shí)正常的10倍!因此10倍的請(qǐng)求壓力瞬間導(dǎo)致了一個(gè)問(wèn)題的發(fā)生。
這個(gè)問(wèn)題就是瞬時(shí)涌入的高并發(fā)請(qǐng)求一下將current緩沖區(qū)寫(xiě)滿,然后兩個(gè)緩沖區(qū)交換,后臺(tái)線程開(kāi)始刷新ready緩沖區(qū)的數(shù)據(jù)到磁盤(pán)文件里去。
結(jié)果因?yàn)楦叻迤谡?qǐng)求涌入過(guò)快,導(dǎo)致ready緩沖區(qū)的數(shù)據(jù)還沒(méi)來(lái)得及刷新到磁盤(pán)文件,此時(shí)current緩沖區(qū)又突然寫(xiě)滿了。
這就尷尬了,線上系統(tǒng)瞬間開(kāi)始出現(xiàn)異常。
典型的表現(xiàn)就是,所有機(jī)器上部署的實(shí)例全部線程都卡死,處于wait的狀態(tài)。
定位問(wèn)題,對(duì)癥下藥
于是,這套系統(tǒng)開(kāi)始在高峰期無(wú)法響應(yīng)任何請(qǐng)求。后來(lái)經(jīng)過(guò)線上故障緊急排查、定位和搶修,才解決了這個(gè)問(wèn)題。
其實(shí)說(shuō)來(lái)解決方法也很簡(jiǎn)單,我們通過(guò)jvm dump出來(lái)快照進(jìn)行分析,查看系統(tǒng)的線程具體是卡在哪個(gè)環(huán)節(jié),然后發(fā)現(xiàn)大量線程卡死在等待current緩沖區(qū)的地方。
這就很明顯知道原因了,解決方法就是對(duì)線上系統(tǒng)擴(kuò)容雙段緩沖的大小,從512kb擴(kuò)容到一個(gè)緩沖區(qū)10mb。
這樣在線上高峰期的情況下,也可以穩(wěn)穩(wěn)的讓降級(jí)機(jī)制的雙緩沖機(jī)制流暢的運(yùn)行,不會(huì)說(shuō)瞬間高峰涌入的請(qǐng)求打滿兩塊緩沖區(qū)。
因?yàn)榫彌_區(qū)越大,就可以讓ready緩沖區(qū)被flush到磁盤(pán)文件的過(guò)程中,current緩沖區(qū)沒(méi)那么快被打滿。
但是這個(gè)線上故障反饋出來(lái)的一個(gè)教訓(xùn),就是對(duì)系統(tǒng)設(shè)計(jì)和開(kāi)發(fā)的任何較為復(fù)雜的機(jī)制,都必須要參照線上高峰期的最大流量來(lái)壓力測(cè)試。只有這樣,才能確保任何在系統(tǒng)上線的復(fù)雜機(jī)制可以經(jīng)得起線上高峰期的流量的考驗(yàn)。