圖4:雙隊列設(shè)計
生產(chǎn)者將行為紀(jì)錄寫入Queue1(主要保持?jǐn)?shù)據(jù)新鮮),Worker從Queue1消費新鮮數(shù)據(jù)。如果發(fā)生上述異常數(shù)據(jù),則Worker將異常數(shù)據(jù)寫入Queue2(主要保持異常數(shù)據(jù))。
這樣Worker對Queue1的消費進度不會被異常數(shù)據(jù)影響,可以保持消費新鮮數(shù)據(jù)。RetryWorker會監(jiān)聽Queue2,消費異常數(shù)據(jù),如果處理還沒有成功,則按照一定的策略(如下圖)等待或者重新將異常數(shù)據(jù)寫入Queue2。
圖5:補償重試策略
另外,數(shù)據(jù)發(fā)生積壓的情況下,可以調(diào)整Worker的消費游標(biāo),從最新的數(shù)據(jù)重新開始消費,保證最新數(shù)據(jù)得到處理。中間未經(jīng)處理的一段數(shù)據(jù)則啟動backupWorker,指定起止游標(biāo),在消費完指定區(qū)間的數(shù)據(jù)之后,backupWorker會自動停止。(如下圖)
圖6:積壓數(shù)據(jù)消解
三、可用性
作為基礎(chǔ)服務(wù),對可用性的要求比一般的服務(wù)要高得多,因為下游依賴的服務(wù)多,一旦出現(xiàn)故障,有可能會引起級聯(lián)反應(yīng)影響大量業(yè)務(wù)。項目從設(shè)計上對以下問題做了處理,保障系統(tǒng)的可用性:
系統(tǒng)是否有單點? DB擴容/維護/故障怎么辦? Redis維護/升級補丁怎么辦? 服務(wù)萬一掛了如何快速恢復(fù)?如何盡量不影響下游應(yīng)用?
首先是系統(tǒng)層面上做了全棧集群化。kafka和storm本身比較成熟地支持集群化運維;web服務(wù)支持了無狀態(tài)處理并且通過負載均衡實現(xiàn)集群化;Redis和DB方面攜程已經(jīng)支持主備部署,使用過程中如果主機發(fā)生故障,備機會自動接管服務(wù);通過全棧集群化保障系統(tǒng)沒有單點。
另外系統(tǒng)在部分模塊不可用時通過降級處理保障整個系統(tǒng)的可用性。先看看正常數(shù)據(jù)處理流程:(如下圖)
圖7:正常數(shù)據(jù)流程
在系統(tǒng)正常狀態(tài)下,storm會從kafka中讀取數(shù)據(jù),分別寫入到redis和mysql中。服務(wù)從redis拉?。ㄈ〔坏綍r從db補償),輸出給客戶端。DB降級的情況下,數(shù)據(jù)流程也隨之改變(如下圖)
圖8:系統(tǒng)降級-DB
當(dāng)mysql不可用時,通過打開db降級開關(guān),storm會正常寫入redis,但不再往mysql寫入數(shù)據(jù)。數(shù)據(jù)進入reids就可以被查詢服務(wù)使用,提供給客戶端。另外storm會把數(shù)據(jù)寫入一份到kafka的retry隊列,在mysql正常服務(wù)之后,通過關(guān)閉db降級開關(guān),storm會消費retry隊列中的數(shù)據(jù),從而把數(shù)據(jù)寫入到mysql中。redis和mysql的數(shù)據(jù)在降級期間會有不一致,但系統(tǒng)恢復(fù)正常之后會通過retry保證數(shù)據(jù)最終的一致性。redis的降級處理也類似(如下圖)
圖9:系統(tǒng)降級-Redis
唯一有點不同的是Redis的服務(wù)能力要遠超過MySQL。所以在Redis降級時系統(tǒng)的吞吐能力是下降的。這時我們會監(jiān)控db壓力,如果發(fā)現(xiàn)MySQL壓力較大,會暫時停止數(shù)據(jù)的寫入,降低MySQL的壓力,從而保證查詢服務(wù)的穩(wěn)定。
為了降低故障情況下對下游的影響,查詢服務(wù)通過Netflix的Hystrix組件支持了熔斷模式(如下圖)。
圖10:Circuit Breaker Pattern
在該模式下,一旦服務(wù)失敗請求在給定時間內(nèi)超過一個閾值,就會打開熔斷開關(guān)。在開關(guān)開啟情況下,服務(wù)對后續(xù)請求直接返回失敗響應(yīng),不會再讓請求經(jīng)過業(yè)務(wù)模塊處理,從而避免服務(wù)器進一步增加壓力引起雪崩,也不會因為響應(yīng)時間延長拖累調(diào)用方。
開關(guān)打開之后會開始計時,timeout后會進入Half Open的狀態(tài),在該狀態(tài)下會允許一個請求通過,進入業(yè)務(wù)處理模塊,如果能正常返回則關(guān)閉開關(guān),否則繼續(xù)保持開關(guān)打開直到下次timeout。這樣業(yè)務(wù)恢復(fù)之后就能正常服務(wù)請求。