如何拆分大型單體系統為微服務

Zhang_Xiang發表於2021-06-12

單體系統如何拆分為微服務

當單體系統越來越大,並難於維護時,很多企業開始有意把單體系統拆分為微服務風格架構。這麼做很有意義,但不容易。要做好這件事情我們必須學習,我們從一個簡單的服務開始,另一方面拉出以垂直功能為基礎的服務,這些功能對業務來說很重要並且經常變更。這些服務首先要很大並且最好不要依賴剩餘的單體系統。我們應該確保每一步遷移對於整體架構而已是一個原子改進。


遷移巨型單體系統到微服務生態系統是一個史詩性任務。從事這項任務的人擁有增加經營規模、促進變化、避免變更帶來的高開銷的意願。他們想要增加他們的團隊規模同時讓團隊以並行的方式傳輸價值並彼此獨立。他們想要快速實驗他們的業務核心功能並且更快的傳遞價值。他們同時要避免關於變更現存單體系統高昂的開銷。

決定解耦何種功能、何時、如何逐步遷移以分解單體系統到微服務生態是架構的挑戰。在這篇文章裡,我分享一些技術以引導交付團隊(開發者、架構師、技術經理)使用這些技術在過程中做出拆分決定。

微服務生態系統目標

在開始之前,大家對微服務生態系統達成共識是關鍵。微服務生態系統是一個服務平臺,每一個服務封裝一個業務功能。一個業務功能代表業務在特殊領域可以做什麼(實現目標和責任)。每個微服務暴露一個 API,以便開發者發現並用於自託管模式。微服務擁有獨立的生命週期。開發者可以獨立開發、構建、測試和釋出。微服務生態系統實施長期自治的團隊組織架構,每個團隊負責一個或多個服務。與大多數看法相反,微服務中的“微”和服務大小几乎沒有關係,而依賴於運營成熟的組織架構而改變(運營成熟的組織架構決定微服務)。

過程介紹

在深入介紹之前,瞭解分解現有系統到微服務會產生很高的總體成本(並且可能產生很多迭代)是很重要的。對於開發者和架構師來說密切的評估是否分解現有系統是正確的道路、微服務是否是正確的目的地。

通過一個簡單的解耦功能熱身

開始微服務需要一個最低層次的程式準備。它需要訪問部署環境,構建新的型別的 CD 管道以獨立構建、測試、部署執行服務,以及安全效能力,除錯和監控一個分散式架構。準備程式就緒成熟化是必須的,無論我們是否構建綠地服務或者分解已有系統。

我的建議是開發和運維團隊構建底層基礎設施、持續交付管道和 API 管理系統,並分解或構建第一個和第二個服務。從分解一個單體系統的功能開始,這個功能相對目前的單體系統來說不需要變更很多客戶端介面應用程式,也不需要資料儲存。交付團隊的優化點是驗證他們的交付渠道,升級團隊的技能,構建最小基礎設施以交付部署安全服務以暴露自託管服務。做為示例,一個線上零售應用程式,第一個服務是“終端使用者認證”服務,單體服務請求以認證終端使用者,第二個服務是“客戶檔案”服務,一個外觀服務為客戶提供更好的客戶檢視。

首先我推薦解耦簡單的邊緣服務。下一步我們採用不同的方式解耦深度嵌入的單體系統。我建議先做邊緣服務是因為在開始之初,交付團隊最大的風險是合理的運維微服務。所以使用邊緣服務體驗運維很有好處。一旦他們定位到問題,他們就可以定位到分離單體服務的關鍵問題。

最小化對單體的依賴

交付團隊的基本原則是最小化新的微服務對單體系統的依賴。微服務的主要好處是快速獨立的釋出閉體系。依賴單體系統的資料、邏輯、API,耦合服務到單體系統的釋出體系,禁止使用單體系統的好處。通常從單體系統架構跑路的主要動機是由於高昂的代價和封裝在單體系統中功能的緩慢變化,所以我們要緩緩的朝單體系統解耦核心功能的方向移動。如果團隊按照這篇文章的指導來為他們的微服務增加功能,那麼他們會發現從單體系統到服務的替換、依賴的反轉。這是理想的依賴方向,因為它不會放慢變更服務的步伐。

考慮一個線上零售系統,“購買”和“促銷活動”是核心功能。在結賬過程中,“購買”使用“促銷活動”給顧客提供最好的促銷活動。如果要決定下一步解耦這兩個功能裡的哪一個,我建議先開始解耦“促銷活動”然後才是“購買”。因為在這個順序下我們減少裡對單體系統的依賴。在這個順序下,“購買”繼續鎖在單體系統中,依賴外部的新的“促銷活動”微服務。

下一步本文將使用其它方式決定解耦服務。這意味著這些服務不是總是能避開對單體系統的依賴。如果一個新的服務最終回撥到單體服務,我建議從單體系統中暴露一個新的 API,新的服務通過反腐層訪問 API 以確保單體系統的概念不洩漏。力爭定義的 API 對於領域概念和結構有良好的反映,即便單體系統的內部實現不是那樣的。在這個不幸的案例中,交付團隊將承擔改變單體系統的開銷和困難,即測試、釋出新的服務和單體系統釋出耦合。

儘早分離黏性功能

假設交付團隊已經開始構建微服務並且準備進攻黏性問題。然而他們可能會發現他們能力有限,使下一個解耦的功能不依賴於單體系統。根本原因是通常是單體系統功能洩漏,定義的領域概念不好,有很多單體系統功能依賴於它。為了能處理這個問題,開發者需要辨別黏性功能,把它解構為定義良好的領域模型然後把這些領域概念實現到隔離的服務中。

例如 Web 單體系統,“session” 是最為常見的耦合因素之一。在線上零售示例中,session 通常是很多特性的封裝,從使用者的偏好(不同的領域邊界,比如:配送和支付偏好)到使用者的意圖和互動(比如:最近訪問的頁面、點選的產品和購買清單)。若非我們處理解耦、解構和具體化當前 session 的概念,我們將陷入解耦功能(這些功能通過洩漏的 session 概念纏住單體系統)的競爭中。同時我也不鼓勵在單體系統外建立 session 服務,因為它會導致和單體系統程式中類似的緊耦合,更糟糕的是,在程式外和跨網路。

開發者可以逐步從黏性功能中抽取微服務,每次一個服務。例如,先重構“顧客願望清單”並抽取到一個新的服務中,然後重構“顧客支付偏好”到另一個服務中。

垂直解耦,儘早釋放資料

從單體系統中解耦功能的主要驅動是可以獨立釋出它們。這是開發者在解耦過程中做每一個決定的首要原則。一個單體系統通常由緊密整合層,甚至幾個系統組成(需要釋出在一起並且有脆弱的相互依賴關係)。例如,在一個線上零售系統中,單體系統由一個或幾個面向顧客的線上購物應用程式組成,一個後端系統實現很多業務功能(包含一個集中的資料儲存)。

大多數解耦嘗試從抽取面向使用者元件、幾個外觀服務為 UI 提供友好的開發 API開始,同時資料仍然鎖在同一個 schema 中。雖然這種方式在一些方面立竿見影,比如更加頻繁的變更 UI,當涉及到核心功能時,交付團隊只能按照最慢的部分步伐,單體系統和它的巨大資料儲存。簡單的說,不解耦資料,架構就不是微服務。所有資料在同一個儲存中與微服務去中心化資料管理的特徵背道而馳。

策略是垂直移除功能,解耦核心功能和它的資料,並重定向所有前端應用程式到新的 API。

有多個應用程式從中心共享資料讀寫是服務解耦資料的主要障礙。交付團隊需要納入一個資料遷移策略,這個策略適配他們的環境依賴,無論他們是否同時重定向和遷移所有資料讀寫者。四段資料遷移策略是其中一種適應很多環境(需要逐步遷移整合資料庫的應用程式,同時所有系統在變更下需要繼續執行)的策略。

解耦對業務重要和頻繁變更的部分

從單體系統中解耦功能不容易。在線上零售應用程式中,抽取一個功能需要仔細抽取功能的資料、邏輯、面向使用者的元件然後重定向它們到新的服務。因為這是一堆重要的工作,開發者需要持續評估解耦得到的好處,比如:跑的更快或者增加規模。例如,如果交付團隊的目標是加速修改已經鎖在單體系統中的功能,那麼他們必須確定修改最多的功能。解耦程式碼中持續經受修改的部分(這部分程式碼持續得到開發者的關注,並最大限度限制了開發者快速交付成功)。交付團隊可以分析程式碼提交模型找出歷史上變化最大的內容,並將其與產品路線圖和產品組合進行疊加,以瞭解在不久的將來會受到關注的最需要的功能。他們需要和業務、產品經理溝通以瞭解對他們來說重要的功能差異。

例如在一個線上零售系統中,“顧客個性化”是一個功能,該功能要進行大量的實驗以為顧客提供最好的體驗,並且也是一個好的解耦候選項。它是一個對業務很重要的功能,使用者體驗,並且頻繁被修改。

解耦功能,不是程式碼

無論何時,開發者們要從一個現存系統中抽出一個服務,他們有兩種方式:抽取程式碼或者重寫功能。

通常情況下,服務抽取或者單體系統解構預設假設為重用已有的實現,原樣抽取到一個分離的服務中。部分原因是我們對我們設計、編寫的程式碼有一個認知偏見。建築(沒錯,這裡就是建築,這裡藉助 IKEA Effect 理論)過程讓我們對它產生熱愛,無論這個過程多麼痛苦,結果多麼不完美。不幸的是這種偏見將阻礙單體系統解構的努力。它引發開發者們和更多的重要技術管理者不理會高開銷和低價值的抽取和重用程式碼。

交付團隊可以選擇重寫功能然後讓老程式碼淘汰。重寫給了他們機會重新訪問業務功能,和業務開始一個新的談話,簡化遺留的過程和挑戰隨著時間推移建在系統中老的假設和限制。它同樣提供了一個重新整理技術的機會,使用最合適的一門程式語言和技術棧實現一個新的服務。

例如在零售系統中,“定價和促銷活動”功能是一段邏輯複雜的程式碼。它啟用動態配置和應用程式定價、促銷活動規則,提供折扣(在各種引數的基礎上,比如:客戶行為、忠誠度、產品包等)。

這個功能可以說是一個很好的重用和抽取的候選項。相反,“顧客文件”是一個簡單的 CRUD 功能,通常由樣板程式碼組成(序列化、處理儲存和配置),因此,它是重寫和淘汰程式碼的候選項。

在我看來,在大多數解構場景中,團隊最好重寫功能到一個新的服務中,並且淘汰老的程式碼。這裡考慮高開銷和低價值的重用,因為以下幾個原因:

  • 有大量的模版程式碼要處理環境依賴,比如在執行時訪問應用程式配置、訪問資料儲存、快取並且構建於老的框架。大多數模版程式碼需要重寫。新的基礎設施要託管一個微服務和幾十年應用程式執行時有很大的不同,並且需要不同種類的模版程式碼。
  • 很有可能存在的功能不是構建於清晰的領域概念。導致傳輸或者儲存資料結構不能反映新的領域模型和需要忍受一個大的重組。
  • 一個長時間存在的遺留程式碼經歷過很多迭代,導致很高的程式碼毒性級別和重用價值低。

除非能力是相關的,與清晰的領域概念保持一致並且具有很高的智慧財產權,否則我強烈建議重寫和淘汰舊程式碼。

先微服務,然後再劃分的更小

在遺留單體系統中尋找領域邊界既是藝術也是科學。通常應用領域驅動設計技術查詢邊界上下文定義微服務邊界是一個好的開始。我承認,我經常看到從巨大的單體系統到真正的小服務的過度修正,真正的小服務的設計是由於受到存在規範化的資料檢視的鼓勵和驅動。這種方式確認服務邊界導致寒武紀爆發大量的貧血服務(CRUD 資源)。對於微服務架構新手來說,這會建立一個高摩擦環境,最終無法通過獨立釋出和執行服務的測試。它建立了一個難於除錯的分散式系統,一個分散式系統打碎了事務邊界,因此難以保持一致性,對於組織的運營成熟度而言過於複雜的系統。雖然有一些如何“微”微服務的啟發式:團隊大小、重寫服務的時間、要封裝多少行為等等。我的建議是大小依賴於有多少服務交付,多少服務運維團隊可以獨立釋出、監控和操作。從圍繞邏輯領域概念的大型服務開始,並在團隊準備就緒時將服務分解為多個服務。

例如,在解耦零售系統的過程中,開發者可能開始於服務“購買”,這個服務封裝了“購物袋”,功能是購物和購物袋(也就是買單)。隨著他們組建更小團隊和釋出更多服務的能力的增長,他們可以將購物袋與結帳分離成單獨的服務。

以原子進化步驟遷移

通過將一個遺留的單體系統解耦成設計精美的微服務而讓它消失的想法在某種程度上是一個神話,可以說是不可取的。任何經驗豐富的工程師都可以分享遺留遷移和現代化嘗試的故事,這些嘗試是在對完全完成過於樂觀的情況下計劃和啟動的,但充其量在足夠好的時間點被放棄。由於巨集觀條件發生變化,此類努力的長期計劃被放棄:該計劃的資金用完,組織將重點轉向其他事物或支援它的領導層離開。所以這個現實應該被設計成團隊如何處理單體應用到微服務之旅。我稱這種方法為“架構演化的原子步驟中的遷移”,其中遷移的每一步都應該使架構更接近其目標狀態。每個進化單元可能是一小步或一大步,但都是原子的,要麼完成,要麼恢復。這一點特別重要,因為我們正在採用迭代和增量方法來改進整體架構和解耦服務。每個增量都必須讓我們在架構目標方面處於更好的位置。使用進化架構適應度函式比喻,遷移的每個原子步驟之後的架構適應度函式應該產生更接近架構目標的價值。

讓我通過一個例子圖解這個觀點。假設微服務架構目的是增加開發者修改整個系統的速度交付價值。團隊決定解耦使用者認證到一個隔離的服務中,以 OAuth 2.0 協議為基礎。這個服務想要同時替換已有客戶端應用程式認證終端使用者,而且新的架構微服務驗證終端使用者。讓我們將這個進化過程中的增量稱為“Auth 服務介紹”。一個方法介紹新的服務是先通過以下步驟:
(1)構建 Auth 服務,實現 OAuth 2.0 協議。
(2)在單體系統中新增一個新的認證路徑並呼叫 Auth 服務認證終端使用者。

如果團隊在這裡停下來並轉向構建一些其他服務或功能,他們會使整體架構處於熵增加的狀態。在這種狀態下,有兩種驗證使用者的方法,新的 OAuth 2.0 基本路徑和舊客戶端的基於密碼/會話的路徑。在這一點上,團隊實際上離他們更快地做出改變的總體目標更遠了。單體程式碼的任何新開發人員都需要處理兩條程式碼路徑,增加理解程式碼的認知負擔,以及更慢的更改和測試過程。

團隊可以包含以下步驟到我們的原子進化單元中:
(1)通過 OAuth 2.0 替換老客戶端的密碼/session
(2)從單體系統中淘汰老的認證程式碼

這時候我們可以說團隊已經接近目標架構了。

單體系統原子單元解構包括:

  • 解耦新服務
  • 重定向消費者到新的服務
  • 從單體系統中淘汰來的程式碼

反模式:解耦新服務用於新的消費者並且從不淘汰老的服務。

我經常發現團隊終止從單體系統中遷移功能,新的功能開發出來以後馬上就宣佈勝利了,也不淘汰老的程式碼路徑,正如上面描述的反模式。主要原因是:(a)聚焦於引入新功能的短期利益(b)淘汰老的程式碼會和構建新功能形成競爭優先順序。為了做正確的事,我們應該儘可能的做原子步驟。

引用

How to break a Monolith into Microservices

相關文章