如何將巨石架構拆解成微服務
何時去耦合以及去耦合什麼
隨著巨石系統變得過於龐大而難以處理,許多企業開始著手將它們拆解成微服務架構樣式。這是一趟值得的旅程,但並不容易。我們了解到,要做好這件事,我們需要從一個簡單的服務開始,然後找出基於對業務來說很重要且容易頻繁變更的垂直能力的服務。這些服務一開始應該要大,最好也不要依賴於其餘的巨石架構。我們應該確保遷移的每一步都代表對整體架構的原子化改進。
2018 年 4 月 24 日
將巨石系統遷移到微服務生態系統是一趟史詩般的旅程。踏上這趟旅程的人抱持著擴大營運規模、加速變更速度以及擺脫高昂變更成本等願景。他們想要增加團隊數量,同時讓團隊能夠並行且獨立地提供價值。他們想要快速實驗其業務的核心能力並更快地提供價值。他們也想要擺脫對現有巨石系統進行變更所帶來的龐大成本。
決定何時去耦合哪項能力以及如何逐步遷移是將巨石架構分解成微服務生態系統時會遇到的部分架構挑戰。在這篇文章中,我將分享一些技術,這些技術可以引導交付團隊(開發人員、架構師、技術經理)在旅程中做出這些分解決策。
為了說明這些技術,我使用了一個多層線上零售應用程式。這個應用程式緊密結合了使用者介面、商業邏輯和資料層。我選擇這個範例的原因是它的架構具有許多企業執行的巨石應用程式的特徵,而且它的技術堆疊夠新,足以證明分解的合理性,而非完全重寫和替換。
微服務生態系統目的地
在開始之前,至關重要的是,每個人都對微服務生態系統有一個共同的理解。微服務生態系統是一個服務平台,每個服務都封裝一個業務功能。業務功能代表業務在特定領域中為實現其目標和責任所做的工作。每個微服務都公開一個 API,開發人員可以以自助服務的方式發現和使用。微服務具有獨立的生命週期。開發人員可以獨立構建、測試和發布每個微服務。微服務生態系統強制執行自主長期團隊的組織結構,每個團隊負責一個或多個服務。與一般認知和微服務中的「微」相反,每個服務的大小最不重要,並且可能根據組織的運營成熟度而有所不同。正如馬丁·福勒所說,「微服務是一個標籤,而不是描述」。
圖 1:服務封裝業務功能,通過自助服務 API 公開數據和功能
旅程指南
在深入了解指南之前,重要的是要知道將現有系統分解為微服務會產生很高的整體成本,並且可能需要多次迭代。開發人員和架構師有必要仔細評估分解現有單體是否是正確的途徑,以及微服務本身是否是正確的目的地。在澄清了這一點之後,讓我們來了解一下指南。
從一個簡單且相當去耦合的能力開始
開始走微服務路徑需要最低限度的運營準備。它需要按需訪問部署環境,構建新型持續交付管道以獨立構建、測試和部署可執行服務,以及保護、調試和監控分佈式架構的能力。無論我們是構建綠地服務還是分解現有系統,都需要運營準備成熟度。有關此運營準備的更多信息,請參閱馬丁·福勒關於微服務先決條件的文章。好消息是,自馬丁的文章發表以來,運營微服務架構的技術已經迅速發展。這包括創建服務網格,一個專用的基礎設施層,用於運行快速、可靠和安全的微服務網路,容器編排系統,用於提供更高級別的部署基礎設施抽象,以及持續交付系統的演進,例如GoCD,用於構建、測試和部署微服務作為容器。
我的建議是,開發人員和運作團隊使用第一個和第二個服務來建立基礎設施、持續交付管道和 API 管理系統,分解或建立新的服務。從與巨石應用程式相當解耦的能力開始,它們不需要變更目前使用巨石應用程式的許多用戶端應用程式,而且可能不需要資料儲存。此時,交付團隊最佳化的目標是驗證其交付方法、提升團隊成員的技能,並建立交付獨立部署的安全服務所需的最低限度基礎設施,公開自助服務 API。舉例來說,對於線上零售應用程式,第一個服務可以是「使用者端驗證」服務,巨石應用程式可以呼叫它來驗證使用者端,而第二個服務可以是「客戶設定檔」服務,一個提供新用戶端應用程式更佳客戶檢視的門面服務。
首先,我建議解耦簡單的邊緣服務。接下來,我們採用不同的方法,解耦深嵌在巨石系統中的能力。我建議先處理邊緣服務,因為在旅程的開始,交付團隊最大的風險是無法正確操作微服務。因此,使用邊緣服務來實作操作前提條件是很好的。一旦他們解決了這個問題,就可以處理拆分巨石應用程式的關鍵問題。
圖 2:使用變更半徑小的簡單能力熱身,以建立我們的操作準備
將對巨石架構的依賴降到最低
作為創始原則,交付團隊需要將新形成的微服務對巨石應用程式的依賴降到最低。微服務的主要好處是擁有快速且獨立的發布週期。對巨石應用程式(資料、邏輯、API)有依賴會將服務與巨石應用程式的發布週期結合在一起,禁止此好處。通常,遠離巨石應用程式的主要動機是鎖定在其中的能力變更成本高且緩慢,因此我們希望逐步朝著通過移除對巨石應用程式的依賴來解耦這些核心能力的方向前進。如果團隊在將能力建置到自己的服務中時遵循此準則,他們會發現相反的依賴,從巨石應用程式到服務。這是理想的依賴方向,因為它不會減慢新服務的變更速度。
考慮在零售線上系統中,「購買」和「促銷」是核心功能。「購買」在結帳過程中使用「促銷」為客戶提供他們有資格享有的最佳促銷活動,具體取決於他們購買的商品。如果我們需要決定接下來要解耦哪一個功能,我建議先從解耦「促銷」開始,然後再解耦「購買」。因為按照這個順序,我們可以減少對單體的依賴。按照這個順序,「購買」首先仍鎖定在單體中,並依賴於新的「促銷」微服務。
接下來的指南提供了其他方法來決定開發人員解耦服務的順序。這意味著他們可能無法總是避免對單體的依賴。在新的服務最終回調到單體的情況下,我建議公開單體的新 API,並透過新的服務中的防腐敗層存取 API,以確保單體概念不會外洩。努力定義反映定義良好的網域概念和結構的 API,即使單體的內部實作可能有所不同。在這種不幸的情況下,傳遞團隊將承擔變更單體、測試和釋出與單體釋出相結合的新服務的成本和難度。
圖 3:首先解耦不需要依賴回單體的服務,並將對單體的變更降到最低
提早拆分黏著能力
我假設在這個時候,傳遞團隊已經習慣建置微服務,並準備解決棘手的問題。然而,他們可能會發現自己受到他們可以解耦的下一項功能的限制,而不會依賴回單體。造成這種情況的根本原因通常是單體中的一個功能有缺陷,沒有很好地定義為網域概念,而且許多單體功能都依賴於它。為了能夠進展,開發人員需要識別出棘手的功能,將其解構為定義良好的網域概念,然後將這些網域概念實體化為獨立的服務。
例如,在基於 Web 的單體中,「(Web) 會話」的概念是最常見的耦合因素之一。在線上零售範例中,會話通常是許多屬性的儲存區,這些屬性範圍從跨不同網域邊界的使用者偏好,例如運送和付款偏好,到使用者意圖和互動,例如最近瀏覽的頁面、按下的產品和願望清單。除非我們處理解耦、解構和實體化「會話」的現有概念,否則我們將難以解耦許多未來的功能,因為它們將透過有缺陷的會話概念與單體糾纏在一起。我也反對在單體之外建立「會話」服務,因為這只會導致類似於目前在單體程序中存在的緊密耦合,只會更糟,超出程序並跨網路。
開發人員可以逐漸從黏著功能中提取微服務,一次一個服務。例如,首先重構「客戶願望清單」,並將其提取到新的服務中,然後將「客戶付款偏好設定」重構到另一個微服務中,並重複此步驟。
圖 4:識別最耦合的概念,並將其解耦、解構和具體化為具體的網域服務

垂直去耦合並提早釋出資料
從單體中解耦功能的主要驅動力,是能夠獨立發布這些功能。這個首要原則應指導開發人員在執行解耦時所做的每項決策。單體系統通常由緊密整合的層,甚至需要一起發布且具有脆弱的相互依賴性的多個系統組成。例如,在線上零售系統中,單體由一個或多個面向客戶的線上購物應用程式、一個實作許多商業功能且具有集中式整合資料儲存庫以保存狀態的後端系統組成。
大多數解耦嘗試都從提取使用者介面元件和一些門面服務開始,以提供對現代使用者介面友善的 API,而資料仍鎖定在一個架構和儲存系統中。儘管這種方法提供了一些快速好處,例如更頻繁地變更使用者介面,但當涉及到核心功能時,傳遞團隊只能像最慢的部分一樣快,也就是單體及其單體資料儲存庫。簡單來說,如果不解耦資料,架構就不是微服務。將所有資料保留在同一個資料儲存庫中,與微服務的 分散式資料管理 特性背道而馳。
策略是垂直移出功能,解耦核心功能及其資料,並將所有前端應用程式重新導向到新的 API。
讓多個應用程式讀寫集中共用的資料是將資料與服務解耦的主要阻礙。傳遞團隊需要納入資料遷移策略,以配合其環境,視其是否能夠同時重新導向並遷移所有資料讀取器/寫入器而定。Stripe 的 四階段資料遷移策略 適用於許多需要透過資料庫逐步遷移整合應用程式的環境,同時所有變更中的系統都需要持續執行。
圖 5:將能力與其資料解耦到公開新介面的微服務,修改並重新導向使用者到新的 API

去耦合對業務來說很重要且經常變更的部分
從巨石架構中解耦能力很困難。我聽過 Neal Ford 使用小心器官手術的類比。在線上零售應用程式中,提取能力涉及小心提取能力的資料、邏輯、使用者介面元件,並將它們重新導向到新的服務。由於這是一項非同小可的工作,因此開發人員需要持續評估解耦的成本與他們獲得的好處,例如更快速或規模擴大。例如,如果傳遞團隊的目標是加速對巨石架構中鎖定的現有能力進行修改,則他們必須找出修改最多的能力以進行移除。解耦持續進行變更、獲得開發人員大量關注且最限制他們快速傳遞價值的程式碼部分。傳遞團隊可以分析程式碼提交模式以找出歷史上變更最多的內容,並將其與產品路線圖和產品組合疊加,以了解在不久的將來會獲得關注的最理想能力。他們需要與業務和產品經理交談,以了解對他們真正重要的差異化能力。
例如,在線上零售系統中,「顧客個人化」是一個會進行大量實驗以提供最佳顧客體驗的能力,而且是解耦的良好候選對象。這是一個對業務、顧客體驗非常重要的能力,而且經常會進行修改。
圖 6:找出並解耦最重要的能力:為業務和顧客創造最大的價值,同時定期變更。
去耦合能力,而非程式碼
每當開發人員想從現有系統中提取服務時,他們有兩種方法:提取程式碼或重新撰寫功能。
通常,服務提取或巨石分解預設為重複使用現有實作,並將其提取到獨立的服務中。部分原因在於我們對自己設計和撰寫的程式碼有認知偏誤。建置的工作,無論過程多麼痛苦或結果多麼不完美,都會讓我們對它產生愛。這實際上稱為 宜家效應。很遺憾,這種偏誤會阻礙巨石分解工作。它會導致開發人員,更重要的是技術經理忽視提取和重複使用程式碼的高成本和低價值。
或者,傳遞團隊有選項可以重新撰寫功能並淘汰舊程式碼。重新撰寫讓他們有機會重新檢視商業功能,與業務部門展開對話,簡化舊流程,並挑戰隨著時間推移而建置到系統中的舊假設和限制。它也提供技術更新的機會,使用最適合特定服務的程式語言和技術堆疊實作新的服務。
例如,在零售系統中,「定價與促銷」功能是一段智力複雜的程式碼。它能動態設定和套用定價與促銷規則,根據各種參數(例如客戶行為、忠誠度、產品組合等)提供折扣和優惠。
這個功能無疑是重複使用和提取的良好候選對象。相反地,「客戶檔案」是一個簡單的 CRUD 功能,主要由序列化的樣板程式碼、處理儲存和設定組成,因此,它是重新撰寫和淘汰的良好候選對象。
根據我的經驗,在大部分的分解情境中,團隊最好將功能重新撰寫為新的服務,並淘汰舊程式碼。這是考量到重複使用的成本高、價值低,原因如下
- 有大量的樣板程式碼處理環境相依性,例如在執行階段存取應用程式設定、存取資料儲存、快取,並使用舊架構建置。大部分的這些樣板程式碼需要重新撰寫。用於主機微服務的新基礎架構與數十年前的應用程式執行階段截然不同,而且需要非常不同的樣板程式碼。
- 現有功能很可能並非圍繞明確的網域概念而建構。這會導致傳輸或儲存未反映新網域模型的資料結構,並需要進行重大重組。
- 經歷過許多變更反覆運算的長壽遺留程式碼,其程式碼毒性等級可能很高,且可重複使用的價值很低。
除非此功能相關、與明確的網域概念一致且具有高智慧財產權,否則我強烈建議重寫並淘汰舊程式碼。
圖 7:重複使用並萃取毒性低的價值高程式碼,重寫並淘汰毒性高的價值低程式碼

先巨觀,再微觀
在遺留巨石應用程式中找出網域邊界既是一門藝術,也是一門科學。一般來說,應用網域驅動設計技術來找出定義微服務邊界的限界脈絡是開始著手的好地方。我承認,我常常看到從大型巨石應用程式過度修正為非常小的服務,這些非常小的服務的設計靈感來自現有的資料正規化檢視,並受其驅動。這種用於識別服務邊界的方法幾乎總是會導致大量貧血服務的寒武紀大爆發,用於 CRUD 資源。對於許多微服務架構的新手來說,這會造成摩擦力高的環境,最終無法通過服務的獨立發布和執行測試。它會建立一個難以除錯的分布式系統,一個跨交易邊界而中斷的分布式系統,因此難以保持一致,一個對組織運作成熟度而言過於複雜的系統。儘管有一些啟發法說明微服務應該「微」到什麼程度:團隊規模、重寫服務所需時間、它必須封裝多少行為等。我的建議是,規模取決於傳遞和運作團隊可以獨立發布、監控和運作多少服務。從圍繞邏輯網域概念的較大服務開始,並在團隊在運作上準備好時將服務分解成多個服務。
例如,在零售系統解耦的旅程中,開發人員可以從一個服務「購買」開始,它包含「購物袋」的內容以及購買購物袋的能力,即「結帳」。隨著他們組成較小團隊和釋出更多服務的能力增長,他們可以將「購物袋」從「結帳」中解耦成一個獨立的服務。
圖 8:圍繞豐富的網域概念解耦巨型服務,並在準備好時將服務分解成較小的網域概念

以原子演化步驟進行遷移
將舊式巨石應用程式解耦成設計精美的微服務並讓它消失在空氣中的想法有點像神話,而且可以說是不可取的。任何經驗豐富的工程師都可以分享舊式應用程式遷移和現代化嘗試的故事,這些嘗試在過度樂觀的完全完成預期下進行規劃和啟動, bestenfalls 在一個足夠好的時間點被放棄。此類工作的長期計畫會被放棄,因為巨觀條件會改變:計畫資金用盡、組織將重點轉移到其他事物,或支持計畫的領導階層離開。因此,團隊如何處理巨石應用程式到微服務的旅程,應該將這個現實考慮進去。我稱這種方法為「架構演進的原子步驟遷移」,其中遷移的每一步都應該讓架構更接近目標狀態。每個演進單元可能是一小步或一大步,但都是原子的,不是完成就是還原。這特別重要,因為我們採取迭代和遞增的方法來改善整體架構和解耦服務。每個遞增都必須讓我們在架構目標方面處於更好的位置。使用 演化架構 適應度函數比喻,每次遷移的原子步驟之後,架構適應度函數應該產生更接近架構目標的值。
讓我用一個例子來說明這一點。假設微服務架構的目標是提高開發人員修改整體系統以提供價值的速度。團隊決定根據 OAuth 2.0 協定將終端使用者驗證解耦成一個獨立的服務。此服務旨在取代現有(舊架構)用戶端應用程式驗證終端使用者的方式,以及新的架構微服務驗證終端使用者。我們將演進中的這個遞增稱為「驗證服務導入」。導入新服務的一種方法是先執行以下步驟
(1) 建立驗證服務,實作 OAuth 2.0 協定。
(2) 在巨石應用程式後端新增一條新的驗證路徑,以呼叫驗證服務來驗證代表其處理要求的終端使用者。
如果團隊在此停下來,並轉向建立其他服務或功能,他們會讓整體架構處於熵增加的狀態。在此狀態下,有兩種驗證使用者的方式,新的 OAuth 2.0 基礎路徑,以及舊客戶端基於密碼/階段的路徑。此時,團隊實際上離他們更快進行變更的整體目標更遠。任何新的單體程式碼開發人員都需要處理兩個程式碼路徑,增加理解程式碼的認知負擔,以及變更和測試的過程更慢。
相反地,團隊可以在我們的原子演化單元中包含下列步驟
(3) 使用 OAuth 2.0 路徑取代舊客戶端基於密碼/階段的驗證
(4) 從單體中移除舊的驗證程式碼路徑
此時,我們可以論證團隊已經更接近目標架構。
圖 9:透過架構演化的原子步驟,將架構演化為微服務,在每個步驟之後,整體架構會朝著其目標改善,即使中間的程式碼變更可能會讓它遠離其適應目標

單體分解的原子單元包括
- 分離新服務
- 將所有消費者重新導向到新服務
- 從單體中移除舊的程式碼路徑。
反模式:分離新服務,用於新消費者,但從不移除舊服務。
我經常發現團隊會將某項功能從單體中移轉出去,並在建立新功能後立即宣告勝利,而不移除舊的程式碼路徑,也就是上面所描述的反模式。造成這種情況的主要原因是 (a) 專注於引入新功能的短期利益,以及 (b) 在面臨建立新功能的競爭優先順序時,移除舊實作所需的總工作量。為了做好這件事,我們需要努力讓原子步驟盡可能小。
透過這種方法進行移轉,我們可以將旅程分成較短的行程。我們可以安全地停止、恢復並存活於這趟漫長的旅程,消滅單體。
重大修訂
2018 年 4 月 24 日:首次發布