微服務的價值
微服務架構的價值在於擴展(scale),主要有以下 3 個方面
但是作為分散式系統的一種依然引入了分散式天生的問題。 分散式系統中局部錯誤是不可避免的,長遠來看所有的系統都可能發生錯誤。 微服務系統的一直運行在一個可能局部失敗的狀態下。
對於分散式容錯系統中的一些常見問題給出泛用的應對思路
緩存不但可以減少服務間通訊的次數,提高性能。當下游服務發生異常的情況下,可以返回緩存的服務作為數據降級。 但是必須考慮緩存擊穿和緩存更新的策略的問題。
如果緩存暫時不可用(比如一個熱key還沒有從資料庫寫入緩存),所有的請求會壓到資料庫,如果未提前做容量預估,可能會把資料庫壓垮(在緩存恢復之前,資料庫可能一直都起不來),導致系統整體不可服務。
解決方案:
旁路緩存:讀取時:首先訪問緩存 --> 緩存不存在 --> 訪問資料庫 --> 更新緩存 --> 返回數據
修改時:對於存在緩存過期機制的情況下,修改資料庫服務 --> 將緩存刪除。
旁路緩存的優點是在於可以在只有在讀取的時候會更新緩存。不需要擔心讀取和修改的先後關係(讀取中修改緩存的先後關係可以通過防止緩存穿透的方案解決),資料庫中數據的正確性要求高於緩存。當資料庫更新成功,緩存重置失敗的時候,容許緩存和資料庫暫時的不一致,通過最終一致的方式保證一致。
當對於緩存和資料庫一致要求很高的時候,可以使用分散式鎖或者兩步提交的方案。但是此方案消耗太高,緩存往往是用於解決一致性問題的,如此操作很有可能得不償失。
對於需要保證數據一致性的業務場景,通常使用兩步提交的方案。利用資料庫的事務特性,操作多條記錄完成後再提交,否則多條操作一起失敗回滾至先前狀態。
但是在微服務的分散式環境下,往往一個服務會有自己獨立的資料庫作為存儲。本地的兩步提交方案無法適維護整個系統的一致性,大大提高了事務處理的複雜度。
如何在分散式系統中保證事務的一致性問題。
如圖所示當一個請求會涉及到多個服務並需要服務間保持一致性的情況下就涉及到分散式事務。
saga 模式是將一整個分散式的事務分解成一個序列的事務,這些分解後的各個事務分別由獨立的服務負責更新各自存儲中的數據。第一個事務處理由外部請求觸發,下一個事務處理依據上一個事務處理的結果觸發。形成邏輯上執行的一個序列。
繼續細分下去由兩種主要方式來實現saga模式。
沒有統一的中控服務,當上一個服務的事務提交的時候發送一個消息(事件)出來,這個消息會被傳遞給下一個服務,下一個服務監聽這個事件並作出相應的處理,處理完之後也會拋出一個事件給再下一個服務直到整個分散式事務的結束。
但是事件驅動的方式存在一個缺點是,當隨著版本迭代,不同的子事務被添加進系統。會發現各個微服務需要處理事務種類越來越多,事件發送的路徑越來越複雜,維護成本極具升高。
命令模式定義了一個單獨的服務,類似交響樂團的指揮,分別告訴各個子服務分散式事務當前階段應該做什麼。而各個服務不需要關心上下游的服務依賴。當然付出的代價是需要多維護一個「指揮」的服務,並且這個「指揮」的服務將比不同服務承擔更多的複雜度。
消息類中間件被廣泛運用於微服務架構中,起到業務結偶,消息分區,削峰平谷等作用。 但是如何可口的處理消息是一個需要深入思考的問題。
在以消息為基礎的非同步系統中,強一致的分散式事務成本過高,往往一致性目標是「最終一致性」。
最終一致性指的是兩個系統的狀態保持一致,要麼都成功,要麼都失敗。當然有個時間限制,理論上越快越好,但實際上在各種異常的情況下,可能會有一定延遲達到最終一致狀態,但最後兩個系統的狀態是一樣的。 以一個銀行的轉賬過程來理解最終一致性,轉賬的需求很簡單,如果A系統扣錢成功,則B系統加錢一定成功。反之則一起回滾,像什麼都沒發生一樣。 然而,這個過程中存在很多可能的意外: 1 A扣錢成功,調用B加錢介面失敗。 2 A扣錢成功,調用B加錢介面雖然成功,但獲取最終結果時網路異常引起超時。 3 A扣錢成功,B加錢失敗,A想回滾扣的錢,但A機器down機。
最終一致性指的是兩個系統的狀態保持一致,要麼都成功,要麼都失敗。當然有個時間限制,理論上越快越好,但實際上在各種異常的情況下,可能會有一定延遲達到最終一致狀態,但最後兩個系統的狀態是一樣的。
1 A扣錢成功,調用B加錢介面失敗。
上文的saga模式就是一種使用最終一致性實現分散式事務的方案。使用消息隊列作為消息通訊的中間件可以有效減少,業務服務放在非同步/最終一致的問題中處理的難度。消息隊列可以暫存一部分消息(kafka)。對於consumer的投遞失敗可以做反覆重新投遞。消息隊列可以實現廣播消息而不需要上游服務維護監聽列表。
為了滿足分散式系統中的最終一致性,常常需要接受以下條件:
方案:
當服務發送一個消息前,先將消息或者消息的等價信息落地。然後再發送消息,當發送失敗或者無法知道消息投遞成功的情況下,以一個超時時間不停輪詢所有待發送消息。最終保證消息能夠發送成功。
這種做法類似於消息隊列可靠投遞的方案:
producer往broker發送消息之前,需要做一次落地。 請求到server後,server確保數據落地後再告訴客戶端發送成功。 支持廣播的消息隊列需要對每個待發送的endpoint,持久化一個發送狀態,直到所有endpoint狀態都OK才可刪除消息。
這種方式隱形地對於服務的自治提供了一種可能性。使用消息隊列關聯的服務不需要依賴於下游服務的健康狀態,最終消息會在下游服務健康的時候被送達,只需要保證當前自己服務消息的傳遞是可靠的。
當消息伺服器把消息傳遞給消費者後(可推可拉),消費者需要能夠明確的告知伺服器是否處理了當前消息,(回ack 或者 nack 消息)。即使邏輯上當前服務能夠處理當前消息,但是由於服務狀態,服務載荷等問題,consumer無法在收到消息的開始知曉消息的處理狀況,所以ack消息的回復往往是在對應消息的處理完成之後。這種方式決定了消息可能在被處理,或者處理完之後再次消費,所以cosumer必須要保證冪等消費消息的能力。
當broker把消息投遞給消費者後,消費者可以立即響應我收到了這個消息。但收到了這個消息只是第一步,我能不能處理這個消息卻不一定。或許因為消費能力的問題,系統的負荷已經不能處理這個消息;或者是剛才狀態機裡面提到的消息不是我想要接收的消息,主動要求重發。
把消息的送達和消息的處理分開,這樣才真正的實現了消息隊列的本質-解耦。所以,允許消費者主動進行消費確認是必要的。當然,對於沒有特殊邏輯的消息,默認Auto Ack也是可以的,但一定要允許消費方主動ack。
分散式系統中保證消費消息的順序和發送消費的順序一致往往是很困難的,或者需要付出更嚴苛的條件。
綜上所述 消息生產/消費系統的一般的設計思路是在保證消息可達的情況下盡量少的投遞/消費次數。
應對接收到重複消息的處理方法:
每個消息生成一個獨立的messageId,這個messageId可以用於作為存儲/中間件的主鍵,可以快速判斷消息是否已被接收過。但是付出的代價是需要存儲大量的消息,並且需要考慮當前處理過程中消息存儲和業務數據存儲的一致性。
對於同一類的消息,上下游保證一個版本號,下游處理完記錄版本號,只有當新消息的版本號大於當前接收的最新版本號才接收這條消息,付出的代價就是對於亂序的消息,小版本號的消息如果後至,則會被拋棄。
每個消息帶上一個自增的版本號,將所有接收到的消息存儲下來,每次接收到消息之後使用存儲的日誌重新構建消息的最終狀態。其中使用狀態機:消息輸入狀態機,基於上一個狀態產生一個新的狀態,以此循環往複,最終獲取最終的狀態。
對於一個邏輯、流程較為簡單的業務,設計的時候可以保證在一個狀態下所能夠接收的消息類型沒有交集。即可以保證在當前狀態下不會接收上一個(上上個)狀態下可接收的消息。
消息隊列需要考慮減少反覆投遞帶來的系統開銷,特別是當流量突發的情況下,反覆重試會造成消息隊列堵塞,服務過載等情況,進而引發雪崩效應。
消息投遞重試的問題,如果多次重試後依然投遞失敗,應當修正消息的繼續投遞。
消息隊列需要考慮當消息堆積時產生的問題及影響,若服務是及時性要求比較高的情況下,堆積會造成及時性的問題,若服務是離線服務,則產生的影響比較小。 消息堆積首先需要確認是否是重複消息反覆投遞,若是反覆投遞可參考上文中的減少消息的反覆投遞。
推薦閱讀: