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