微服務(wù)架構(gòu)的分布式容錯
『看看論文』是一系列分析計算機和軟件工程領(lǐng)域論文的文章,我們在這個系列的每一篇文章中都會閱讀一篇來自 OSDI、SOSP 等頂會中的論文,這里不會事無巨細地介紹所有的細節(jié),而是會篩選論文中的關(guān)鍵內(nèi)容,如果你對相關(guān)的論文非常感興趣,可以直接點擊鏈接閱讀原文。
本文要介紹的是 2019 年 SOSP 期刊中的論文 —— Aegean: Replication beyond the client-server model[^1],這篇論文實現(xiàn)的 Aegean 并不是復(fù)雜的系統(tǒng),它更像是一個工具或者框架,能夠在今天復(fù)雜的微服務(wù)架構(gòu)中保證請求處理的正確性。經(jīng)典的復(fù)制協(xié)議(Replication Protocol),例如:主從復(fù)制、Paxos 和 PBFT 能夠很好地適用于常見的客戶端 / 服務(wù)端模型(Client-Server Model),但是它們卻很難在多服務(wù)的設(shè)置中保證正確性。
在傳統(tǒng)的客戶端 / 服務(wù)端模型中,客戶端的請求往往都是由如下圖所示的一組相同服務(wù)器副本處理的,不同的部分會運行相同的代碼,只是它們的角色可能有所不同,這些副本在處理請求時也基本不會依賴其他的服務(wù);但是在微服務(wù)架構(gòu)中,接收用戶請求的服務(wù)往往只是 HTTP 入口,它會調(diào)用系統(tǒng)中的其他服務(wù)完成用戶期望的功能:

微服務(wù)架構(gòu)的復(fù)雜性來源于服務(wù)之間的大量交互以及網(wǎng)絡(luò)請求的不確定性,調(diào)用路徑上的任何服務(wù)超時或者宕機都可能影響用戶請求的處理,服務(wù)的宕機也可能會造成用戶無法感知請求的結(jié)果、系統(tǒng)處于異常狀態(tài)并無法回滾等問題。
該論文主要做了四部分工作,分別是陳述并定義微服務(wù)架構(gòu)中的現(xiàn)有問題、提出三種用于解決上述問題的技術(shù)、在解決問題的基礎(chǔ)上優(yōu)化系統(tǒng)的性能以及實現(xiàn) Aegean 框架在 TPC-W 基準(zhǔn)測試[^2]下評估 Aegean 的性能,我們在這篇文章中更關(guān)注論文的前兩部分工作:問題陳述以及解決方案。
問題陳述
在這里,我們會設(shè)定一些簡單的前提條件來展示微服務(wù)架構(gòu)中的問題,如下圖所示,客戶端會向服務(wù)端發(fā)送一組請求,這些請求會由服務(wù)端的一組中間服務(wù)(Middle Service)副本處理,中間服務(wù)在處理請求的過程中會調(diào)用后端服務(wù)(Backend Service)的接口,接收請求的中間服務(wù)會包含多個,而后端服務(wù)只會包含一個:

如果客戶端發(fā)起了下單請求,那么中間服務(wù)是處理結(jié)賬請求的服務(wù),而后端服務(wù)是處理轉(zhuǎn)賬請求的服務(wù)?,F(xiàn)實世界中微服務(wù)之間的交互遠遠比上圖展示的復(fù)雜得多,但是這個簡單的模型已經(jīng)可以足夠說明微服務(wù)架構(gòu)中服務(wù)交互對保證正確性帶來的影響了。

主從復(fù)制、類 Paxos 協(xié)議以及預(yù)測執(zhí)行是在分布式系統(tǒng)中保證系統(tǒng)正確性最常見的三種技術(shù),但是這三種技術(shù)在服務(wù)交互的場景下卻不能很好地工作,這里簡單介紹下幾種技術(shù)的缺陷。
主從復(fù)制
在常見的主從復(fù)制算法中,主節(jié)點會負責(zé)處理所有的請求并將狀態(tài)更新的結(jié)果同步到從節(jié)點,當(dāng)從節(jié)點確認了狀態(tài)的更新后,主節(jié)點才會將結(jié)果返回給客戶端。如果我們在主從復(fù)制的模型中引入了中間服務(wù),就會出現(xiàn)如下所示的情況:

當(dāng)主節(jié)點向后端服務(wù)發(fā)送嵌套請求后宕機,后端服務(wù)會接收并處理該請求; 作為后端服務(wù)的觀察者,主節(jié)點并沒有來得及將消息復(fù)制給從節(jié)點就發(fā)生了宕機; 從節(jié)點超時并成為主節(jié)點后會丟失該請求相關(guān)的信息;
在這種情況下,原有的從節(jié)點不知道主節(jié)點處理了什么請求,它可能會重新執(zhí)行嵌套請求、稍有不同的請求甚至可能會向客戶端發(fā)送『商品已經(jīng)售罄』的錯誤消息,然而該用戶的轉(zhuǎn)賬請求已經(jīng)被后端服務(wù)處理了。
這里出現(xiàn)的最根本問題是,主節(jié)點在向后端發(fā)送嵌套請求實際上是通知外部服務(wù)提交變更,然而在發(fā)出請求之前,它卻沒有將自己的狀態(tài)同步到從節(jié)點,防止在故障時發(fā)生狀態(tài)的丟失。
類 Paxos 算法
Paxos 算法和它的變種是目前使用最廣泛的復(fù)制協(xié)議,這些協(xié)議會使用主動復(fù)制機制,也就是先確定請求的順序,然后按照順序執(zhí)行客戶端發(fā)出的所有請求。然而 Paxos 以及變種算法會遇到很多問題,首先,主動復(fù)制機制意味著所有的中間服務(wù)都會收到并處理請求,后端服務(wù)會收到并執(zhí)行多份相同的嵌套請求:

檢測重復(fù)請求并不能解決全部的問題,后端服務(wù)還需要引入響應(yīng)緩存(Reply Cache)保證相同請求可以得到相同的響應(yīng),也就是冪等性;很多交易相關(guān)的服務(wù)都會使用唯一標(biāo)識符來去重并防止重入。
正確性不是 Paxos 算法帶來的唯一問題,在微服務(wù)的架構(gòu)中,線性執(zhí)行請求會對性能造成非常嚴(yán)重地影響,能夠明顯地降低服務(wù)的吞吐量以及服務(wù)的可擴展性。
預(yù)測執(zhí)行
預(yù)測執(zhí)行(Speculative execution)是高并發(fā)提高性能的一種重要工具,使用預(yù)測執(zhí)行的復(fù)制協(xié)議會在一組副本對執(zhí)行順序達成一致之前向下游發(fā)出請求。預(yù)測執(zhí)行在微服務(wù)復(fù)雜的架構(gòu)可能會影響系統(tǒng)的正確性,因為在客戶端服務(wù)器模型中,預(yù)測執(zhí)行需要系統(tǒng)向客戶端能夠隱藏錯誤預(yù)測帶來的不一致狀態(tài),服務(wù)端也需要支持狀態(tài)的回滾:

而在微服務(wù)架構(gòu)中,預(yù)測執(zhí)行會導(dǎo)致問題變得更加復(fù)雜,當(dāng)客戶端通過預(yù)測執(zhí)行調(diào)用中間服務(wù)時,中間服務(wù)會調(diào)用后端服務(wù),客戶端在這時如果突然發(fā)現(xiàn)預(yù)測發(fā)生了錯誤,我們需要級聯(lián)回滾來恢復(fù)狀態(tài),這不僅需要回滾中間服務(wù),還需要回滾后端服務(wù)。
小結(jié)
三種不同的復(fù)制協(xié)議都不能直接適用于包含服務(wù)交互的微服務(wù)架構(gòu),在對三種復(fù)制協(xié)議的研究中,論文提出了微服務(wù)架構(gòu)帶來的三大挑戰(zhàn):

復(fù)制客戶端(Replicated client)— 在客戶端服務(wù)器模型中,客戶端是唯一一個向服務(wù)端發(fā)送請求的機器,然而在微服務(wù)架構(gòu)中,一組復(fù)制的中間服務(wù)可能向其他服務(wù)發(fā)送請求,這些服務(wù)副本發(fā)送的不同請求應(yīng)該在邏輯上被看做單個請求; 嵌套響應(yīng)的持久化(Durability of nested responses)— 當(dāng)中間服務(wù)接收到其他服務(wù)返回的響應(yīng)時,它應(yīng)該先保證該響應(yīng)被足夠的副本確認,這樣才能在宕機時保證其他副本能夠正確返回響應(yīng); 預(yù)測執(zhí)行(Speculative execution)— 在預(yù)測狀態(tài)下,中間服務(wù)不能發(fā)出任何的嵌套請求,這可能會導(dǎo)致后端服務(wù)提交未經(jīng)確認的狀態(tài);
這些都是微服務(wù)架構(gòu)中常見的問題,對服務(wù)端開發(fā)稍微有經(jīng)驗的讀者應(yīng)該對上述三大挑戰(zhàn)非常熟悉,這篇論文只是使用了更加正式的模型進行了歸納和總結(jié)。
解決方案
為了解決微服務(wù)架構(gòu)中最常見的三個挑戰(zhàn),論文中引入了服務(wù)器墊片(Server Shim)、響應(yīng)持久化(Response Durability)和改良預(yù)測(Taming Speculation)三種技術(shù)分別解決復(fù)制客戶端、嵌套響應(yīng)的持久化和預(yù)測執(zhí)行幾個問題。

服務(wù)器墊片
當(dāng)中間服務(wù)使用主動的復(fù)制協(xié)議時,后端服務(wù)會收到大量的重復(fù)請求,為了較少冗余請求的處理并保證所有的請求都得到相同的響應(yīng),我們可能需要對后端服務(wù)進行修改;然而修改所有的后端服務(wù)是比較大的工程,我們更希望對現(xiàn)有的架構(gòu)造成較小的影響,所以論文提出了如下所示的簡單抽象:

服務(wù)器墊片會與后端服務(wù)一起執(zhí)行,中間服務(wù)發(fā)出的所有請求會先經(jīng)過服務(wù)器墊片再到后端服務(wù),墊片主要會負責(zé)以下兩大功能:
接收請求:服務(wù)器墊片會認證其他服務(wù)發(fā)送的請求并確認發(fā)送方的身份,在收到請求時也不會立刻交給下游的后端服務(wù)處理,而是會等待一組(quorum)來自副本客戶端的相同請求,收到足夠的請求后會將請求轉(zhuǎn)發(fā)給后端并忽略剩余副本的請求; 返回響應(yīng):當(dāng)后端服務(wù)生成響應(yīng)之后,服務(wù)器墊片會負責(zé)將響應(yīng)廣播給正在等待的一組客戶端,同時為了防止消息的丟失,墊片還會為每個客戶端持久化響應(yīng)緩存;
響應(yīng)持久化
在客戶端服務(wù)器模型中,副本服務(wù)的輸入僅僅來源于客戶端的輸入,而在微服務(wù)的架構(gòu)中,嵌套請求的響應(yīng)也是中間服務(wù)的輸入;因為傳統(tǒng)的復(fù)制協(xié)議需要所有的輸入都必須持久化地存儲到日志中,所以在多服務(wù)的設(shè)置中,我們需要保證來自客戶端的輸入和嵌套響應(yīng)都存儲在日志中,只有在持久化日志之后,我們才可以向客戶端或者后端服務(wù)提交變更。
改良預(yù)測
在多服務(wù)的設(shè)置中,如果一組中間服務(wù) A、B 和 C 在達成一致之前,A 服務(wù)就進行了預(yù)測執(zhí)行調(diào)用下游服務(wù)提交變更,那么當(dāng) B 和 C 發(fā)現(xiàn)當(dāng)前請求無效要求回滾時,我們就在系統(tǒng)中引入了不一致的狀態(tài)。為了解決預(yù)測可能帶來的不確定性問題,我們會在請求執(zhí)行的不同階段引入屏障來對齊多個線程或者副本之間的狀態(tài):

當(dāng)請求執(zhí)行遇到屏障時,它會等待其他副本直到它們的狀態(tài)發(fā)生收斂,只有在狀態(tài)達成一致之后,它們才會執(zhí)行具有副作用的任務(wù),例如:調(diào)用下游服務(wù)或者返回響應(yīng)。
總結(jié)
作為 SOSP 中的論文,Aegean: Replication beyond the client-server model 中介紹的問題與我們在工程上遇到的非常相似,雖然它提出了一些解決方案,但是作者認為這些解決方案太過于正式和學(xué)術(shù)。
我們在工程上往往會使用更加粗糙和容易實現(xiàn)的方式解決服務(wù)的重入、冪等以及錯誤恢復(fù)的問題,例如:利用唯一標(biāo)識符去重、通過 MySQL 或者 Redis 存儲請求的響應(yīng)、在后臺啟動線程重試失敗的任務(wù),這些方法雖然不能保證強一致性,但是它們在多數(shù)場景下都是夠用的,只有當(dāng)一致性和正確性成為較強的需求時,再考慮文中的幾種稍微復(fù)雜的技術(shù)也是可以的。

更多精彩文章
