微服務(wù)與異常處理(上)

背景


微服務(wù)的價值

微服務(wù)架構(gòu)的價值在于擴展(scale),主要有一下三個方面

  • 通過無狀態(tài)的水平擴展可以分擔(dān)流量
  • 通過添加不同類型的業(yè)務(wù)服務(wù)可以輕易實現(xiàn)業(yè)務(wù)功能的擴展
  • 通過對數(shù)據(jù)集的分區(qū)處理可以實現(xiàn)數(shù)據(jù)集的擴展

但是作為分布式系統(tǒng)的一種依然引入了分布式天生的問題。
分布式系統(tǒng)中局部錯誤是不可避免的,長遠(yuǎn)來看所有的系統(tǒng)都可能發(fā)生錯誤。
微服務(wù)的系統(tǒng)的一直運行在一個可能局部失敗的狀態(tài)下。

本文目標(biāo)


對于分布式容錯系統(tǒng)中的一些常見問題給出泛用的應(yīng)對思路

如何應(yīng)對微服務(wù)架構(gòu)中的錯誤


緩存和數(shù)據(jù)降級

緩存不但可以減少服務(wù)間通訊的次數(shù),提高性能。當(dāng)下游服務(wù)發(fā)生異常的情況下,可以返回緩存的服務(wù)作為數(shù)據(jù)降級。
但是必須考慮緩存擊穿和緩存更新的策略的問題。

使用緩存需要考慮雪崩

如果緩存暫時不可用(比如一個熱key還沒有從數(shù)據(jù)庫寫入緩存),所有的請求會壓到數(shù)據(jù)庫,如果未提前做容量預(yù)估,可能會把數(shù)據(jù)庫壓垮(在緩存恢復(fù)之前,數(shù)據(jù)庫可能一直都起不來),導(dǎo)致系統(tǒng)整體不可服務(wù)。解決方案:
1 緩存的高可用/緩存分區(qū),減小單實例緩存壓力。
2 多個請求相同的key請求時,合并請求,保證只有同時只有一個請求會穿透到數(shù)據(jù)庫。
3 緩存擊穿的數(shù)量過大之后限流,保護數(shù)據(jù)庫/下游服務(wù)壓力過大。

使用緩存如何考慮保證數(shù)據(jù)庫和緩存的一致性

旁路緩存:
讀取時:首先訪問緩存 --> 緩存不存在 --> 訪問數(shù)據(jù)庫 --> 更新緩存 --> 返回數(shù)據(jù)
修改時:對于存在緩存過期機制的情況下,修改數(shù)據(jù)庫服務(wù) --> 將緩存刪除。

旁路緩存的優(yōu)點是在于可以在只有在讀取的時候會更新緩存。不需要擔(dān)心讀取和修改的先后關(guān)系(讀取中修改緩存的先后關(guān)系可以通過防止緩存穿透的方案解決),數(shù)據(jù)庫中數(shù)據(jù)的正確性要求高于緩存。當(dāng)數(shù)據(jù)庫更新成功,緩存重置失敗的時候,容許緩存和數(shù)據(jù)庫暫時的不一致,通過最終一致的方式保證一致。

當(dāng)對于緩存和數(shù)據(jù)庫一致要求很高的時候,可以使用分布式鎖或者兩步提交的方案。但是此方案消耗太高,緩存往往是用于解決一致性問題的,如此操作很有可能得不償失。

微服務(wù)系統(tǒng)與事務(wù)

對于需要保證數(shù)據(jù)一致性的業(yè)務(wù)場景,通常使用兩步提交的方案。利用數(shù)據(jù)庫的事務(wù)特性,操作多條記錄完成后再提交,否則多條操作一起失敗回滾至先前狀態(tài)。

但是在微服務(wù)的分布式環(huán)境下,往往一個服務(wù)會有自己獨立的數(shù)據(jù)庫作為存儲。本地的兩步提交方案無法適維護整個系統(tǒng)的一致性,大大提高了事務(wù)處理的復(fù)雜度。

分布式事務(wù)解決方案 - saga

如何在分布式系統(tǒng)中保證事務(wù)的一致性問題。


image

如圖所示當(dāng)一個請求會涉及到多個服務(wù)并需要服務(wù)間保持一致性的情況下就涉及到分布式事務(wù)。

saga 模式是將一整個分布式的事務(wù)分解成一個序列的事務(wù),這些分解后的各個事務(wù)分別由獨立的服務(wù)負(fù)責(zé)更新各自存儲中的數(shù)據(jù)。第一個事務(wù)處理由外部請求觸發(fā),下一個事務(wù)處理依據(jù)上一個事務(wù)處理的結(jié)果觸發(fā)。形成邏輯上執(zhí)行的一個序列。

image

繼續(xù)細(xì)分下去由兩種主要方式來實現(xiàn)saga模式。

  • 事件驅(qū)動
    沒有統(tǒng)一的中控服務(wù),當(dāng)上一個服務(wù)的事務(wù)提交的時候發(fā)送一個消息(事件)出來,這個消息會被傳遞給下一個服務(wù),下一個服務(wù)監(jiān)聽這個事件并作出相應(yīng)的處理,處理完之后也會拋出一個事件給再下一個服務(wù)直到整個分布式事務(wù)的結(jié)束。


    image

    但是事件驅(qū)動的方式存在一個缺點是,當(dāng)隨著版本迭代,不同的子事務(wù)被添加進系統(tǒng)。會發(fā)現(xiàn)各個微服務(wù)需要處理事務(wù)種類越來越多,事件發(fā)送的路徑越來越復(fù)雜,維護成本極具升高。

  • 命令模式(樂團模式)
    命令模式定義了一個單獨的服務(wù),類似交響樂團的指揮,分別告訴各個子服務(wù)分布式事務(wù)當(dāng)前階段應(yīng)該做什么。而各個服務(wù)不需要關(guān)心上下游的服務(wù)依賴。當(dāng)然付出的代價是需要多維護一個“指揮”的服務(wù),并且這個“指揮”的服務(wù)將比不同服務(wù)承擔(dān)更多的復(fù)雜度。

image

消息隊列與消息處理

消息類中間件被廣泛運用于微服務(wù)架構(gòu)中,起到業(yè)務(wù)結(jié)偶,消息分區(qū),削峰平谷等作用。
但是如何可口的處理消息是一個需要深入思考的問題。

消息隊列與一致性

在以消息為基礎(chǔ)的異步系統(tǒng)中,強一致的分布式事務(wù)成本過高,往往一致性目標(biāo)是“最終一致性”。

最終一致性指的是兩個系統(tǒng)的狀態(tài)保持一致,要么都成功,要么都失敗。當(dāng)然有個時間限制,理論上越快越好,但實際上在各種異常的情況下,可能會有一定延遲達(dá)到最終一致狀態(tài),但最后兩個系統(tǒng)的狀態(tài)是一樣的。
以一個銀行的轉(zhuǎn)賬過程來理解最終一致性,轉(zhuǎn)賬的需求很簡單,如果A系統(tǒng)扣錢成功,則B系統(tǒng)加錢一定成功。反之則一起回滾,像什么都沒發(fā)生一樣。
然而,這個過程中存在很多可能的意外:
1 A扣錢成功,調(diào)用B加錢接口失敗。
2 A扣錢成功,調(diào)用B加錢接口雖然成功,但獲取最終結(jié)果時網(wǎng)絡(luò)異常引起超時。
3 A扣錢成功,B加錢失敗,A想回滾扣的錢,但A機器down機。

上文的saga模式就是一種使用最終一致性實現(xiàn)分布式事務(wù)的方案。使用消息隊列作為消息通訊的中間件可以有效減少,業(yè)務(wù)服務(wù)放在異步/最終一致的問題中處理的難度。消息隊列可以暫存一部分消息(kafka)。對于consumer的投遞失敗可以做反復(fù)重新投遞。消息隊列可以實現(xiàn)廣播消息而不需要上游服務(wù)維護監(jiān)聽列表。

可靠發(fā)送

為了滿足分布式系統(tǒng)中的最終一致性,常常需要接受以下條件:
1 同一個消息可能會被投遞多次
2 消息接受的順序不一定和消息發(fā)送的順序相同
3 消息的接收可能會有延時。

方案:
當(dāng)服務(wù)發(fā)送一個消息前,先將消息或者消息的等價信息落地。然后再發(fā)送消息,當(dāng)發(fā)送失敗或者無法知道消息投遞成功的情況下,以一個超時時間不停輪詢所有待發(fā)送消息。最終保證消息能夠發(fā)送成功。

這種做法類似于消息隊列可靠投遞的方案:

producer往broker發(fā)送消息之前,需要做一次落地。
請求到server后,server確保數(shù)據(jù)落地后再告訴客戶端發(fā)送成功。
支持廣播的消息隊列需要對每個待發(fā)送的endpoint,持久化一個發(fā)送狀態(tài),直到所有endpoint狀態(tài)都OK才可刪除消息。

這種方式隱形地對于服務(wù)的自治提供了一種可能性。使用消息隊列關(guān)聯(lián)的服務(wù)不需要依賴于下游服務(wù)的健康狀態(tài),最終消息會在下游服務(wù)健康的時候被送達(dá),只需要保證當(dāng)前自己服務(wù)消息的傳遞是可靠的。

可靠消費

當(dāng)消息服務(wù)器把消息傳遞給消費者后(可推可拉),消費者需要能夠明確的告知服務(wù)器是否處理了當(dāng)前消息,(回ack 或者 nack 消息)。即使邏輯上當(dāng)前服務(wù)能夠處理當(dāng)前消息,但是由于服務(wù)狀態(tài),服務(wù)載荷等問題,consumer無法在收到消息的開始知曉消息的處理狀況,所以ack消息的回復(fù)往往是在對應(yīng)消息的處理完成之后。這種方式?jīng)Q定了消息可能在被處理,或者處理完之后再次消費,所以cosumer必須要保證冪等消費消息的能力。

當(dāng)broker把消息投遞給消費者后,消費者可以立即響應(yīng)我收到了這個消息。但收到了這個消息只是第一步,我能不能處理這個消息卻不一定?;蛟S因為消費能力的問題,系統(tǒng)的負(fù)荷已經(jīng)不能處理這個消息;或者是剛才狀態(tài)機里面提到的消息不是我想要接收的消息,主動要求重發(fā)。

把消息的送達(dá)和消息的處理分開,這樣才真正的實現(xiàn)了消息隊列的本質(zhì)-解耦。所以,允許消費者主動進行消費確認(rèn)是必要的。當(dāng)然,對于沒有特殊邏輯的消息,默認(rèn)Auto Ack也是可以的,但一定要允許消費方主動ack。

消費的順序

分布式系統(tǒng)中保證消費消息的順序和發(fā)送消費的順序一致往往是很困難的,或者需要付出更嚴(yán)苛的條件。

  • 消費發(fā)送和消費接受都是需要單點單線程的。
    邏輯上消息的接受和消息的發(fā)送都是排他的,不然難以同步/接受方的順序,(在一個排隊執(zhí)行的場景下,并行操作意義不大)。

  • 消息可能丟失:
    當(dāng)一個消息反復(fù)投遞或者處理失敗時,為了保證接下來的消息能夠繼續(xù)消費,只能丟棄當(dāng)前的消息。不然整個消息隊列會進入一個死鎖的狀態(tài)。
    綜上所述 消息生產(chǎn)/消費系統(tǒng)的一般的設(shè)計思路是在保證消息可達(dá)的情況下盡量少的投遞/消費次數(shù)。

消息的冪等處理

應(yīng)對接收到重復(fù)消息的處理方法:

  • MessageId方法
    每個消息生成一個獨立的messageId,這個messageId可以用于作為存儲/中間件的主鍵,可以快速判斷消息是否已被接收過。但是付出的代價是需要存儲大量的消息,并且需要考慮當(dāng)前處理過程中消息存儲和業(yè)務(wù)數(shù)據(jù)存儲的一致性。

  • 版本號方案:

對于同一類的消息,上下游保證一個版本號,下游處理完記錄版本號,只有當(dāng)新消息的版本號大于當(dāng)前接收的最新版本號才接收這條消息,付出的代價就是對于亂序的消息,小版本號的消息如果后至,則會被拋棄。

  • 狀態(tài)機方案(復(fù)雜)
    每個消息帶上一個自增的版本號,將所有接收到的消息存儲下來,每次接收到消息之后使用存儲的日志重新構(gòu)建消息的最終狀態(tài)。其中使用狀態(tài)機:消息輸入狀態(tài)機,基于上一個狀態(tài)產(chǎn)生一個新的狀態(tài),以此循環(huán)往復(fù),最終獲取最終的狀態(tài)。

  • 狀態(tài)機方案(簡單)
    對于一個邏輯、流程較為簡單的業(yè)務(wù),設(shè)計的時候可以保證在一個狀態(tài)下所能夠接收的消息類型沒有交集。即可以保證在當(dāng)前狀態(tài)下不會接收上一個(上上個)狀態(tài)下可接收的消息。

減少消息的反復(fù)投遞

消息隊列需要考慮減少反復(fù)投遞帶來的系統(tǒng)開銷,特別是當(dāng)流量突發(fā)的情況下,反復(fù)重試會造成消息隊列堵塞,服務(wù)過載等情況,進而引發(fā)雪崩效應(yīng)。

消息投遞重試的問題,如果多次重試后依然投遞失敗,應(yīng)當(dāng)修正消息的繼續(xù)投遞。

  • 停止繼續(xù)投遞,根據(jù)業(yè)務(wù)特性,必要的時候可以進入業(yè)務(wù)回滾
  • 降低繼續(xù)投遞的頻率,避免因為重試消耗過多資源。
  • 重試投遞的消息進入另一個備份的慢消息處理隊列,避免因為重試消息使當(dāng)前的消息隊列堆積。

缺失的篇章

消息隊列堆積的處理方案

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時請結(jié)合常識與多方信息審慎甄別。
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點,簡書系信息發(fā)布平臺,僅提供信息存儲服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容