1. 程式人生 > >如何高效實現從單體架構向微服務架構的過渡?

如何高效實現從單體架構向微服務架構的過渡?

你很有可能正在處理大型複雜的單體應用程式,每天開發和部署應用程式的經歷都很緩慢而且很痛苦。微服務看起來非常適合你的應用程式,但它也更像是一項遙不可及的必殺技。如何才能走上微服務架構的道路?下面將介紹一些策略,幫你擺脫單體地獄,而無須從頭開始重寫你的應用程式。

通過開發所謂的絞殺者應用程式(strangler application),可以逐步將單體架構轉換為微服務架構。絞殺者應用程式的想法來自絞殺式藤蔓,這些藤蔓在雨林中生長,它們包圍繞樹木生成,甚至有時會殺死樹木。絞殺者應用程式是一個由微服務組成的新應用程式,通過將新功能作為服務,並逐步從單體應用程式中提取服務來實現。隨著時間的推移,當絞殺者應用程式實現越來越多的功能時,它會縮小並最終消滅單體應用程式。開發絞殺者應用程式的一個重要好處是,與宇宙大爆炸式的徹底重寫不同,它可以立刻落地,更快為企業提供價值。

有三種主要策略可以實現對單體的“絞殺”,並逐步用微服務替換之:

1)將新功能實現為服務。

2)隔隔表現層和後端。

3)通過將功能提取到服務中來分解單體。

第一種策略阻止了單體的發展。它通常是一種快速展示微服務價值的方法,有助於讓遷移和重構的工作獲得公司內部各個層面支援。另外兩種策略打破了單體。在重構單體時,你有時可能會使用第二種策略,但你肯定會使用第三種策略,因為它能實現將功能從單體遷移到絞殺者應用程式中。

下面讓我們來看一看這些策略。

1.將新功能實現為服務

“挖坑法則”(The Law of Holes)指出:如果你發現自己已經陷入了困境,就不要再給自己繼續挖坑了。當你的單體應用變得無法管理時,這是一個很好的可供參考的建議。換句話說,如果你有一個龐大的、複雜的單體應用程式,請不要通過向單體新增程式碼來實現新功能。這將使你的單體變得更龐大,更難以管理。相反,你應該將新功能實現為服務。

這是開始將單體應用程式遷移到微服務架構的好方法。它降低了單體的生長速度,加速了新功能的開發(因為是在全新的程式碼庫中進行開發),還能快速展示採用微服務架構的價值。

把新的服務與單體整合

圖 1顯示了將新功能實現為服務後的應用程式架構。除了新服務和單體外,該架構還包括另外兩個將服務整合到應用程式中的元素:

■ API Gateway:將對新功能的請求路由到新服務,並將遺留請求路由到單體。

■ 整合膠水程式碼:將服務與單體結合。它使服務能夠訪問單體所擁有的資料,並能夠呼叫單體實現的功能。

整合膠水的程式碼不是一個獨立元件。相反,它由單體中的介面卡和使用一個或多個程序間通訊機制的服務組成。

何時把新功能實現為服務

理想情況下,你應該在絞殺者應用程式中而不是在單體中實現每個新功能。你將實現新功能作為新服務或作為現有服務的一部分。這樣你就可以避免和單體程式碼庫打交道。不幸的是,並非每個新功能都可以作為服務實現。

因為微服務架構的本質是一組圍繞業務功能組織的鬆耦合服務。例如,某個功能可能太小而無法成為有意義的服務。例如,你可能只需要向現有類新增一些欄位和方法。或者新功能可能與單體中的程式碼緊耦合。如果你嘗試將此類功能實現為服務,則通常會發現,由於過多的程序間通訊而導致效能下降。你可能還會遇到資料一致性的問題。如果新功能無法作為服務實現,則解決方案通常是首先在單體中實現新功能。之後,你可以將該功能以及其他相關功能提取到自己的服務中。

以服務的方式實現新功能,可以加速這些功能的開發。這是快速展示微服務架構價值的好方法。它還能夠降低單體的增長速度。但最終,你需要使用另外兩種策略來分解單體。你需要通過將單體中的功能提取到服務,從而將單體中的功能遷移到絞殺者應用程式。你也可以通過水平分割單體架構來提高開發速度。我們來看看如何做到這一點。

2.隔離表現層與後端

縮小單體應用程式的一個策略是將表現層與業務邏輯和資料訪問層分開。典型的企業應用程式包含以下各層:

■ 表現邏輯層:它由處理 HTTP 請求的模組組成,並生成實現 Web UI 的 HTML 頁面。在具有複雜使用者介面的應用程式中,表現層通常包含大量程式碼。

■ 業務邏輯層:由實現業務規則的模組組成,這些模組在企業應用程式中可能很複雜。

■ 資料訪問邏輯層:包含訪問基礎設施服務(如資料庫和訊息代理)的模組。 表現邏輯層與業務和資料訪問邏輯層之間通常存在清晰的邊界。業務層具有粗粒度 API,由一個或多個封裝業務邏輯的門面(Facade)組成。這個 API 是一個自然的接縫,你可以沿著它將單體分成兩個較小的應用程式,如圖 2 所示。

一個應用程式包含表現層,另一個包含業務和資料訪問邏輯層。分割後,表現邏輯應用程式對業務邏輯應用程式進行遠端呼叫。

以這種方式拆分單體應用有兩個主要好處。它使你能夠彼此獨立地開發、部署和擴充套件這兩個應用程式。特別是,它允許表現層開發人員快速迭代使用者介面並輕鬆執行A/B測試,而無須部署後端。這種方法的另一個好處是它公開了業務邏輯的一組遠端API,可以被稍後開發的微服務呼叫。

但這種策略只是部分解決方案。很可能至少有一個或兩個最終的應用程式仍然是一個難以管理的單體。你需要使用第三種策略將單體替換為服務。

3.提取業務能力到服務中

將新功能實現為服務,並從後端拆分出前端Web應用程式並不會讓你抵達勝利的彼岸。你仍將最終在單體程式碼中進行大量開發。如果你希望顯著改進應用程式的架構並提高開發速度,則需要通過逐步將業務功能從單體遷移到服務來拆分單體應用。當你使用此策略時,隨著時間推移,服務實現的業務功能數量會增加,而單體會逐漸縮小。

你想要提取到服務中的功能是對單體應用自上而下的一個“垂直切片”。該切片包含以下內容:

■ 實現API端點的入站介面卡。 ■ 領域邏輯。 ■ 出站介面卡,例如資料庫訪問邏輯。 ■ 單體的資料庫模式。

如圖 3 所示,此程式碼從單體中提取並移至獨立服務中。API Gateway 將呼叫提取的業務功能的請求路由到該服務,並將其他請求路由到單體。單體和服務通過整合膠水程式碼進行協作。整合膠水由服務中的介面卡和使用一個或多個程序間通訊機制的單體組成。

提取服務具有挑戰性。你需要確定如何將單體的領域模型分成兩個獨立的領域模型,其中一個模型成為服務的領域模型。你需要打破物件引用等依賴。你甚至可能需要拆分類,以將功能移動到服務中。對了,你還需要重構資料庫。

提取服務通常很耗時,尤其是當單體的程式碼庫很混亂時。因此,你需要仔細考慮要提取的服務。應當重點關注重構那些能夠提供很多價值的應用程式部分。在提取服務之前,問問自己這樣做的好處是什麼。

例如,提取一項實現對業務至關重要且不斷髮展的功能的服務是值得的。如果沒有太多的好處,那麼在提取服務方面投入精力是沒有價值的。在本節的後面部分,我將介紹一些用於確定服務提取範圍和時間的策略。但首先讓我們更詳細地瞭解一下在提取服務時將面臨的一些挑戰以及解決這些挑戰的方法。

提取服務時會遇到以下這些挑戰:

■ 拆解領域模型。 ■ 重構資料庫。

拆解領域模型

為了提取服務,你需要從單體的領域模型中提取服務相關的領域模型。你需要進行大動作來拆分領域模型。你將遇到的一個挑戰是消除跨越服務邊界的物件引用。保留在單體中的類可能會引用已移動到服務的類,反之亦然。例如,想象一下,如圖 4 所示,你提取了Order Service,其Order類引用了單體的Restaurant類。因為服務例項通常是一個程序,所以讓物件引用跨越服務邊界是沒有意義的。你需要消除這種型別的物件引用。

解決此問題的一個好方法是根據DDD聚合進行思考。聚合使用主鍵而不是物件引用相互引用。因此,你可以將 Order 和 Restaurant 類視為聚合,如圖5所示,將Order類中對 Restaurant 的引用替換為儲存主鍵值的restaurantId 欄位。

使用主鍵替換物件引用的一個問題是,雖然這是對類的一個小改動,但它可能會對期望物件引用的類的客戶端產生很大的影響。在本節的後面部分,我將介紹如何通過在服務和單體之間複製資料來減少更改的範圍。例如,Delivery Service可以定義一個Restaurant類,後者是單體中Restaurant 類的複製品。

提取服務通常比將整個類移動到服務中的工作量要大得多。拆分領域模型面臨的更大挑戰是提取嵌入在具有其他職責的類中的功能。這個問題經常出現在具有過多職責的上帝類(God Class)中。例如,Order 類是FTGO應用程式中的上帝類之一。它實現了多種業務功能,包括訂單管理、送餐管理等。Delivery 實體會實現之前與Order類中的其他功能捆綁在一起的送餐管理功能。

重構資料庫

拆分領域模型不僅僅涉及更改程式碼。領域模型中的許多類都是在資料庫中持久化儲存的。它們的欄位對映到具體的資料庫模式。因此,當你從單體中提取服務時,你也會移動資料。你需要將表從單體的資料庫移動到服務的資料庫。

此外,拆分實體時,需要拆分相應的資料庫表並將新表移動到服務中。例如,在將送餐管理提取到服務中時,你需要拆分Order實體並提取出一個Delivery實體。在資料庫級別,你要拆分ORDERS表並定義新的DELIVERY表。然後,將DELIVERY表移動到該服務。

複製資料以避免更廣泛的更改

如上所述,提取服務需要你對單體的領域模型做出更改。例如,使用主鍵和拆分類替換物件引用。這些型別的更改可能會影響程式碼庫,並要求你對單體各個受影響的部分進行廣泛的更改。例如,如果拆分Order實體並提取Delivery實體,則必須更改程式碼中引用被移動欄位而受影響的每個部分。進行這些改變可能會非常耗時,並且可能成為打破單體的巨大障礙。

延遲並可能避免進行這些昂貴更改的一種好方法是使用類似於《資料庫重構》一書中描述的方法。重構資料庫的一個主要障礙是更改該資料庫的所有客戶端以使用新模式。本書中提出的解決方案是在過渡期內保留原模式,並使用觸發器在原模式和新模式間同步。然後,你可以將客戶端從舊模式遷移到新模式。

從單體中提取服務時,我們可以使用類似的方法。例如,在提取Delivery實體時,我們將Order實體在過渡期內大部分保持不變。如圖6所示,我們將與交付相關的欄位設定為只讀,並通過將資料從Delivery Service複製回單體來使其保持最新。因此,我們只需要在單體的程式碼中找到更新這些欄位的位置,並更改它們為呼叫新的Delivery Service即可。

通過從Delivery Service複製資料來保留Order實體的結構,可以顯著減少我們需要立即完成的工作量。隨著時間的推移,我們可以將使用與交付相關的Order實體欄位或ORDERS表列的程式碼遷移到Delivery Service。更重要的是,我們可能永遠不需要在單體中做出改變。如果隨後將該程式碼提取到服務中,則該服務可以訪問DeliveryService。

確定提取何種服務以及何時提取

正如我所提到的,拆解單體是耗時的。它分散了實施新功能的人力資源。因此,你必須仔細確定提取服務的順序。你需要專注於提取能夠帶來最大收益的服務。更重要的是,你希望不斷向業務部門展示遷移到微服務架構的價值。

在任何旅程中,瞭解你要去的地方至關重要。開始遷移到微服務的好方法是使用時間框架來定義工作。你應該花費很短的時間,例如幾周,集思廣益討論理想架構並定義一組服務。這將為你提供一個目標。但是,重要的是要記住,這種架構並非一成不變。當你分解單體並獲得經驗後,你應該應用你所獲得的經驗對重構計劃及時做出調整。

一旦確定了目標,下一步就是開始拆分單體結構。可以使用幾種不同的策略來確定提取服務的順序。

一種策略是有效地凍結單體架構的開發並按需提取服務。你可以提取必要的服務並進行更改,而不是在單體中實現功能或修復錯誤。這種方法的一個好處是它會迫使你打破單體。一個弊端是服務的提取是由短期需求而不是長期需求驅動的。例如,即使你對系統中相對穩定的部分進行了少量更改,也需要你提取服務。因此,你做的大量工作可能只能換來較小的收益。

另一種策略是更有計劃的方法,你可以根據提取應用程式模組獲得的預期收益,對應用程式的模組進行排名。提取服務有益的原因有以下幾點:

■ 加速開發:如果你的應用程式的路線圖表明應用程式的特定部分將在明年進行大量開發,那麼將其轉換為服務可加速開發。

■ 解決效能、可擴充套件性或可靠性問題:如果應用程式的特定部分存在效能、可擴充套件性問題或不可靠,那麼將其轉換為服務是有價值的。

■ 允許提取其他一些服務:由於模組之間的依賴關係,有時提取一個服務會簡化另一個服務的提取。 你可以使用這些條件將重構任務新增到應用程式的“待辦事項”中,並按預期收益排名。這種方法的好處在於它更具戰略性,並且更符合業務需求。在做 Sprint 的計劃時,你可以確定實現功能或提取服務