微服務架構:拆分單體應用的難點

技術瑣話發表於2022-12-05

拆分單體應用為服務的難點

從表面上看,透過定義與業務能力或子域相對應的服務來建立微服務架構的策略看起來很簡單。但是,你可能會遇到幾個障礙:

  • 網路延遲。

  • 同步程式間通訊導致可用性降低。 

  • 在服務之間維持資料一致性。

  • 獲取一致的資料檢視。

  • 上帝類阻礙了拆分。

讓我們來看看每個問題,先從網路延遲開始。

網路延遲

網路延遲是分散式系統中一直存在的問題。你可能會發現,對服務的特定分解會導致兩個服務之間的大量往返呼叫。有時,你可以透過實施批處理API在一次往返中獲取多個物件,從而將延遲減少到可接受的數量。但在其他情況下,解決方案是把多個相關的服務組合在一起,用程式語言的函式呼叫替換昂貴的程式間通訊。

同步程式間通訊導致可用性降低

另一個需要考慮的問題是如何處理程式間通訊而不降低系統的可用性。例如,實現createOrder()操作最常見的方式是讓Order Service使用REST同步呼叫其他服務。這樣做的弊端是REST這樣的協議會降低Order Service的可用性。如果任何一個被呼叫的服務處在不可用的狀態,那麼訂單就無法建立了。有時候這可能是一個不得已的折中,但是在第3章中學習非同步訊息之後,你就會發現其實有更好的辦法來消除這類同步呼叫產生的緊耦合並提升可用性。

在服務之間維持資料一致性

另一個挑戰是如何在某些系統操作需要更新多個服務中的資料時,仍舊維護服務之間的資料一致性。例如,當餐館接受訂單時,必須在Kitchen Service和Delivery Service中同時進行更新。Kitchen Service會更改Ticket的狀態。Delivery Service安排訂單的交付。這些更新都必須以原子化的方式完成。

傳統的解決方案是使用基於兩階段提交(two phase commit)的分散式事務管理機制。但正如你將在第4章中看到的那樣,對於現今的應用程式而言,這不是一個好的選擇,你必須使用一種非常不同的方法來處理事務管理,這就是Saga。Saga是一系列使用訊息協作的本地事務。Saga比傳統的ACID事務更復雜,但它們在許多情況下都能工作得很好。Saga的一個限制是它們最終是一致的。如果你需要以原子方式更新某些資料,那麼它必須位於單個服務中,這可能是分解的障礙。

獲取一致的資料檢視

分解的另一個障礙是無法跨多個資料庫獲得真正一致的資料檢視。在單體應用程式中,ACID事務的屬性保證查詢將返回資料庫的一致檢視。相反,在微服務架構中,即使每個服務的資料庫是一致的,你也無法獲得全域性一致的資料檢視。如果你需要一些資料的一致檢視,那麼它必須駐留在單個服務中,這也是服務分解所面臨的問題。幸運的是,在實踐中這很少帶來真正的問題。

上帝類阻礙了拆分

分解的另一個障礙是存在所謂的上帝類。上帝類是在整個應用程式中使用的全域性類。上帝類通常為應用程式的許多不同方面實現業務邏輯。它有大量欄位對映到具有許多列的資料庫表。大多數應用程式至少有一個這樣的上帝類,每個類代表一個對領域至關重要的概念:銀行賬戶、電子商務訂單、保險政策,等等。因為上帝類將應用程式的許多不同方面的狀態和行為捆綁在一起,所以將使用它的任何業務邏輯拆分為服務往往都是一個不可逾越的障礙。

Order類是FTGO應用程式中上帝類的一個很好的例子。這並不奇怪:畢竟FTGO的目的是向客戶提供食品訂單。系統的大多數部分都涉及訂單。如果FTGO應用程式具有單個領域模型,則Order類將是一個非常大的類。它將具有與應用程式的許多不同部分相對應的狀態和行為。圖6顯示了使用傳統建模技術建立的Order類的結構。

微服務架構:拆分單體應用的難點

圖6 Order這個上帝類承載了太多的職責

如你所見,Order類具有與訂單處理、餐館訂單管理、送餐和付款相對應的欄位及方法。由於一個模型必須描述來自應用程式的不同部分的狀態轉換,因此該類還具有複雜的狀態模型。在目前情況下,這個類的存在使得將程式碼分割成服務變得極其困難。

一種解決方案是將Order類打包到庫中並建立一箇中央Order資料庫。處理訂單的所有服務都使用此庫並訪問訪問資料庫。這種方法的問題在於它違反了微服務架構的一個關鍵原則,並導致我們特別不願意看到的緊耦合。例如,對Order模式的任何更改都要求其他開發團隊同步更新和重新編譯他們的程式碼。

另一種解決方案是將Order資料庫封裝在Order Service中,該服務由其他服務呼叫以檢索和更新訂單。該設計的問題在於這樣的一個Order Service將成為一個純資料服務,成為包含很少或沒有業務邏輯的貧血領域模型(anemic domain model)。這兩種解決方案都沒有吸引力,但幸運的是,DDD提供了一個好的解決方案。

更好的方法是應用DDD並將每個服務視為具有自己的領域模型的單獨子域。這意味著FTGO應用程式中與訂單有關的每個服務都有自己的領域模型及其對應的Order類的版本。Delivery Service是多領域模型的一個很好的例子。如圖7所示為Order,它非常簡單:取餐地址、取餐時間、送餐地址和送餐時間。此外,DeliveryService使用更合適的Delivery名稱,而不是稱之為Order。

微服務架構:拆分單體應用的難點

圖7 Delivery Service的領域模型

Delivery Service對訂單的任何其他屬性不感興趣。

Kitchen Service有一個更簡單的訂單檢視。它的Order版本就是一個Ticket(後廚工單)。如圖8所示,Ticket只包含status、requestedDeliveryTime、prepareByTime以及告訴餐館準備的訂單項列表。它不關心消費者、付款、交付等這些與它無關的事情。

微服務架構:拆分單體應用的難點

圖8 Kitchen Service的領域模型

Order Service具有最複雜的訂單檢視,如圖9所示。即使它有相當多的欄位和方法,它仍然比原始版本的那個Order上帝類簡單得多。

微服務架構:拆分單體應用的難點

圖9 Order Service的領域模型

每個領域模型中的Order類表示同一Order業務實體的不同方面。FTGO應用程式必須維持不同服務中這些不同物件之間的一致性。例如,一旦OrderService授權消費者的信用卡,它必須觸發在Kitchen Service中建立Ticket。同樣,如果Kitchen Service拒絕訂單,則必須在Order Service中取消訂單,並且為客戶退款。在第4章中,我們將學習如何使用前面提到的事件驅動機制Saga來維護服務之間的一致性。

除了造成一些技術挑戰以外,擁有多個領域模型還會影響使用者體驗。應用程式必須在使用者體驗(即其自己的領域模型)與每個服務的領域模型之間進行轉換。例如,在FTGO應用程式中,向消費者顯示的Order狀態來自儲存在多個服務中的Order資訊。這種轉換通常由API Gateway處理,將在第8章中討論。儘管存在這些挑戰,但在定義微服務架構時,必須識別並消除上帝類。

我們現在來看看如何定義服務API。

6 定義服務API

到目前為止,我們有一個系統操作列表和一個潛在服務列表。下一步是定義每個服務的API:也就是服務的操作和事件。存在服務API操作有以下兩個原因:首先,某些操作對應於系統操作。它們由外部客戶端呼叫,也可能由其他服務呼叫。另次,存在一些其他操作用以支援服務之間的協作。這些操作僅由其他服務呼叫。

服務透過對外發布事件,使其能夠與其他服務協作。第4章將描述如何使用事件來實現Saga,這些Saga可以維護服務之間的資料一致性。第7章將討論如何使用事件來更新CQRS檢視,這些檢視支援有效的查詢。應用程式還可以使用事件來通知外部客戶端。例如,可以使用WebSockets將事件傳遞給瀏覽器。

定義服務API的起點是將每個系統操作對映到服務。之後確定服務是否需要與其他服務協作以實現系統操作。如果需要協作,我們將確定其他服務必須提供哪些API才能支援協作。首先來看一下如何將系統操作分配給服務。

把系統操作分配給服務

第一步是確定哪個服務是請求的初始入口點。許多系統操作可以清晰地對映到服務,但有時對映會不太明顯。例如,考慮使用noteUpdatedLocation()操作來更新送餐員的位置。一方面,因為它與送餐員有關,所以應該將此操作分配給Courier Service。另一方面,它是需要送餐地點的DeliveryService。在這種情況下,將操作分配給需要操作所提供資訊的服務是更好的選擇。在其他情況下,將操作分配給具有處理它所需資訊的服務可能是有意義的。表4顯示了FTGO應用程式中的哪些服務負責哪些操作。

表4 FTGO應用程式的系統操作對映到具體的服務

微服務架構:拆分單體應用的難點

把操作分配給服務後,下一步是確定在處理每一個系統操作時,服務之間如何互動。

確定支援服務協作所需要的API

某些系統操作完全由單個服務處理。例如,在FTGO應用程式中,Consumer Service完全獨立地處理createConsumer()操作。但是其他系統操作跨越多個服務。處理這些請求之一所需的資料可能分散在多個服務周圍。例如,為了實現createOrder()操作,Order Service必須呼叫以下服務以驗證其前置條件並使後置條件成立:

  • Consumer Service:驗證消費者是否可以下訂單並獲取其付款資訊。

  • Restaurant Service:驗證訂單行專案,驗證送貨地址和時間是否在餐廳的服務區域內,驗證訂單最低要求,並獲得訂單行專案的價格。

  • Kitchen Service:建立Ticket(後廚工單)。

  • Accounting Service:授權消費者的信用卡。

同樣,為了實現acceptOrder()系統操作,Kitchen Service必須呼叫Delivery Service來安排送餐員交付訂單。表2-3顯示了服務、修訂後的API及協作者。為了完整定義服務API,你需要分析每個系統操作並確定所需的協作。

表5 服務、修訂後的API及協作者

微服務架構:拆分單體應用的難點

微服務架構:拆分單體應用的難點

總結

  • 微服務中的服務是根據業務需求進行組織的,按照業務能力或者子域,而不是技術上的考量。

  • 有兩種分解模式:

        按業務能力分解,其起源於業務架構。

        基於領域驅動設計的概念,透過子域進行分解。

  • 可以透過應用DDD併為每個服務定義單獨的領域模型來消除上帝類,正是上帝類引起了阻礙分解的交織依賴項。

本文摘自《微服務架構設計模式》,經出版方授權釋出。

來自 “ ITPUB部落格 ” ,連結:http://blog.itpub.net/31562044/viewspace-2661500/,如需轉載,請註明出處,否則將追究法律責任。

相關文章