講講拆分:從單體式應用到微服務的低風險演變

本文轉自微信號EAWorld。掃描下方二維碼,關注成功後,回覆“普元方法+”,將會獲得熱門課堂免費學習機會!本文轉自微信號EAWorld。本文轉自微信號EAWorld。掃描下方二維碼,關注成功後,回覆“普元方法+”,將會獲得熱門課堂免費學習機會!本文轉自微信號EAWorld。

在爲期兩天的微服務研討會中,我一直在思考如何向大家解釋單體應用(monolith-application)分解以及它向微服務過渡後將會是什麼樣。本文是該主題的一小部分,但我想與大家分享,並得到反饋(在研討會中,我們會更詳細地討論是否應該分解單體應用!)。本文中我總結了一些親歷的經驗,以及在過去幾年中與北美許多紅帽(Red Hat)客戶合作的經歷。這裏的第一部分主要探討了架構,即將發佈的第二部分則會介紹一些能提供 很大幫助的技術。關注我的Twitter(@christianposta)或 http://blog.christianposta.com,可以獲取最近的更新和討論。

在深入討論之前,讓我們先做以下假設:

微服務架構並非總是適用的(後面會詳細討論)
需要採用微服務架構時,我們應該確認單體式應用(monolith)會因此發生什麼變化。
在極少數情況下,單體式應用會按照原樣拆分。其它大多數情況下,要麼需要構

建新的特性,要麼圍繞單體式應用重新實現現有的業務流程(這有點逆潮流而動)

在需要拆分功能或重新實現的情況下,一個不能忽略的事實是單體式應用如今仍在用於生產,並帶來巨大的商業價值。
要設法解決這個問題,同時還必須保證不會干擾系統的整體業務價值。
由於單體式應用是個整體,所以變更它下面的數據模型或數據庫時非常困難甚至是不可能的。
我們應當降低演變的風險,這過程中可能需要多次部署和發佈。

一、抽取微服務

有關這個話題的會議或博文中,都提供了以下的建議:
圍繞名詞進行組織
做一件事,做好一件事
單一責任原則
它很難

但這些建議沒什麼用。

有效的建議應該像這樣:
識別模塊(現有的或是新的模塊)
拆分出與這些模塊相對應的表,並用服務進行包裝
更新此前直接依賴數據庫表的代碼並用它調用新服務
重複上述流程(Rinse and repeat)

具體來說:

第1步:識別模塊

圖片描述

這一過程從煩人的單體式應用開始。在上圖中,我簡化了這一點來表示其中可能涉及到的不同模塊和數據庫表。我們要確定哪些模塊是想從單體式應用裏拆分出來的,找出涉及到的表,然後繼續。當然,現實情況是單體式應用極易與模塊(如果有的話)相互纏繞。

第2步:拆分數據庫表,用服務包裝,更新依賴關係

圖片描述

第二步是確定Foo 模塊使用了哪些表,將它們拆分,然後加入模塊自身的服務中去。該服務就成爲現在唯一能訪問這些Foo表的服務了。再沒有別的共享表了!這是件好事。過去引用Foo的所有功能現在都必須經過新創建的服務的API。在上圖中,我們更新了Bar和Cheese服務,當它們需要Foo的時候,會引用Foo服務。

第3步:重複上述流程

圖片描述

最後一步是重複這個過程,直到單體式應用全部消失。在上圖中,我們對Bar 服務做了同樣的處理,把它搬到了一個架構裏,在這裏,服務擁有自己的數據和開放的API,這聽起來已經很像是微服務了。

通常,這算是一套不錯的指導方針,但上述步驟其實迴避了許多我們不應忽略的真相。比如我們不能要求時間暫停,然後從數據庫中把表刪除。同樣的:

很少能簡潔漂亮地將單體式應用模塊化
表格間的關係可以高度規範化,而且在各實體之間表現出緊密的耦合或完整性約束
我們不可能完全清楚單體式應用中的某些代碼到底調用了哪些表格
雖然我們已將表抽取到了一個新的服務中,但這並不意味着現有的業務流程停止了,我們可以讓他們一個個遷移到新的服務
有一些煩人的遷移步驟也不會憑空消失
可能會存在一些收益遞減的回報點,從這個點開始,把某些東西從單體式應用中拆分出來是毫無意義的

……等等等等

現在讓我們來看個具體的例子,看看這個方法/模式是什麼樣的,以及可供的選擇都有哪些。

二、具體舉例

這個例子來自上面提到的研討會。我將在分析拆分服務時做些潤色,但是研討會上談到的更多內容,包括領域驅動設計、耦合模型以及物理或邏輯架構,這裏先暫時不提。這個方法表面上似乎只能用於分解現有單體式應用的功能,但其實它同樣能爲單體式應用增加新功能。後者出現的概率可能更高,因爲直接變更單體式應用風險是相當大的。

三、瞭解單體式應用

圖片描述

這就是單體式應用(Monolith)。它建立在developers.redhat.com上的TicketMonster①教程的基礎上。該教程最初只是探討如何構建一個典型的Java EE應用程序,但最終卻成了一個很好的例子:它不過於複雜,而且有足夠的內容讓我們可以用來說明一些關鍵點。在即將發佈的整個主題的第二部分中,我們將深入探討技術框架或平臺。

在這張圖中,單體式應用將所有模塊/組件/UI共同部署到了一個單體數據庫中。當我們試圖變更時,就會牽一髮而動全身。試想一下,這個應用程序已經使用10多年了,所以現在變更起來難度很大(有技術原因,還有團隊或組織結構的原因)。我們希望拆分出UI和關鍵服務,使業務變更起來更快,更獨立,以交付新的客戶價值和商業價值。

注意事項
單體式應用(代碼和數據庫模式)很難變更
變更需要整個重新部署和團隊間高度的協調
我們需要進行大量測試來做迴歸分析
我們需要一個全自動的部署方式

四、抽取UI

圖片描述

在這步中,我們將從單體式應用中解耦UI。實際上在這個架構中,我們並未從中刪除任何東西。爲了降低風險,我們添加了一個包含UI的新部署。這個架構中的新UI組件需要非常接近單體式應用中的同一個UI(甚至完全一致),並調用它的REST API。 所以這意味着單體式應用擁有一個合理的API可供外部UI使用。但是,我們可能會發現並不是這麼回事:通常這類API可能更像是“內部的”API,這裏,我們需要考慮集成單獨的UI組件和後端的單體式應用,以及讓面向公衆的API更可用。

我們可將這個新的UI組件部署到架構中,並使用平臺將流量緩慢地路由到這個新架構,同時仍路由一些流量到舊的單體式應用。這樣我們就不用停機。同樣的,在本主題的第二部分,我們會更詳細地看到如何做到這點。無論如何,灰度上線(dark launch)/金絲雀 發佈(canary)/滾動發佈(rolling release)②等概念在這裏(以及後續步驟中)都非常重要。

注意事項

一開始,先不要變更單體式應用;只需將複製UI或者將它傳到單獨的組件即可
在UI和單體式應用間需要有一個合適的遠程API—但並非所有情況下都需要
擴大安全面
需要用某種方法以受控的方式將流量路由或分離到新的UI或單體式應用,以支持灰度上線(dark launch)/金絲雀測試(canary)/滾動發佈(rolling release)

五、從單體式應用中刪除UI

圖片描述

在上個步驟中,我們引入了一個UI,並緩慢地將流量轉移到新的UI(它與單體式應用直接通信)。在這一步中,我們將採用一個類似的部署策略,但不同的是,UI被刪之後,我們緩慢地發佈了一個單體式應用的新部署。如果發現問題,我們可以慢慢地讓流量流出,然後迴流。在把所有的流量都送到已刪除UI的單體式應用(此後稱後端-Backend)中,我們就可以完全刪除單體式應用部署了。通過分離UI,我們現在已對單體式應用進行了小規模的分解,並依靠灰度上線(dark launch)/金絲雀測試(canary)/滾動發佈(rolling release)降低了風險。

注意事項
從單體式應用中刪除UI組件
需要對單體式應用進行最小的變更(棄用/刪除/禁用UI)
不停機的前提下,再次使用受控的路由/整流方法來引入這種變更

六、引入新的服務

圖片描述

接下來的這步,跳過了耦合、領域驅動設計等細節,我們引入了一項新的服務:Orders服務。在這項關鍵服務裏,業務部分希望比其它應用程序變更的頻度更高,但同時它的編寫模式相當複雜。我們也可用這個模型來探索CQRS之類的架構模式(跑題了)。

我們要根據現有Backend內的實現來關注Orders服務的邊界和API。實際上,這個實現更可能是個重寫而不是利用現有代碼的端口,但是想法或方法都是相同的。注意在這個架構中,Orders服務有自己的數據庫。這點很好,儘管還差那麼幾步,但離達成一個完整的解耦也已經不遠了。接下來還需要考慮以下幾個步驟。

同時,這也是考慮該服務在整個服務架構中所處角色的好時機,需要做的是關注於它可能發佈或消耗的事件。現在是時候進行事件衝突(Event Storming)這類活動了,並思考在開始處理事務性工作負載時我們該發佈的事件。這些事件在集成其它系統甚至在演變單體式應用時,都會派上用場。

我們要關注被抽取的服務的API設計或邊界
可能需要重寫單體式應用中的某些內容
在確定API後,將爲該服務實施一個簡單的框架(scaffolding)/place holder
新的Orders服務將擁有自己的數據庫
新Orders服務目前不會承擔任何流量

七、將API與實現進行對接

圖片描述

在這裏,我們應該繼續推演該服務的API和領域模型,以及如何在代碼中實現模型。該服務會將新的事務性工作負載存儲到其數據庫中,並將數據庫與其它服務分開。服務訪問這些數據時必須經過API。

不能忽視的是:新服務及其數據與單體式應用中的數據關係緊密(雖然在某些地方不完全相同)。實際上這非常不方便。開始構建新服務時,需要來自Backend服務數據庫的現有數據的支持。由於數據模型中的標準化、FK約束、關係,這可能會非常棘手。在單體式應用/backend上重用現有API的話,粒度可能過於粗糙,這就需要重新發明一些技巧來獲取特定形式的數據。

我們要做的是通過底層API以只讀模式從Backend獲取數據,並重塑數據以適應新服務的領域模型。在此架構中,我們將連接到後端數據庫,並且直接查詢數據。這一步需要一個能反映直接訪問數據庫的一致性模型。

一開始,可能有些人會不敢採用這種方法。但事實是,這方法絕對可行,而且已經有在關鍵系統中應用成功的案例了。更重要的是,它不是最終架構(不要認爲它可能成爲最終架構)。可能你會認爲連接到後端數據庫、查詢數據和將數據製作成新服務領域模型所需的正確形式,會牽涉到許多不成熟,堆砌而成的代碼。但我認爲這只是暫時的,所以在單體式應用的演化過程中,這可能是沒問題的,也就是說,首先利用技術債,然後再迅速償還它們。不過,還有個更好的辦法。我會在本主題的第二部分討論。

又或者,大家還會說:“好吧,只需要在後臺數據庫前立個REST API,然後就可以提供更低級的數據訪問,再用新的服務調用它”。這也是個可行的方法,但它不是沒有缺點。同樣的,我也會在第二部分更詳細地討論這點。

注意事項
抽取的/新的服務的數據模型按照定義,是與單體式應用數據模型緊密耦合的
最可能的情況是,單體式應用提供的API不能在正確級別獲取數據
即使我們獲取了數據,也需要大量的代碼樣例來改造數據的形式
我們可以臨時連接到Backend數據庫以進行只讀查詢
單體式應用很少改變其數據庫

八、發送shadow traffic到新的微服務(dark launch)

圖片描述

接下來,需要將流量引入到新的微服務。注意,這不是一場重量級的發佈。簡單地把它扔到生產流量中顯然是不行的(特別是考慮到本例中使用了接受訂單的“訂購(order)”服務!這個過程中我們當然不想產生任何問題!)。雖然更改底層的單體式應用數據庫不是件容易的事,但如果可能,您可以小心地去嘗試更改單體式應用應用程序,使其調用新的訂單服務。如果你不知道哪種方式最好,我強烈推薦你看看Michael Feather的《有效利用遺留代碼》③。Sprout Method/Class或Wrap Method/Class這樣的模式也能幫到你。

當變更單體式應用/後臺時,我們希望保留舊的代碼路徑。這就需要加入足夠的代碼,讓新舊代碼路徑都能運行,甚至並行運行。理想情況下,變更後的新版單體式應用應該允許我們在運行時,能選擇是將流量發送給新的訂單服務、還是使用舊的代碼路徑,或是兩者兼顧。無論採用什麼調用路徑組合,我們應當瞭解新舊執行路徑之間存在哪些潛在偏差。

另外要注意的是,若允許單體式應用將執行命令發送給舊代碼路徑以及用於調用新服務,我們需要某種方法來將該新服務的事務或調用標記爲“合成(synthetic)”調用。如果你的新服務沒有本例那麼重要,且可以處理重複內容,那麼識別這個合成請求可能就不那麼重要。如果你的新服務傾向於更多的爲服務於只讀流量,可能就不用再識別哪些是合成的事務。然而,在綜合交易的前提下,你會希望能夠端到端地運行整個服務,包括存儲和數據庫。此時您可以選擇使用“合成(synthetic)”標誌來標記數據並存儲,或者在數據存儲支持的前提下,回滾該事務。

最後需要注意的是,當我們變更單體式應用/Backend時,我們希望再次使用灰度上線(dark launch)/金絲雀測試(canary)/滾動發佈(rolling release)。但基礎設施必須支持它才行。在第二部分我們會詳細討論。

在這裏,流量被迫回到單體式應用。我們試圖不擾亂主要的調用流程,以便當canary無效時能夠快速回滾。另一方面,部署網關或控制組件可能會發揮一些作用,它們能以更細的粒度控制對新服務的調用,而不是將調用強加給單體式應用。這種情況下,網關將具備控制邏輯,即能選擇是否將事務發送給單體式應用、新服務還是兩者都發。

圖片描述

注意事項
將新訂單服務引入代碼路徑有風險
要以可控的方式將流量發送給新服務
希望流量能被引到新服務以及舊代碼路徑
要測量和監控新服務的影響
要設法標記“合成(synthetic)”事物,以防發生比較頭疼的業務一致性問題
希望新功能部署到特定的羣組或用戶

九、金絲雀測試或滾動發佈新的微服務

圖片描述

若前面的步驟不會對事務路徑產生不良影響,同時,我們有很大信心能夠通過背景流量相關的測試及初期的生產實驗,那麼現在我們就可以將單體式應用設置爲“NOT shadow”,並將流量發送到新的微服務上了。這時,要指定特定的羣組或用戶,讓其始終轉入微服務。同時,我們正在慢慢導出那些從舊代碼路徑通過的真實生產流量。我們可以增加Backend服務的滾動發佈頻率,直到所有用戶都轉到新的訂單微服務上。

需要提醒一下,這裏存在風險:當我們開始將實時流量(非影子或合成流量)滾動到微服務時,期望與羣組匹配的用戶總是去調用這個微服務。因爲我們已經不能在新舊代碼路徑之間來回切換了。此時,如果我們想要實現回滾,就會牽涉到很多協調,才能使新事務從新業務移回到舊業務單元時也能使用。希望這種情況不會發生,但我們必須有所警惕並事先做好計劃,有相應的測試。

注意事項
確定羣組,並將實時事務流量發送給新的微服務
直接連接數據庫仍然是需要的,因爲在此期間,事務仍會從兩條代碼路徑通過
將所有流量轉到微服務後,就該放棄舊功能了
請注意,在將實時流量發送給微服務後,回滾到舊代碼路徑將遇到困難,需要協調

十、離線數據ETL/遷移

圖片描述

至此,訂單微服務開始承載實時的生產流量了。單體式應用或Backend仍然在處理其它需求,但我們已成功地將服務功能遷出了單體式應用。接下來需要迫切關注的是,需要還清新的微服務和Backend服務之間建立直接數據庫連接時產生的技術債。這很可能牽涉到從單一數據庫到新服務的一次性ETL(提取轉換加載)。單體式應用可能仍需要只讀式地保存那些數據(比如出於合規的考慮等)。如果它們是共享的引用數據(比如只讀的),這麼做應該沒問題。必須確保單體式應用和新的微服務中,各自的數據不共享。如果它們是的話,那麼最終會出現數據或數據所有權的相關問題。

注意事項
我們新的訂單微服務馬上就要完全自治了
將訂單服務數據庫連接到Backend數據庫時欠下的技術債,必須還清
對留在訂單服務中的數據應該實施一次性的ETL
要注意各種數據問題

十一、解耦數據存儲

圖片描述

完成了上一步,新的訂單微服務準備就緒,可以加入到服務架構中去了。本文介紹的步驟都有各自的注意事項和優缺點。我們的目標應該是完成所有步驟,避免技術債產生利息。當然,這種模式與實際操作可能會有差異,但方法沒有問題。

在接下來的後續博文中,我將展示如何使用之前提到的示例服務來完成以上步驟,並深入探討對哪些是有幫助的工具、框架和基礎設施。我們會看看Kubernetes、Istio④、特性標誌框架、數據視圖工具和測試框架等內容。請保持關注!

原文鏈接:
http://blog.christianposta.com/microservices/low-risk-monolith-to-microservice-evolution/

參考地址:
https://developers.redhat.com/ticket-monster/
http://blog.christianposta.com/deploy/blue-green-deployments-a-b-testing-and-canary-releases/
https://www.amazon.com/Working-Effectively-Legacy-Michael-Feathers/dp/0131177052
https://istio.io/

關於EAWorld
微服務,DevOps,元數據,企業架構原創技術分享,EAii(Enterprise Architecture Innovation Institute)企業架構創新研究院旗下官方微信公衆號。
掃描下方二維碼,關注成功後,回覆“普元方法+”,將會獲得熱門課堂免費學習機會!
微信號:EAWorld,長按二維碼關注。圖片描述

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章