領域驅動設計和服務自演進能力是內功。
前言
《微服務的團隊應對之道》提到,微服務幫助企業提升其響應力,而企業需要從DevOps、服務構建、團隊和文化四點入手,應對微服務帶來的複雜度和各種挑戰,從而真正獲益。如果說運維能力是微服務的加油站,服務則是其核心。
企業想要實施微服務架構,經常問到的第一個問題是,怎麼拆?如何從單體到服務化的結構?第二個問題是拆完後業務變了增加了怎麼辦?另外,我們想要改變的系統往往已經成功上線,並有著活躍的使用者。那麼對其拆分還需要考慮現有的系統執行,如何以安全最快最低成本的方式拆分也是在這個過程中需要回答的問題。
本文會針對以上問題,介紹我們團隊在服務拆分和演進過程中的實踐和經驗總結。
我們專案架構的演化歷程
該專案始於2009年,到現在已有7年的時間。在這7年中覆蓋的業務線不斷擴大,從工單、差旅、計費、檔案、報表、增值業務等;業務流程從部分節點到使用者端的全線延伸;7年間打造多個產品,架構經歷了多次調整,從單體架構、RPC、服務化、規模化到微服務。
主要架構變遷如下圖所示:
在這7年架構演進路上,我們遇到的主要挑戰如下:
- 如何拆?即如何正確理解業務,將單體結構拆分為服務化架構?
- 拆完後業務變了增加了怎麼辦?即在業務需求不斷髮展變化的前提下,如何持續快速地演進?
- 如何安全地持續地拆?即如何在不影響當下系統執行狀態的前提下,持續安全地演進?
- 如何保證拆對了?
- 拆完了怎麼保證不被破壞?
問題1:如何將單體結構拆分為服務化架構?
就如庖丁解牛一樣,拆分需要摸清內部的構造脈絡,在筋骨縫隙處下刀。那麼微服務架構中,我們認為服務是業務能力的代表,需要圍繞業務進行組織。拆分的關鍵在於正確理解業務,識別單體內部的業務領域及其邊界,並按邊界進行拆分。
1. 識別業務領域及邊界。
首先需要將客戶、體驗設計師、業務分析師、技術人員集結在一起對業務需求進行溝通,隨後對其進行領域劃分,確定限界上下文(Boundary Context),也稱戰略建模。
以下我們經常使用的方法和參考的紅藍寶書:
- Inception-> User Journey | Scenarios,用於梳理業務流程,由粗粒度到細粒度逐一場景分析。
- 四色建模,用於提取核心概念、關鍵資料項和業務約束。
- 領域驅動設計-戰略設計,用於劃分領域及邊界、進行技術驗證。
- Eventstorming,用於提取領域中的業務事件,便於正確建模。
Inception與DDD戰略設計的對比:
一個業務領域或子域是一個企業中的業務範圍以及在其中進行的活動,核心子域指業務成功的主要促成因素,是企業的核心競爭力;通用子域不是核心,但被整個業務系統所使用;支撐子域不是核心,不被整個系統使用,該能力可從外部購買。一個業務領域和子域可以包括多個業務能力,一個業務能力對應一個服務。領域的邊界即限界上下文,也是服務的邊界,它封裝了一系列的領域模型。
一個業務流程代表了企業的一個業務領域,業務流程所涉及的資料或角色或是通用子域,或是支撐子域,由其在企業的核心競爭力的角色所決定。比如企業有統一身份認證,決策不同部門負責不同的流程任務,那麼身份認證子域並不產生業務價值,不是業務成功的促成因素,但是所有流程的入口,因而為通用子域,可為單獨服務;而部門負責的業務則為核心子域。
舉個例子
工單業務流程:
某企業為服務人員提供工單服務的業務流程簡化如下。首先搜尋服務人員,選取服務人員購買的服務,基於目標國家的工單流程,向服務人員收取資料,對其進行審計,最後傳送結果。
其中服務為其核心競爭能力,包括該企業對全球各國的政策理解,即法律流程,服務資料(問卷),計算服務,資料審計服務,相比其他競爭對手的服務(價位/效率等),這些都為改企業提供核心的業務價值,自然也是核心子域。而其他用於統計改企業員工工作的工單,組織結構和員工為支撐子域,並不直接產生業務價值
領域劃分的原則
在劃分的過程中,經常糾結的一個問題是:這個模型(概念或資料)看起來放這個領域合適,放另一個也合適,如何抉擇呢?
- 第一,依據該模型與邊界內其他模型或角色關係的緊密程度。比如,是否當該模型變化時,其他模型也需要進行變化;該資料是否通常由當前上下文中的角色在當前活動範圍內使用。
- 第二,服務邊界內的業務能力職責應單一,不是完成同一業務能力的模型不放在同一個上下文中。
- 第三,劃分的子域和服務需滿足正交原則。領域名字代表的自然語言上下文保持互相獨立。
- 第四,讀寫分離的原則。例如報表需有單獨報表子域。核心子域的劃分更多基於來自業務價值的產生方,而非不產生價值的報表系統。
- 第五,模型在很多業務操作中同時被修改和更新。
- 第六,組織中業務部分的劃分也是一種參考,一個業務部門的存在往往有其獨特的業務價值。
簡單打個比方,同一個領域上下文中的模型要保持近親關係,五福以內,同一血統(業務)。
領域劃分的誤區和建議
- 業務能力還是計算能力?在劃分一些貌似通用的領域時,其實只是用到了通用的計算能力而不是業務能力,只需採用通用庫的方式進行封裝,而無需使用服務的方式。如我們系統的模板服務,是構建通用的模板服務,服務於整個平臺的服務;還是每個服務擁有獨立的模板模組?
- 儘早識別剝離通用領域。如身份認證與鑑權領域,是企業系統中最複雜、有相對多變的領域,需要及早隔離它對核心業務的干擾。
- 時刻促成技術人員與客戶、業務人員的對話。業務領域的劃分離不開對業務意圖的真正理解。而需求人員和體驗設計師對於User Journey的使用更熟悉,而技術人員、架構師對領域驅動設計、Eventstorming更熟悉。不管哪種方法都要求跨角色的群體協同工作,即客戶人員、業務分析師、體驗設計師與技術人員、架構師。而現實的情況中,User Journey更多的在Inception,在需求階段進行,而領域驅動設計、Eventstorming更多的在開發設計階段被使用,故而需求階段經常缺失技術人員,而開發設計階段經常缺失客戶、業務人員的參與。 另一個常見的現象是,Inception的參與人員和真正的開發團隊有可能不是同一個群體,那麼Inception中的業務溝通往往以UI的方式作為傳遞,因此在開發中經常只能通過UI設計來理解業務的真正意圖。 所以要想將正確的理解業務,做對軟體,需要時刻促成技術人員與客戶、業務人員的對話。
識別了被拆物件的結構和邊界,下一步需要決定拆分的策略和拆分的步驟。
2.拆分方法與策略
拆分方法需要根據遺留系統的狀態,通常分為絞殺者與修繕者兩種模式。
- 絞殺者模式 指在遺留系統外圍,將新功能用新的方式構建為新的服務。隨著時間的推移,新的服務逐漸“絞殺”老的一流系統。對於那些老舊龐大難以更改的遺留系統,推薦採用絞殺者模式。
- 修繕者模式 就如修房或修路一樣,將老舊待修繕的部分進行隔離,用新的方式對其進行單獨修復。修復的同時,需保證與其他部分仍能協同功能。
我們過去所做的拆分中多為修繕者模式,其基本原理來自Martin Fowler的branch by abstraction的重構方法,如下圖所示:
就如我們團隊所總結的16字重構箴言,我覺得十分的貼切:
“舊的不變,新的建立,一步切換,舊的再見”。
通過識別內部的被拆模組,對其增加介面層,將舊的引用改為新介面呼叫;隨後將介面封裝為API,並將對介面的引用改為本地API呼叫;最後將新服務部署為新程式,呼叫改為真正的服務API呼叫。
同時,拆分建議從業務相對獨立、耦合度最小的地方開始。待團隊獲取相應經驗和基礎設施平臺構建完善後,再進行核心應用遷移和大規模的改造。另外,核心通用服務儘量先行,如身份認證服務。
3. 拆分步驟
對於模組的拆分包括兩部分:資料庫與業務程式碼,可以先資料庫後業務程式碼,亦可先業務程式碼後資料庫。然而我們的專案拆分中遇到的最大挑戰是資料層的拆分。在2015年的拆分中發現,資料庫層由於當時系統效能調優的驅動,在程式碼中出現了跨模組的資料庫連表查詢。這導致後期服務的拆分非常的困難。因此在拆分步驟上我們更多的推薦資料庫先行。
4.資料庫拆分
我們借鑑了重構資料庫一書中提到的方法,通過重複schema同步資料,對資料庫的讀寫操作分別進行遷移。如下圖所示:
雖然技術上是可行的,然而這仍然佔用了大量不必要的時間,包括大量的資料遷移。這也是導致當時的拆分無法在給定時間內完成的很大因素。
5. 我們的結果:
系統架構圖:
問題2:拆分後業務變了增加了怎麼辦?
隨著客戶業務的變化,我們的服務也在持續的增加,而其中碰到了一個特大的服務。服務的大小如何衡量呢?該服務生產程式碼7萬行+,測試程式碼14萬行+,測試執行時間2個小時。團隊中7個stream每天50%工作需要對這個服務進行更改,使得團隊間的依賴非常嚴重,獨立功能無法單獨快速前行,交付速度及質量都受到了影響。
我們的總結:
客戶的業務是在變化的,我們對業務的認知也是逐漸的過程,所以Martin Fowler在他的文章中提出,系統的初期建議以單體結構開始,隨業務發展決定其是否被拆分或合併。那麼這也意味著這樣構建的服務在它的生命週期中必然會持續被拆分或合併。那麼為了實現這樣一個目標,使系統擁有快速的響應力,也要求這樣的拆分必然是高效的低成本的。
因此,服務的設計需要滿足如下的原則:
- 服務要有明確的業務邊界,以單體開始並不意味著沒有邊界。 服務要有邊界,即使以單體開始也要定義單體時期的邊界。我們系統中有一個名為“Monkey”的服務,是在中國虎年啟動的,由此它並不是一個業務概念。當這個服務的名字為MonkeyAPI時,可以想象5年來它變成了什麼?幾乎所有和這個產品相關的功能都放入了這個服務中。脫離平臺來看這一個產品的系統,其實它只是做了前後端分離而已。這個例子告訴我們,沒有邊界就會導致大雜燴,之後對其進行整理和重造的代價很大,可能需要花費“幾代人”的努力。
- 服務要有明確清晰的契約設計,即對外提供的業務能力。
- 服務內部要保持高度模組化,才能夠容易的被拆分。
- 可測試。
問題3:如何安全地持續地拆?
就如前言中提到的,系統已經上線大量的使用者正在使用,如何在不影響當下系統執行狀態的前提下,持續安全地演進?其實持續演進就是一場架構層次的重構,在這樣的路上同樣需要:
- 壞味道驅動,架構的壞味道是程式碼壞味道在更高層次的展現,也就意味著架構的混亂程度同樣反映了該系統程式碼層的質量問題。
- 安全小步的重構。
- 有足夠的測試進行保護——契約測試。
- 持續驗證演進的方向。
真正有挑戰的問題4:如何保證拆對了?
拆分不能沒有目標,尤其在具有風險的架構層次拆分更需謹慎。那麼我們如何驗證拆分的結果和收益?或許它可以提高開發效率,交付速度快,上線快,當機時間也短,還能提高開發質量,可擴充套件性好,穩定,維護成本低,新人成長快,團隊容易掌握等等。然而軟體開發是一個複雜的事情,拆分可以引起多個維度的變化,度量的難度在於如何準確定位由拆分這一單一因素引起的價值的變化(增加或降低)。
其實要回答這個問題,還是要回到拆分之初:為什麼而拆? 我所見過的案例中有因為政治原因拆的、業務發展需要的、系統整合驅動的等等;有因之而成功的,也有因之而失敗的。拆並不是一件容易的事,有諸多的因素。我認為不管表象是什麼,拆之前需要弄清拆分的價值所在,這也是我們可以保證拆分結果的源頭。
總結
系統可由單體結構開始,不斷的演進。而團隊需要對業務保持敏感,與客戶、業務人員進行業務對話,不斷修煉領域驅動設計和重構的能力。
在拆分的路上,我們的經驗顯示其最大的障礙來自義大利麵一樣的系統。不管我們是什麼樣的架構風格,高內聚低耦合的模組化程式碼內部質量仍然是我們架構演進的基石。具有夯實領域驅動設計和重構功底的團隊才可以應對這些挑戰,持續演進,保持其生命力。而架構變遷之前需要弄清背後的變遷動因與價值,探索性前進,及時反饋驗證,才是正解。那麼我們如何保證架構不被破壞呢?這個問題會在後續的文章中持續探討。
最後,勿忘初心,且行且演進。