分佈式系統架構(或稱微服務架構)【1】是由多個小型的服務(微服務)組成單一系統的架構風格,既然是多個服務構成,必然涉及到服務間相同通信以完成特定的功能的情況。服務間通信的方式除了參考單體應用本地方法調用衍生出來的程過程調用(Remote Procedure Call)這種最主要的方式外,還有消息投遞、數據共享、分佈式鎖等,他們都是參考自進程內和進程間(IPC)通信方法,在跨進程間通信場景也發揮了重要的作用。這些通信方法有各自的特點及適用場景,接下來我來一一介紹。
遠程過程調用
之所以遠程過程調用是分佈式系統主要通信方法,是因為在單體應用架構時代,就是程序都部署在一個進程的時候,程序開發的主要工作就是編寫各種方法(或稱函數)並相互調用,方法是實現業務邏輯主要手段和工具。以方法為核心的編程思維也自然而然的延續到分佈式架構(或稱微服務架構)時代,部署在不同進程的程序之間依然採用方法調用的方式進行交互,只不過從本地方法調用變為遠程過程調用,雖然技術實現不同但是使用目標和方式一致。
雙向同步調用
方法調用無論是本地還是遠程,本質上是一種同步交互方式,為什麼説這麼説呢?我們先來拆解一下方法調用的流程:
- 程序調用某函數,然後函數執行
- 程序等待,然後函數反饋結果
- 控制權返回給程序,然後程序繼續處理
之所以説方法調用是一種同步交互方式,關鍵在於第二步中程序需要等待函數執行完畢才能繼續執行後續流程,等待就意味着當前線程會被阻塞直到方法返回結果。同步也是一種符合人類直覺的編程思維,串行化的流程也更加容易理解。同步模式很適合需要根據返回結果作為程序後續流程執行依據的場景,例如參數判斷、前置條件等。
可靠性
人們容易忽視的一點是在分佈式環境中同步方法調用可能降低服務的可用性。以當前的技術手段通過各種RPC框架已經完全屏蔽了RPC底層網絡通信的實現細節,我們完全可以像調用本地方法一樣調用遠程方法,這就給了我們一個錯覺——遠程方法也像本地方法一樣可靠。如果你也這麼想就是被技術便利性矇蔽了雙眼,完全忽略了網絡抖動、延時、分區、不可達、進程異常、服務宕機等各種意料之外但是一定會出現的異常情況。如果一個服務所依賴的服務產生異常,同步方法調用會將異常傳遞給調用者服務,依賴的服務越多,服務的可靠性越低。
如何提高可用性
如何解決同步方法導致的可靠性降低問題呢?克里斯·理查森在《微服務架構設計模式》中提出了一種使用異步通信的解決方案(詳情見下圖):
簡單説就是所有服務的同步方法調用都採用異步消息(消息我們會在後面介紹)的方式代替,一次方法調用需要兩條消息,一條由客户端發送給服務器端代表方法請求,另一條有服務器端發送給客户端代表方法的響應。各個服務間都是通過異步消息傳遞信息,不會等待消息返回結果。因為消息可以暫存於消息中間件的隊列中,即使後端服務因為網絡原因或者服務故障暫時離線,消息也不會消失,等待服務迴歸後可以重新處理。通過這種全異步交互方式確實可以提升系統整體的彈性,不會因為局部異常導致整體不可用。但是筆者認為這種方式是本質只是提供了一種等待異常恢復的能力,而等待過程中的系統的不確定性並沒有解決。
響應時長不確定
系統的業務邏輯大部分都是同步行為,同步有一個顯著的特徵——既方法調用者需要被調用服務返回一個明確結果,異步模式下這個結果返回的時間有可能被故障無限延長,這種反饋時間的不確定是無法接受,大部分情況下寧可要個失敗的結果也不能無限期等一個成功的結果(類似於CAP理論中AP系統實現)。
返回狀態不確定
對於這個問題作者也提出解決方案,可以先創建一箇中間態的訂單並返回給用户,訂單依賴的用户和餐廳服務依然採取異步消息通信,待依賴服務返回消息後再更新訂單狀態為可用的確定狀態。雖然返回了一箇中間態的訂單,但是這個訂單是不確定的並且也是不可用的,後續的其他功能都沒有辦法基於中間態訂單開展。一旦出現所依賴服務異常導致訂單狀態無法及時更新,只能等到故障恢復後才能消除訂單狀態的不確定性,這種訂單狀態的長時間不確定性也是無法接受的。
服務降級
如果同步轉異步的方式能暫緩異常影響但是會帶來了結果不確定性,我們不妨換一個思路。既然異常無法避免【2】,何不坦然面對並且將注意力放到異常發生之後的如何處理上。比如如果用户服務故障無法返回結果,我們可以選擇將異常返回給客户端並告知用户稍後處理,或者可以嘗試從緩存中讀取我們需要的信息,或者如果獲取用户信息的流程非必須流程我們也可以忽視異常並跳過這個流程。以上這些根據業務需要在發生異常後的處理方式就是服務治理中常説的降級策略,合理應用降級策略可以異常發生之後最大限度的保持系統的可確定性。在不犧牲確定性的前提下保證可用性才是我們的最終目標也是最優的選擇。
性能
RPC框架封裝遠程過程調用後讓我們產生的第二個錯覺是遠程過程調用和本地方法調用有相同的性能,但是RPC框架技術再先進也不能抹平網絡通信所帶來的性能鴻溝,遠程過程與本地方法調用數量級上的性能差異就決定了他們在設計模式和使用方式上必然有所區分。
合理的方法粒度
設計合理的方法顆粒度在分佈式系統中尤其重要,對於分佈式系統服務對外暴露接口必須可以支撐完整的業務場景,避免暴露過於細粒度的方法。馬丁·福勒在《企業應用架構模式》一書中就提出“不要分佈你的對象”(Don't distribute your objects)這一分佈式對象設計原則,明確指出不要將對象的方法作為服務接口對外暴露。如果服務接口都是對象方法維度,一來會使服務接口碎片化,實現一個完整的業務邏輯要調用不同服務的接口,二來也範圍劃分不清晰的服務間調用關係也會相對複雜。
合理的接口粒度設計同樣有助於降低遠程過程調用的次數,顯著提升性能。這裏要注意不能矯枉過正,不能僅僅為了提升性能而應將不同的業務邏輯強行合併到一個接口中去。微服務範圍劃分的第一要務是通過一系列高內聚、低耦合的小型服務支撐完整的業務領域。
優化的依賴路徑
如果不合理的方法粒度會造成業務邏輯對應服務依賴太多,那麼不合理的依賴路徑會造成業務邏輯對應的服務調用鏈過長,前者完成一個業務功能調用關係是服務A——>服務B、服務C、服務D,後者就是是服務A——>服務B——>服務C——>服務D。和上一小節的問題類似,調用路徑過長本質上也是由服務範圍劃分不清晰的導致的。如何劃分微服務範圍在此我就不再贅述,感興趣的讀者可以去看我的另一篇文章《如何界定微服務範圍》。
避免重複調用
你可能覺得這個要求很奇怪,為什麼要重複調用方法呢。那我問你一個問題,如果你是一個Java程序員在程序中你有沒有多次調用user對象getName()方法呢,我想答案一定是有的。我們往往不會為簡單的方法設置局部變量而在使用的地方直接調用,這對於本地方法沒有問題,而對於遠程方法就會產生性能問題,正確的做法使用局部變量存儲遠程調用的返回結果避免重複調用,當然根據業務邏輯緩存的範圍不僅僅侷限於局部變量,還可以是對象屬性、靜態變量、本地緩存甚至可以是集中式緩存服務。
另一個重複調用的場景就是在循環中重複調用遠程方法,改進措施也很簡單,就是讓服務端提供批量方法,將多次循環調用改為一次批量調用。方法的重複調用相較於上面兩小節介紹的問題更容易發現和解決,只要你在開發過程中時刻提醒自己遠程過程調用有性能成本就可以避免很多問題。
技術選型
除了以上介紹的在接口設計和使用上面的改進外,還可以在技術選型時候選擇偏向性能的RPC框架。當今RPC框架眾多,它們在簡單、普適和高性能方面各有側重,注重性能表現的RPC框架通常使用專用二進制格式序列化器、在序列化效率上較XML、JSON等字符格式有顯著提升,在網絡傳輸上方面也會選擇高性能通信協議或者自研協議,例如gRPC是使用HTTP2.0作為傳輸協議,HTTP2.0支持多路複用、頭部壓縮和傳輸優先級等功能非常適合大批量、短時間、小數據傳輸場景。而Thrift和Dubbo都基於TCP自研高性能傳輸協議,也非常適合高併發分佈式服務調用場景。
消息投遞
消息投遞不是分佈式服務間專屬通信方式。Java語言原生支持經典觀察者模式(java.util.Observable + java.util.Observer)、還可以通過java.util.concurrent中提供的各種隊列實現簡單的消息傳輸,或者使用Google Guava EventBus和Spring Event等第三方法框架實現更加複雜的功能。消息隊列也是同主機進程間(IPC)的通信標準方法之一,有着廣泛的使用場景。擴展到不同主機不同進程間——暨分佈式服務間的場景後,除了實現的底層技術手段不同之外,消息這種通信方式的概念和適用場景並沒有改變。
單向異步通信
無論是進程內還是跨進程消息通信,消息本質上是一種單向異步通信方式,消息的通信流程如下:
- 發送方(生產者、被觀察者)發送消息後返回
- 接收方(消費者、觀察者)接收消息並處理
觀察上面的流程可見,發送方發送消息後沒有等待的過程,接收方接收消息後沒有返回的動作,所以消息一種單向(只發不回)的異步(不等待結果)通信方式。這也是消息投遞與方法調用最大的區別。
性能
一説到異步,通常會聯想到性能提升。如果使用得當,異步確實是提升系統整體性能的重要手段。等待即意味着線程阻塞、線程阻塞即意味着浪費CPU性能,消除阻塞或者説不在等待,讓所有流程都可以並行運行,可以充分的利用CPU時鐘週期,降低系統的響應時長並提高吞吐量。但是使用異步提升性能的關鍵點就流程是否可並行執行。
串行場景
筆者以大家都熟悉的電商平台下單流程為例,下面流程為簡化版本示例,不必糾結流程完整性和合理性:
- 審核訂單信息及用户權限(合規服務)
- 扣減用户賬户金額完成支付(支付服務)
- 扣減庫存並標記發貨(物流服務)
對訂單信息和用户權限的審核可以規避無效的訂單和非授權的行為,它是整個下單流程的前置條件,審核完成後先支付後發貨也是電商通用流程,最後一步通過物流服務扣減庫存併發貨,顯然這三個步驟是有順序要求的串行流程,即每一步都要等待上一步完成後根據上一步返回的結果決定是否繼續進行。
對於這種流程,最好的通信方式就是採用遠程過程調用與相關流程所對應的服務進行同步通信。如果硬要使用上文中《微服務架構設計模式》所提出的異步通信的方式(流程如下),不僅後面臨如何處理消息回調結果、如何保證消息執行順序等服務編排方面的複雜問題,還要處理各類異常場景。除了實現複雜外,性能上提升也很有限甚至會因為流程處理不當而下降。所以硬要把異步技術應用與同步的場景並不是一個理性的選擇。
-
發送消息審核訂單信息及用户權限(合規服務)
- 等待消息結果
-
發送扣減用户賬户金額完成支付(支付服務)
- 等待消息結果
-
發送扣減庫存並標記發貨(物流服務)
- 等待消息結果
並行場景
我們調整一下上文中下單的流程及步驟
- 審核訂單信息及用户權限(合規服務)
- 查看商品庫存是否充足(庫存服務)
- 查看用户賬號餘額是否充足(支付服務)
- 扣減用户賬户金額完成支付(支付服務)
- 扣減庫存並標記發貨(物流服務)
對於前三步都是檢查流程,無需一個一個的串行執行,我們可以向合規服務、庫存服務、支付服務發送消息,然後等待所有消息返回後根據結果決定是否進行後續的步驟。具體步驟如下:
-
發送異步消息:
- 發送審核訂單信息及用户權限(合規服務)消息、發送查看商品庫存是否充足(庫存服務)消息、發送查看用户賬號餘額是否充足(支付服務)消息
- 阻塞等待所有相關服務收到消息後返回結果,根據結果決定後續操作。
這樣做確實會因為並行執行而帶來性能提升,但是因為我們還是需要等待返回結果所以本質上只是將三次串行的等待打包成一次並行的等待。雖然可以通過消息方式實現異步,但是還是要想辦法處理返回消息(較上文場景無需關係返回順序),從代碼實現難易程度來説這也不是消息投遞的最適合場景。上面並行場景通過多線程(或者協程)異步發起遠程調用實現起來更加簡單高效。
解耦
服務間耦合
上文中描述的訂單場景中的流程無論是否可以異步執行本質上調用者執行流程中調用的時候都是要等待被調用者返回結果(區別在於串行有序等待還是並行批量等待)。當服務間需要通過請求-響應方式相互通信時候,我們説服務間相互耦合,或者説一個服務依賴於另一個服務。服務間相互依賴是由業務需求決定,訂單服務就是需要合規服務提供審核能力,也需要支付服務提供支付能力,這本就是服務間正常的交互行為。但是依賴為分佈式系統帶來了兩個技術問題:
- 第一個問題就是服務發現問題,就是調用者需要知道被調用服務在哪裏,怎麼調用,一旦被調用服務地址變化調用者也要相應調整。
- 第二個就是可用性問題,一個服務依賴的其他服務越多,受到其他服務異常牽連的概率越大,每一個被依賴服務產生異常都直接導致所在服務不可用。
這兩個問題都可以通過消息投遞解決,更準確的説法是使用消息隊列中間件來解決。在更進一步介紹前,我先要明確一下消息投遞和消息隊列中間件的各自的定義及關係,避免讀者混淆。消息投遞是一種單向異步的通信方式,而消息隊列中間件是實現消息投遞的一種技術手段。在分佈式系統中消息投遞有兩種實現方式,無代理模式和有代理模式,無代理模式就是消息直接由發送者向接收者傳輸,中間不依賴任何第三方組件。而有代理模式就是使用消息隊列中間件作為中轉站在生產者和消費者間進行消息傳輸。顯然無代理模式實現方式更加直接,但由於需要感知消息接收者、消息緩存異常丟失等問題並不常用,主流的消息投遞的實現方式還是使用消息隊列中間件。
適合場景
適合場景
通過使用消息隊列作為消息投遞的媒介,就可以解決上文中所説的服務間耦合或者説服務依賴所導致的問題,同時還帶來了業務靈活性的優勢,具體説:
- 通過消息隊列,發送者可以無需關心接收的服務誰是?在哪裏?只要向消息隊列發送消息,消息隊列會將消息傳輸到訂閲消息的接收服務,這就解決了服務發現問題。
- 消息隊列還可以緩存消息,如果消息接收者因為網絡分區或者服務故障暫時離線,消息會被保留到服務下次上線後再次投遞,無需在代碼邏輯中增加重試等異常處理邏輯,提高了系統整體的可靠性。
- 消息的接收者可以任何添加刪除,無需消息發送者感知和調整,這就給業務調整帶來的很大的靈活性。試想如果採用遠程調用的方式,任何的業務流程變動都需要重新修改流程代碼後部署上線,而使用消息隊列的話,原有流程代碼完全無需調整,所有變動都發生在消息隊列後面的消息接收端。
但是使用消息投遞作為服務間通信手段來解決耦合問題是有前提條件的,不是什麼場景都使用的,否則就像上文中介紹的《微服務架構設計模式》通過將同步調用改為異步消息來提高可用性的方式一樣費力不討好。任何一項技術手段都不是銀彈,不可能包治百病。所以什麼才是消息投資的適配場景呢?我們還用電商下單流程舉例:
- 審核訂單信息及用户權限(合規服務)
- 扣減用户賬户金額完成支付(支付服務)
- 扣減庫存並標記發貨(物流服務)
- 為用户增加積分(積分服務)
- 發送短信和郵件通知用户下單成功(消息服務)
我們最後新增了兩個流程(放在最後只是方便讀者區分,順序並不重要),仔細觀察這兩個流程就會發現,這兩個流程執行的結果都不影響下單操作主幹流程走向,無論用户積分是否添加成功或者短信郵件是否發送成功,下單操作已經完成了。這樣的流程我們稱之為分支流程,因為不需要分支流程的執行結果以判斷主幹流程流轉,所以分支流程就非常適合消息投遞這種單向異步的通信方式。借用消息隊列的能力,我們在下單完成後我們將下單成功的消息發送到消息隊列中,由下單成功消息的訂閲者接收消息並完成相應的業務流程,改造後的流程如下:
下單流程:
- 審核訂單信息及用户權限(合規服務)
- 扣減用户賬户金額完成支付(支付服務)
- 扣減庫存並標記發貨(物流服務)
- 向消息隊列發送下單成功的消息
訂閲下單成功消息的服務及行為:
- 積分服務:為消費者增加積分
- 消息服務:向用户發送短信和郵件
- 審計服務:提取訂單信息以備日後審計
- 營銷服務:記錄用户訂單數據,優化用户商品推薦模型
- 。。。
改造的下單流程去掉了最後兩個流程變為向消息隊列發送消息,流程不在直接依賴(或者耦合)積分服務和消息服務,只依賴消息隊列中間件,這就解決了服務發現和可靠性問題。訂閲下單成功消息的服務由原來的兩個增加到了四個,還可以繼續擴展,下單操作完全不知道消息隊列後面有哪些服務,這就是消息隊列如何提升了業務的靈活性的表現。
讀到這裏讀者可能還有疑問,添加用户積分真的是一個無需結果的分支流程嗎?也許有些業務要求下單、扣減庫存、添加積分都是事務性操作,要麼全部成功或者全部失敗。這是有可能的,雖然筆者認為添加積分可以作為分支流程不必納入下單事務中,但是一切開發活動都是要以實際的業務場景為準,所以如果扣減積分不屬於下單事務中。那麼消息投遞就不再適合,筆者更建議使用同步調用結合Saga來實現這樣的場景。
異常處理
讀者可能還有一個疑問,萬一添加用户積分操作失敗了,通過消息的方式我們又不需要感知結果,那麼如何處理異常呢。這裏就要分情況考慮:
- 首先,既然分支流程已經被排除在主幹流程之外,那麼主幹流程也就無需關心分支流程的異常處理,異常處理應該分支流程相關服務團隊例如積分服務、者消息服務、審計服務等開發團隊負責處理,訂單服務無需關注,也就是説不是不處理異常,而是誰負責執行誰處理異常。
- 其次,根據流程的重要程度,可以選擇靜默處理、定期重試等多種異常處理方式。
- 最後,如果對一致性要求較高也可以由分支服務定期向主幹服務發起數據對賬請求,核對自身數據和主幹流程服務數據結果是否一致。
數據共享
數據共享就是分佈式系統中不同的服務間通過共享數據的方式進行數據傳輸。共享數據的媒介可能是文件系統的某個文件、數據庫中某個表、對象存儲某個文件等第三方。由一方向媒介中寫入數據,然後另一方從媒介中讀取數據。可以通過時間驅動,例如寫入方每天下午3點之前將當天的財務報表數據寫入FTP中某個文件中,讀取方按照約定時間每天下午3點半下載並讀取文件。也可以事件驅動,寫入財務報表後發送一條“數據已寫入”消息,訂閲者手坳消息後讀取文件並處理。後者的時效性好於前者。本質上説數據共享也是一種異步通信方法,發送這不依賴接收者獨立完成數據寫入工作,也不關心文件是否被讀取和處理的結果,只要約定好數據的格式,所有感興趣的服務都可以讀取共享數據並處理。所以雖然不一種主流的傳輸方式,但是在大規模數據傳輸且時效性要求不高的場景都可以作為一種方案備選。使用者數據共享的方式有一點需要注意,就是共享數據的保存時效,需要有過期清理策略和機制,避免長時間寫入大量數據造成存儲介質寫滿。
分佈式鎖
分佈式所其實不符合服務間通信方式狹義場景的定義,它主要的使用場景是進程間同步和互斥,避免不同進程同時訪問相同鎖定範圍。但是鑑於Linux進程間通信(IPC)方式有信號量這一和分佈式類似的交互方式,所以筆者出於邏輯完整性考慮將其納入文章範圍。簡單的分佈式鎖可以使用MySQL、Redis等數據庫類中間件實現,但是鑑於這些中間件單節點可用性問題和分佈式鎖看似簡單但是實則複雜的細節和異常處理,筆者還是建議使用ETCD或ZooKeeper等支持CP(CAP WHITOUT A)模型且有分佈式鎖原生支持的專業中間件。
寫在最後
本文介紹了四種常用的分佈式系統服務間通信的方法,其中遠程過程調用和消息投遞是應用最多的兩種方法,不應該只是關注於它們之間同步和異步的區別,還要從更多方面匹配它們各自的適用場景,前者適用於同步、串行、需要返回結果或者有事務要求的主幹流程場景中,而後者更適合無需結果的分支流程中。數據共享雖然適配場景不多,但是如果在沒有太高實時性要求的異步大批量數據傳輸場景下,也是一種不錯的選擇。將技術與合適的場景匹配不僅僅能發揮技術的最大功效,實現方法也更加簡單高效。
備註
【1】分佈式系統和微服務系統概念相同,兩個名詞會在文中中混用交替使用。
【2】通過網絡冗餘、服務冗餘、存儲冗餘、負載均衡、容災備份、兩地三中心等各種技術手段只能降低異常發生的概率。
【3】可見性為簡化模型,暫時不考慮CAP原理一致性問題。