DDD知識沉淀(一):淺談微服務體系中的分層設計和領域划分


我們主張將其Web服務架構分為五層:基礎設施層、領域服務層、應用服務層、網關層和用戶界面層(表示層)

其中,各層的職能和作用為:

    • 用戶界面層:負責向用戶顯示和解釋用戶指令。這里指的用戶可以是另一個計算機系統,不一定是使用用戶界面的人(比如外部應用調用對應接口)。
    • 網關層: 負責提供對外的HTTP服務或者其他應用層協議(這里是指OSI七層協議中的應用層,別混淆了哈)服務。
    • 應用服務層:定義軟件要完成的任務,並且指揮表達領域概念的對象來解決問題。這一層所負責的工作對業務來說意義重大,也是與其他系統的應用層進行交互的必要渠道。應用層要盡量簡單,不包含業務規則或者知識,而只為下一層中的領域對象協調任務,分配工作,使他們互相協作。它沒有反應業務情況的狀態,但是卻可以具有另外一種狀態,為用戶或者程序顯示某個任務的進度。
    • 領域服務層:負責表達業務概念,業務狀態信息以及業務規則。盡管保存業務狀態的技術細節是由基礎設施層實現的,但是反應業務情況的狀態是由本層控制並且使用的。領域層是業務軟件的核心。
    • 基礎設施層:為上面各層提供通用的技術能力,為應用層傳遞消息,為領域層提供持久化機制,為用戶界面層繪制屏幕組件(PS:這個在互聯網應用中幾乎用不到)等等。互聯網Web應用系統中基礎設施包含了數據持久化服務,中間件服務(數據庫,Redis,Memcached,ZooKeeper,ELK等等)以及第三方服務等。

各層除了實現自己的功能外,還需要遵守以下原則:

    1. 每一層設計保持內聚,並且只依賴於它的下方的層。
    2. 下層向上層發起的通信只能通過中間件等間接方式進行。
    3. 上層和下層只能有松散耦合(各自為獨立個體,通過簡單引用關聯)。在某些微服務框架比如Dubbo中,可以把api包提供給上層引用即可。而Spring Cloud的上下層耦合更為松散,通過契約約定即可。前者的優點是調用者可以直接使用提供方定義好的契約和方法。后者的優點則在於最大限度的降低了耦合,避免在上層無限制的進行下層包引入。

這里重點說明應用服務層和領域服務層之間的關系。舉一個例子:有一家上市企業A公司,靠賣水果發家,其首席架構師科學合理的按照DDD搭建了一套基於微服務體系的賣水果應用,其架構圖如下:

今年水果行情一般,而房地產十分火熱,A公司高層發現房地產帶動的五金行業也十分火熱,於是下達任務給技術部,要求其立即着手搭建五金銷售系統,貨源已經談好。得益於首席架構師之前優秀的架構設計,他發現只需要做一個賣五金的網站以及另外對微服務進行微量的調整即可滿足老板的需求——因為賣五金和賣水果並無本質區別,他們涉及的環節幾乎一致。加入五金售賣的系統架構圖如下:

可見應用服務層代表是某一個業務應用,它代表的更多的是從需求出發的應用定義,而領域服務層則是業務領域按照自身的邊界進行設計的一個高內聚的服務體。應用層通過協調和組合各個領域服務即可形成一個新的應用服務。《領域驅動設計》中明確指出,在設計領域服務時無需考慮表示層和持久層服務的東西。我在現實開發中總是遇到大量工程師按照產品的設計稿一溜煙的從上至下設計應用層服務和領域層服務,完全沒有考慮業務領域的概念,導致后面微服務數量膨脹,功能重復度高。這種開發習慣代表的是《領域驅動設計》作者極力吐槽的一種模式——SMART UI “反模式”。

 

這里着重講下應用層,為什么需要加應用層?

對於習慣了單體應用開發者來說,一個微服務很可能就直觀對應成了一個個垂直的應用服務,每個服務間的關系是這樣的:

 

其實這樣的體系本質上仍然不能解決軟件的復雜性,這只是把系統簡單粗暴的拆分了,耦合問題仍然很嚴重,甚至這很有可能比原來的單體應用更復雜(多對多依賴),如果使用微服務體系來處理復雜系統,其服務體系應當是這樣的:

 

 

這兩幅圖的區別在於,其實第一幅圖中的每個服務都包含了完整的2~3層,所以不再需要單獨的應用層。而第二幅圖各個領域模塊互相協作,對外提供服務時,則需要有一層直面用戶需求的應用層。

達成了微服務體系是解決復雜系統的出路之一這個共識后,我們再來看“應用層服務存在的必要性”有哪些理由:

    • 統一權限校驗:如上文所說,網關層只負責網絡級的安全防護,業務層的權限校驗則需要應用層來完成,試想一個沒有應用層的微服務體系,就意味着每一個微服務都需要加上權限校驗邏輯,這不僅編碼上困難(可以用過濾器,AOP),而且對於成千上萬個微服務(據了解,騰訊目前微服務數量已經超過2萬,大眾點評有將近千個微服務)來說,這會浪費大量時間,調用鏈越長,浪費的時間越多。換句話說,微服務體系有一個不突出但是很重要的特征—— 領域間環境安全,領域間的通信應當是可信的 ,否則分布式的缺點(多服務意味着多次通信)會被加劇。
    • 業務數據網關:舉個例子,一個order-service提供了一個queryOrder的接口,輸入起始日期查詢對應的訂單列表,其有2個消費者:C端網站應用服務 和 報表應用服務 ,C端網站應用服務 只需要知道訂單的基本信息如下單時間、商品名稱、金額就可以了,而報表應用服務是給管理者看的,需要的訂單數據很全,除了C端網站應用服務需要的之外,還需要看平台與商家的結算金額。根據第4部分最后一點的思路,我們肯定不能為調用方寫定制接口(寫不完的,有的要這個數據,有的要那個數據,每次新增調用方,領域服務還得找人修改)。而如果我們統一使用的全量數據,並且沒有應用層(同樣的也沒有應用層模型DTO了),那么很可能我們吐出去的數據包含了我們與商家的結算價,這會引發很多不必要的麻煩的。所以應用層還充當了業務數據網關的作用,應用層應用服務需要保證僅吐出調用方感興趣的數據。
    • 資源控制和緩存:想象一下雙十一高並發的情況,如果查詢庫存每次都查庫是多么恐怖的一件事。所以一般僅在支付的時候做一次庫存校驗,而在商品展示時查緩存的庫存即可。那么問題來了,如果沒有應用層,緩存直接放在庫存微服務上是否可行呢?首先這會入侵庫存領域,庫存微服務需要按照調用方的需求做特定時間的緩存,而不是自己想緩存多久就多久,我想庫存微服務的開發者也會很不滿的,他會提出,讓你自己去做緩存。他的方案是科學的,因為還有一些其他服務可能需要實時的數據。這時候就需要有一層來做對其下方微服務返回的數據按照應用自身的需求進行必要的緩存,而不是把這些需求都推給資源提供方,想象一下一個資源提供方有多少需求者,每個需求方都有自己的定制需求,該多痛苦。當然這一點也不是說微服務自身不能做緩存,微服務自身的緩存一定是考慮自身域的合理性后的一個措施(比如訂單查詢服務會做一個500ms的緩存,因為不會有正常人500ms里點兩次查詢還必須要求兩次都是最新的),而不是由調用方來決定的。
    • 資源聚合和加工:其實第2點也有加工的味道在里面,只是這里更多的是描述應用層應用根據自身需求來對下層返回的數據進行聚合和處理的過程。舉個例子就能很好的說明這一點:任何APP都有首頁,而首頁的數據可能是五花八門的,可以有用戶昵稱、最近下的訂單簡要信息、最近支出曲線、積分信息等。這4個信息可以來自4個領域微服務,他們是:用戶中心、訂單中心、支付中心和積分中心。那么有讀者會說,直接暴露微服務讓前端分別調用4個接口再做聚合不是也行嗎?顯然這種粗暴的方式是極其不合理的,會額外增加廣域網網絡調用3次不說,還傳輸了很多不必要的信息。
    • 應用隔離和流控:如果將每個領域服務直接暴露到網關層對外提供服務,那么在多應用場景下,多個應用間是共享這些服務能力的,在服務降級的時候,如果需要按照應用進行降級(比如將優先級不高的應用進行限流),就很難實現。但如果每個應用對應了一個應用層服務,只需要對其暴露的網關接口進行統一限流就行了,或者在應用層做一個開關,將其流量阻止在應用層,而不是拖垮整個領域服務。舉個例子,假如我們的平台不僅有自己的網站服務,還有第三方的對接服務,如果某個第三方被攻擊而我們直接將領域服務暴露了出去,那么我們就需要在各個領域層服務里去編寫對應的開關,這將侵入領域層服務,導致不必要的耦合。而有了應用層這些都不是問題,因為應用層充當了一個調度者的角色,調度者可以很輕松的決定是否調度下層的服務。

領域划分和微服務化

根據DDD理論,領域建模主要發生在領域服務層,各領域模塊都應該是高內聚低耦合的,具有清晰的業務邊界。本文不打算討論具體的DDD建模(服務,工廠,倉庫,實體,值對象,聚合等),這需要對DDD有較深入的研究,就目前所從事過的公司來看,似乎沒有一家真正嚴格按照DDD進行項目代碼設計的,就像摘要中說的,這對整個軟件工程鏈路上的人員都有較高的要求。有機會可以單獨寫一篇關於自己對DDD建模的思考和建議,本文更多的是討論高視角下的領域服務拆分,從而搭建一個低耦合高內聚的微服務體系。如果一定要將微服務和DDD聯系起來的話,領域層的微服務就對應了DDD中的領域模塊Module,每個Module由多個Service模式對象以及對應的模型對象(實體, 值對象以及它們的聚合)組成。

從《領域驅動設計:軟件核心復雜性應對之道》中我學到的主要有兩塊:領域設計思想和領域建模模式。本文更多的是對前者的運用,后者的對立模式是貧血模型,大家日常用到的也都是貧血模型,我也覺得貧血模型有存在的必要性,所以本文我們主要從其中借鑒一下領域設計思想。本文所描述的設計理念,並不影響具體的模型設計方法,我們仍然可以在每個微服務中使用DDD領域建模。

如何切分領域模塊並沒有一個明確的規則,不同的場景下可能相同的業務塊邊界也不盡相同。這里提幾點領域划分的個人心得:

    • 領域設計一定要有清晰的功能邊界。一個領域服務對應了一個功能集合,這些功能一定是有一些共性的。比如,訂單服務,那么創建訂單、修改訂單、查詢訂單列表,一般是訂單域的功能集合。
    • 領域拆分並不是一步到位的,應當根據實際情況逐步展開。從單體應用到微服務體系的拆分過程能很好的說明這個問題,一上來拆的很細的改造方案一定會死的很慘。所以如果一開始不知道應該划分多細,完全可以先粗粒度划分,然后隨着需要,初步拆分。比如一個電商一開始索性可以拆分為商品服務和交易服務,一個負責展示商品,一個負責購買支付。隨后隨着交易服務越來越復雜,就可以逐步的拆分成訂單服務和支付服務。
    • 領域拆分並不是一成不變的,應當具體情況具體分析。2015年在大眾點評的時候,其訂單服務就拆分為了order-service和order-query-service,一來為了讀寫分離,二來order-query-service作為單獨應用可以按需水平擴容。
    • 領域可以是多個子領域的一個虛擬集合,換句話說多個微服務也可以形成一個大域,不必糾結於領域和微服務之間的數量對應關系。我們在做架構設計PPT的時候可能就把訂單域作為一個領域,代表了這個域就是關於訂單的,具體該有幾個微服務,這需要更細的詳細設計來提供。
    • 領域層服務設計應當是調用者無關的。這一點有點像第一點,但是它強調的是領域層服務的設計不應該受調用者的影響,這個觀點在《領域驅動設計:軟件核心復雜性應對之道》這本書里也可以找得到。領域層服務開發和設計的理念是關注自己的域,一旦邊界划分清楚了,開發所需要考慮的永遠都只是輸入和輸出,提供的服務一定是盡可能通用的,面向功能來開發的,而不是面向調用方來開發的。比如某個調用方提出了一個需求:調用方B希望A服務提供一個買汽車的接口,那么A服務設計的接口就應該是buyCar(),而不是buyCarForA()。

原文地址:https://tbwork.org/2018/10/25/layed-dev-arch/


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM