乾貨總結:使用 DDD 指導微服務拆分的邏輯

開發者在剛開始嘗試實現自己的微服務架構時往往會產生一系列問題 :

  • 微服務到底應該怎麼劃分?
  • 一個典型的微服務到底應該有多微?
  • 如果做了微服務設計,最後真的會有好處嗎?

回答上面的問題需要首先了解微服務設計的邏輯,科學的架構設計應該通過一些輸入並逐步推導出結果,架構師要避免憑空設計和“拍腦門”的做法。

乾貨總結:使用 DDD 指導微服務拆分的邏輯

解耦的單體應用和微服務系統在邏輯上是一樣的。對於服務拆分的邏輯來說,先設計高內聚低耦合的領域模型,再實現相應的分佈式系統是一種比較合適的方式。服務的劃分有一些基本的方法和原則,通過這些方法能讓微服務劃分更有操作性。最終在微服務落地實施時也能按圖索驥,無論是對遺留系統改造還是全新系統的架構都能遊刃有餘。

微服務拆分的幾個階段

在開始劃分微服務之前,架構師需要在大腦中有一個重要的認識:微服務只是手段,不是目的。

微服務架構是為了讓系統變得更容易拓展、更富有彈性。在把單體應用變成靠譜的微服務架構之前,單體系統的各個模塊應該是合理、清晰地。也就是說,從邏輯上單體系統和微服務沒有區別,某種理想情況下微服務只是把單體系統的各個模塊分開部署了而已(最近流行的monorepo把多個服務的代碼倉庫以模塊的形式組織到了一起,證明了這一點)。

乾貨總結:使用 DDD 指導微服務拆分的邏輯

大量的實踐教訓告訴我們,混沌的微服務架構,比解耦良好的單體應用會帶來更多麻煩。

乾貨總結:使用 DDD 指導微服務拆分的邏輯

(混亂的微服務VS良好的單體)

開源社區為此進行了大量討論,試圖對系統解耦尋找一種行之有效的方法,因此具有十幾年歷史的領域驅動設計(DDD)方法論被重新認識。領域驅動設計立足於面向對象思想,從業務出發,通過領域模型的方式反映系統的抽象,從而得到合理的服務劃分。

採用 DDD 來進行業務建模和服務拆分時,可以參考下面幾個階段:

  • 使用 DDD(領域驅動建模) 進行業務建模,從業務中獲取抽象的模型(例如訂單、用戶),根據模型的關係進行劃分限界上下文。
  • 檢驗模型是否得到合適的的抽象,並能反映系統設計和響應業務變化。
  • 從 DDD 的限界上下文往微服務轉化,並得到系統架構、API列表、集成方式等產出。
乾貨總結:使用 DDD 指導微服務拆分的邏輯

(使用DDD劃分微服務的過程)

如何抽象?

抽象需要找到看似無關事務的內在聯繫,對微服務的設計尤為重要。

假設有一天,你在某電商網站購買了一臺空調,當你支付了空調訂單的費用後,又讓你再次支付安裝訂單費用,你肯定大為光火。原因僅僅可能是架構師在設計系統時,為空調這種普通產品生產了一個訂單,而安裝作為了另外業務邏輯生成了單獨的訂單。

你一定覺得這個例子太傻了,架構師不會這點都沒考慮到,”安裝“ 應該被抽象成一個產品,而”安裝行為“可以作為另外一個服務實現。然而現實的例子比比皆是,電信或移動營業廳還需要用戶分兩步辦理號卡業務、寬帶業務。原始是不合適的抽象模型造成的,並最終影響了微服務的劃分。

我們可以使用概念圖來描述一些概念的抽象關係。

乾貨總結:使用 DDD 指導微服務拆分的邏輯

(商品這一概念的概念圖)

如果沒有抽象出領域模型,就得不到正確的微服務劃分。

使用DDD進行業務建模

通過利用DDD對系統從業務的角度分析,對系統進行抽象後,得到內聚更高的業務模型集合,在DDD中一組概念接近、高度內聚並能找到清晰的邊界的業務模型被稱作限界上下文(Bounded Context)。

限界上下文可以視為邏輯上的微服務,或者單體應用中的一個組件。在電商領域就是訂單、商品以及支付等幾個在電商領域最為常見的概念;在社交領域就是用戶、群組、消息等。

DDD的方法論中是如何找到子系統的邊界的呢?

其中一項實踐叫做事件風暴工作坊,工作坊要求業務需求提出者和技術實施者協作完成領域建模。把系統狀態做出改變的事件作為關鍵點,從系統事件的角度觸發,提取能反應系統運作的業務模型。再進一步識別模型之間的關係,劃分出限界上下文,可以看做邏輯上的微服務。

事件是系統數據流中的關鍵點,類似於電影製作中的關鍵幀。在未建立模型之前,系統就像是一個黑盒,不斷的刺探系統的狀態的變化就可以識別出某種反應系統變化的實體。

例如系統管理員可以登錄、創建商品、上架商品,對應的系統狀態的改變是用戶已登錄、商品已創建、商品已經上架;相應的顧客可以登錄、創建訂單、支付,對應的系統狀態改變是用戶已登錄、訂單已創建、訂單已支付。

於是可以通過收集上面的事件瞭解到,“哦,原來是商品相關事件是對系統中商品狀態做出的改變,商品可以表達系統中某一部分,商品可以作為模型”。

乾貨總結:使用 DDD 指導微服務拆分的邏輯

(利用事件刺探業務黑盒並抽象出模型)

在得到模型之後,通過分析模型之間的關係得出限界上下文。例如商品屬性和商品相對於用戶、用戶組關係更為密切,通過這些關係作出限界上下文拆分的基本線索。

其次是識別模型中的二義性,讓限界上下文劃分更為準確。

例如,在電商領域,另外一個不恰當設計的例子是:把訂單中的訂單項當做和商品同樣的概念劃分到了商品服務,但訂單中的商品實際上和商品庫中的商品不是同一個概念。當訂單需要修改訂單下的商品信息時,需要訪問商品服務,這勢必造成了訂單和商品服務的耦合。

合理的設計應該是:商品服務提供商品的信息給訂單服務,但是訂單服務沒有理由修改商品信息,而是訪問作為商品快照的訂單項。訂單項應該作為一個獨立的概念被劃分到訂單服務中,而不是和商品使用同一個概念,甚至共享同一張數據庫表。

乾貨總結:使用 DDD 指導微服務拆分的邏輯

(典型具有”二義性“陷阱的場景)

”訂單下的商品“和”商品“在不同的系統中實際上表達不同的含義,這就是術語”上下文“的由來。一組關係密切的模型形成了上下文(context),二義性的識別能幫我們找到上下文的邊界(bounded)。同樣的例子還有 “訂單地址” 和 “用戶地址”的區別。

當然,在DDD中具體識別限界上下文的線索還很多,例如模型的生命週期等,我們會在後面的文章中逐步展開。在後續的文章中,我們會介紹更多關於 DDD 和事件風暴的思想和原理。

驗證和評審領域模型

前面我們說到限界上下文可以作為邏輯上的微服務,但並不意味著我們可以直接把限界上下文變成微服務。在這之前很重要的一件事情是對模型進行驗證,如果我們得到的限界上下文被抽象的不良好,在微服務實施後並不能得到良好的拓展性和重用。

限界上下文被設計出來後,驗證它的方法可以從我們採用微服務的兩個目的出發:降低耦合、容易擴展,可以作為限界上下文評審原則:

原則1,設計出來的限界上下文之間的互相依賴應該越少越好,依賴的上游不應該知道下游的信息。(被依賴者,例如訂單依賴商品,商品不需要知道訂單的信息)。

原則2,使用潛在業務進行適配,如果能在一定程度上響應業務變化,則證明用它指導出來的微服務可以在相當一段時間內足以支撐應用開發。

乾貨總結:使用 DDD 指導微服務拆分的邏輯


(一般抽象程度的領域模型)

上圖是一個電信運營商的領域模型的局部,這部分展示了電信號碼資源以及群組、用戶、寬帶業務、電話業務這幾個限界上下文。主要業務邏輯是,系統提供了號碼資源,用戶在創建時會和號碼資源進行綁定寫卡操作,最後再開通電話或寬帶業務。在開通電話這個業務流程中,號碼資源並不需要知道調用者的信息。

但是理想的領域模型往往抽象程度、成本、複用性這幾個因素中獲取平衡,軟件設計往往沒有理想的領域模型,大多數情況下都是平衡各種因素的苟且,因此評審領域模型時也要考慮現實的制約。

乾貨總結:使用 DDD 指導微服務拆分的邏輯


(”抽象”的成本)

用一個簡單的圖來表達話,我們的領域模型設計往往在複用性和成本取得平衡的中間區域才有實用價值。前面電信業務同樣的場景,業務專家和架構師表示,我們需要更為高度的抽象來滿足未來更多業務的接入,因此對於兩個業務來說,我們需要進一步抽象出產品和訂單的概念。

但是同時需要注意到,我們最終落地時的微服務會變得更多,也變得更為複雜,當然優勢也是很明顯的 —— 更多的業務可以接入訂單服務,同時訂單服務不需要知道接入的具體業務。對於用戶的感知來說,可以一次辦理多個業務並統一支付了,這正是某電信當前的痛點之一。

乾貨總結:使用 DDD 指導微服務拆分的邏輯


(高度抽象的領域模型)

幾個典型的誤區

在大量使用DDD指導微服務拆分的實踐後,我們發現很多系統設計存在一些常見的誤區,主要分為三類:未成功做出抽象、抽象程度過高、錯誤的抽象。

未成功做出抽象

在實際開發過程中,大家都有一個體會,設計階段只考慮了一些常見的服務,但是發現項目中有大量可以重用的邏輯,並應該做成單獨服務。當我們在做服務拆分時,遺漏了服務的結果是有一些業務邏輯被分散到各個服務中,並不斷重複。

以下是一個檢查單,幫助你檢查項目上常見的抽象是否具備:

  • 用戶
  • 權限
  • 訂單
  • 商品
  • 支付
  • 賬單
  • 地址
  • 通知
  • 報表單
  • 日誌
  • 收藏
  • 發票
  • 財務
  • 郵件
  • 短信
  • 行為分析

錯誤抽象

對微服務或DDD理解不夠。模型具有二義性,被放到不同的限界上下文。例如,訂單中的收貨地址、用戶配置的常用地址以及地址庫中的標準地址。這三種地址雖然名稱類似,但是在概念上完全不是一回事,假如架構師將”地址“劃分到了標準地址庫中,勢必會造成用戶上下文和系統配置上下文、訂單上下文存在不必要的耦合。

乾貨總結:使用 DDD 指導微服務拆分的邏輯

(左邊為抽象錯誤帶來的依賴,右邊為正確的依賴關係)

上圖的右邊為正常的依賴關係,左邊產生了不正常的依賴,會進一步產生雙向依賴。

在系統設計時,領域模型的二義性是一個比較難以識別和理解問題。好在我們可以通過畫概念圖來梳理這些概念的關係,概念圖是中學教輔解釋大量概念的慣用手段,在表達系統設計時一樣有用。

乾貨總結:使用 DDD 指導微服務拆分的邏輯

(電商系統中“地址”概念的梳理)

與地址類似的常見還有商品和訂單項中的商品;用戶和用戶組之間有一個成員的概念;短信的概念應該更為具體到一條具體的短信和短信模板的區別。

組織對架構的干預

另外一種令人感到驚訝的架構問題是企業的組織架構和團隊劃分影響了領域模型的正確建立。有一些公司按照渠道來劃分了團隊,甚至按照 To C (面向於用戶)和 To B(面向企業內部)劃分的團隊,最終設計出來的限界上下文中赫然出現 ”C端文章服務“,”B端文章服務“。

不乏有一些公司因為團隊職責的關係,將本應該集中的服務不得已下放給應用或者BFF(面向前端的backend)。對於這類問題,其實超出了DDD能解決的範圍,只能說在建模時警惕此類行為對系統造成很嚴重的影響。

另外企業組織架構和技術架構的關係,請參考康威定律的敘述。一個由無數敏捷團隊組成的企業,和微服務有天然的聯繫;傳統實時瀑布模型的企業,在大型軟件時代競爭力十足,但是在互聯網時代卻無力應對變化。

乾貨總結:使用 DDD 指導微服務拆分的邏輯


(常見一些公司的組織架構)

抽象程度過高

抽象程度過高最典型的一個特徵是得到的限界上下文極端的微小。回到我們成本、複用性和抽象程度這幾個概念上來,上面我們討論過,抽象程度雖然可以帶來複用性的提高,但是帶來的成本非常高,甚至不可接受。

抽象程度過高帶來的成本有:更多的微服務部署帶來的運維壓力、開發調試難度提高、服務間通信帶來的性能開銷、跨服務的分佈式事務協調等。因此抽象不是越高越好,應根據實際業務需要和成本考慮。

那相應的,微服務到底應該多小呢?

業界流傳一句話來形容,微服務應該多小:“一個微服務應該可以在二週內完成重寫“。這句話可能只是一句調侃,如果真的作為微服務應該多微的標準是不可取的。

微服務的大小應該取決於劃分限界上下文時各個限界上下文內聚程度。訂單服務往往是很多IT系統中最為複雜、內聚程度最高的服務,往往比較龐大,但無法強行分為 ”訂單part1“ ”訂單part2“ 等多個微服務;同樣,短信服務可能僅僅負責和外部系統對接,表現的極為簡單,但我們往往也需要單獨部署。

從限界上下文到系統架構

在通過 DDD 得到領域模型和限界上下文後,理論上我們已經得到了微服務的拆分。但是,限界上下文到系統架構還需要完成下面幾件事。

設計微服務之間的依賴關係

一個合理的分佈式系統,系統之間的依賴應該是非常清晰地。依賴,在軟件開發中指的是一個應用或者組件需要另外一個組件提供必要的功能才能正常工作。因此被依賴的組件是不知道依賴它的應用的,換句話說,被調用者不需要知道調用方的信息,否則這不是一個合理的依賴。

乾貨總結:使用 DDD 指導微服務拆分的邏輯

在微服務設計時,如果 domain service 需要通過一個 from 參數,根據不同的渠道做出不同的行為,這對系統的拓展是致命的。例如,用戶服務對於訪問他的來源不應該知曉;用戶服務應該對訂單、商品、物流等訪問者提供無差別的服務。

因此,微服務的依賴關係可以總結為:上游系統不需要知道下游系統信息,否則請重新審視系統架構。

設計微服務間集成方式

拆分微服務是為了更好的集成到一起,對於後續落地來說,還有服務集成這一重要的階段。微服務之間的集成方式會受到很多因素的制約,前面在討論微服務到底有多微的時候就順便提到了集成會帶來成本,處於不同的目的可以採用不同的集成方式。

  • 採用 RPC(遠程調用) 的方式集成。 使用RPC的方式可以讓開發者非常容易的切換到分佈式系統開發中來,但是RPC的耦合性依然很高,同時需要對RPC平臺依賴。業界優秀的RPC框架有dubbo、Grpc、thrift等
  • 採用消息的方式集成。 使用消息的方式異步傳輸數據,服務之間使用發佈-訂閱的方式交互。另外一種思想是通過對系統事件傳遞,因此產生了 Event Sourcing 這種集成模式,讓微服務具備天然的彈性。
  • 採用RESTful方式集成。 RESTful是一種最大化利用HTTP協議的API設計方式,服務之間通過HTTP API集成。這種方式讓耦合變得極低,甚至稍作修改就可以暴露給外部系統使用。

這三種集成方式耦合程度由高到低,適用於不同的場景,需要根據實際情況選擇,甚至在系統中可能同時存在。服務間集成的方式還有其他方式,一般來說,上面三種微服務集成的方式可以概括目前常見系統大部分需求。

可視化架構和沉澱輸出

第一次讀DDD相關的資料和書籍時,沒有記住DDD的很多概念,但是子域劃分像極了潮汕牛肉火鍋的劃分圖,給我留下深刻的印象。DDD 強調技術人員和業務人員共同協作,DDD 對圖的繪製表現的非常隨意自然。

但是在做系統設計時,應該使用更為準確和容易傳遞的架構圖,例如使用 C4 模型中的系統全景圖(System Landscape diagram)來表達微服務之間的關係。當然你也可以使用UML來完成架構設計。C4 只是層次化(架構縮放)方式表達架構設計,和UML並不衝突。

系統架構圖除了微服務的關係之外,也需要講技術選型表達出來。

微服務集成方式除了通過架構圖標識之外,最好也通過API列表的方式將事件風暴中的事件轉換為API;除此之外,可以將DDD領域模型細化成聚合根、實體、值對象,請參考DDD的戰術設計。

總結

邏輯往往比經驗更為重要。寫這篇文章的初衷是為了回答一個問題:如果老闆問我,你這個微服務劃分的依據是什麼,我該怎麼有說服力的回覆?

我該回答 “具體情況具體分析?By experience?”還是說,我是通過一套方法對業務邏輯進行分析得到的。當沒有足夠的經驗直接解決問題,或問題龐大到不足以使用經驗解決時,能支撐你做出決策就只有對輸入問題進行有效的分析。

使用 DDD 指導微服務劃分,能在一定程度上彌補經驗的不足,做出有理有據的系統架構設計。


分享到:


相關文章: