1. 程式人生 > >淺談微服務體系中的分層設計和領域劃分

淺談微服務體系中的分層設計和領域劃分

1.摘要

本文闡述了一種將分層設計和DDD領域設計應用於微服務體系架構的方案實踐,也是個人的最佳實踐。對於網際網路公司來說,我們主張將其Web服務架構分為五層:基礎設施層、領域服務層、應用服務層、閘道器層和使用者介面層(表示層)。領域服務層和應用服務層均可以採用微服務設計進行拆分,其中領域服務層將按照DDD領域建模進行領域劃分,設計為一個個領域模組微服務,每個微服務高度內聚,僅關注自己的業務,領域服務間通過介面呼叫進行鬆耦合。這種設計方案可以大大簡化大系統,並且在後期的維護中優勢會日漸凸顯,然而把大系統分而治之拆成微服務同時也對架構師和開發人員提出了更高的要求。第2部分介紹了相關背景,接著第3部分探討了分層設計以及每一層的功能,第4部分結合微服務和DDD對領域服務層層進行服務模組劃分和設計。最後一部分則就分層設計和DDD建模中常見的問題進行了整理。

2.背景介紹

想寫這樣一篇文章很久了,雖然本科學的是軟體工程,但礙於自己能力有限,從08年寫程式碼以來一直斷斷續續的思考,始終對專案程式碼結構設計(分包、分層)沒有一個可以讓自己覺得滿意且無糾結點的答案,假設了某個設計,很快在實踐中又會發現其存在著一些問題。直到2014年畢業工作了解了DDD領域驅動設計後,才有了相對清晰的方向。實際上早在2004年,Eric Envas的《領域驅動設計:軟體核心複雜性應對之道》就出版了,畢竟軟體開發自計算機普及以來已經存在很長一段時間了,早期國外程式設計師對軟體開發理論的研究也十分興盛,如今成熟後反而研究的相對少了,基本上依葫蘆畫瓢即可。DDD領域驅動設計對軟體設計各個環節的人員都有較高的要求,用《領域驅動設計》一書的話來說它需要一個“領域驅動團隊”[1],它要求從分析階段,產品經理、專案經理、架構師以及開發工程師就使用統一的模型語言(Ubiquitous Language)來進行溝通,並且他們都懂一些程式碼、產品和建模相關的知識,事實上這在國內很難實施,國內的產品經理約等於需求整理工,對其計算機基礎的要求是少之又少,在我所從事的公司裡,也曾發生過產品經理直接指導開發,以至於後面雙方理解的同一個詞有著不同含義的情況。近年來,隨著分散式的發展,傳統中小型機集中式伺服器已經不在流行,所以微服務體系也成為了各大網際網路公司主流的選擇。直觀的感受下微服務

DDD兩者,似乎一個是微系統,另一個則是大系統的設計方法,似乎兩者天生互斥,微服務化的小系統也用不著DDD,其實並不是,DDD是針對整個複雜的軟體解決方案的一種科學設計方法,微服務化也是把複雜的大系統拆分為小系統,方便維護和管理,所以兩者都有一個特點——為複雜的大系統服務。下面咱們就來探討下,如何把DDD的領域設計和其主張的分層設計應用到微服務體系架構中。需要說明的是本文主要是個人多年來的一點總結,未必適合所有場景,有更好通用性更為廣泛的方案請不吝賜教。

3.分層設計

準確的說分層設計(Layered Architecture)跟DDD沒有必然的聯絡,我最早接觸分層設計是在攜程網,當時內部使用的應該只是簡單的業務層(Biz)和表示層,資料庫訪問之類的也是放在各自的業務包下的。後來接觸和學習了《領域驅動設計:軟體核心複雜性應對之道》,書的第4章“分離領域”中說到了四層分層設計,即:基礎設施層、領域層、應用層和使用者介面層(表示層)。DDD產生的年代微服務還未流行,當時甚至基於瀏覽器的Web應用都比較少,更多的是PC軟體和EJB等網路應用,所以作者更多的是想表達對複雜系統的邏輯分層,並不在意每個領域是單獨的系統還是一個軟體系統內不同的模組。所以為了跟其做區分,我們建議的四層為在其基礎上引入“服務”兩個字,即:基礎設施層、領域服務層、應用服務層和使用者介面層。這樣做的意圖是讓開發人員立刻可以瞭解到——每個領域模組即一個微服務(一個領域可以對應一個微服務,也可以對應多個同域的)。摘要中提到我們主張的分層體系中還有一個層,即閘道器層

,這又是什麼鬼呢。剛剛提到的DDD的時代背景,PC軟體系統或者企業內部使用的網路應用系統是根本沒有閘道器層(有也是網路閘道器裝置)這一說的,而現如今網際網路公司產品的輸出形式無外乎Web應用(網站、或者網路服務),並且為了更好的適配PC站和App,一般會採用前後端分離的應用設計方案,這時候會產生一個需求——內部網路應用系統如何把自己的服務輸出到網際網路上,供外部系統或者瀏覽器網頁訪問。最直接的方式就是把應用層直接暴露在公網上,但我們不建議這麼做,應用層服務更多的是關注業務應用,對網路級的系統安全性(防DDOS、釣魚、跨域等)、請求監控等缺乏考慮,這些工作交給閘道器層統一管理會輕鬆很多(比如淘寶的TOP平臺)。

這時候我們在Web應用系統中引入閘道器層用於銜接表示層應用層,因為這樣可以更好的劃分各層的職能。閘道器層也可以看作是應用服務層的對外包裝層。如果一定要把閘道器層做到應用服務層裡理論上也是可行的,比如針對於Spring Cloud這種框架下的微服務體系,可以考慮直接暴露應用層,只需輔助一些運維手段即可。假設我們選擇引入閘道器層,那麼我們就得到了以下網路應用系統分層體系:

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

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

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

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

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

在這裡插入圖片描述

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

在這裡插入圖片描述

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

4.領域劃分和微服務化

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

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

5.Q&A

5.1 能不能在所有層使用資料持久層模型,簡單快捷?

大家一定聽說過不同層的資料模型的叫法不同的概念,比如資料持久層的模型物件叫DBO(database object)或者DPO(data persistence object),領域層的模型物件叫DMO(Domain Model Object)或者就叫Model,資料傳輸層的模型物件叫DTO(Data Transfer Object)。那為啥要這麼多模型呢,直接使用Mybatis等ORM框架生成DBO,然後一路吐給前端不是更爽(還真有同事嘗試立項寫Mybatis外掛來實現這種所謂的程式碼自動化)。我個人建議如果您真的是要搭建一個複雜的大系統,大平臺,一定不要偷這種懶,最好的就是做到"一層一模型"(閘道器層使用應用層模型即可)。各層之間採用手動的資料賦值(getter,setter)來完成,並且拒絕使用BeanUtils.copyProperties()這種工具類,因為這樣的工具類會讓"一層一模型"形同虛設。下面我們來細談下不能在每一層都是用資料持久層模型的具體原因:

  • 應用層對接閘道器層,是向面向C端或者呼叫者的一個數據出口,但是呼叫者只需要這個出口輸出使用者感興趣的資料,並且有些敏感資料不能吐出去。如果應用層(面向呼叫者)使用的仍然是資料庫模型,而開發人員沒有在應用層把無關值置空的話(相信我,需求一多,工作一忙,鬼才會在意這些細節),那麼資料庫裡的整條記錄就作為介面輸出吐出去了。比如訂單記錄,使用者訂單列表可能只需要訂單ID,商品名稱,訂單金額。而像商家結算價這種就不能吐出去,萬一被有心人察覺到了,使用者一定會投訴——你跟商家結算價200,賣給我400?
  • 前端或者介面呼叫方會很痛苦,一個介面契約多一兩個無關欄位是沒關係的,但是一個契約裡給的30個欄位,我只用到5個,前端會罵孃的,我親眼見過這種事,設計原則裡有個原則叫"迪米特法則",也叫最小知識原則,介面設計也可以參考這個原則,儘量讓你的呼叫方知道盡可能少的資訊點就能完成相關的任務。
  • "一層一模型"本質是解耦模型依賴。我在上家公司做架構師時為了兼顧開發的感受,決定讓他們可以在領域層和基礎設施層都是用資料持久層模型,而只需要在應用層做資料控制(解決第一個問題),然而我的妥協也慢慢露出弊端,開發有時候覺得某個資料庫欄位命名不合適修改之後,整個引用了該模型的微服務都需要修改,如果一層一模型的話,只需要關聯資料庫訪問的服務修改下DPO和DM的對映就行了,其他上層微服務都是依賴DM的。雖然我們不鼓勵隨意改動資料庫欄位,但設計框架上最好能支援這種情況。

剛開始推廣"一層一模型"的時候,會有耍小聰明的開發去把下一層的模型POJO直接拷貝過來改個名字,然後用BeanUtils.copyProperties()完成賦值,這樣跟直接使用資料持久層模型就沒有區別了,所以要杜絕這種情況的發生。

5.2 為啥需要應用層,領域層微服務直接通過閘道器暴露不就行了嗎?

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

在這裡插入圖片描述

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

在這裡插入圖片描述

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

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

為了加深對應用層的理解,我們舉個程式碼的例子,假如我們寫一個很簡單的首頁應用:

Response getHomeData(Request request){

    String nickName = userService.getNickName(request.getUserId()); 
    OrderInfo orderInfo = orderService.queryLatestOrder(request.getUserId());
    CostTrend costTrend = payService.queryCostTrend(request.getUserId());
    Integer points = pointService.queryAvailablePoints(request.getUserId());
    return new Response(nickName, orderInfo, costTrend, points);
}

這裡的4個服務類例項userService,orderService、payService和pointService如果都是本地的方法,那麼這就是一個單體應用,而微服務化後這4個可能都是微服務了,但是應用層應用的結構還是可以不用變化(現在很多的RPC框架都做到了與呼叫本地方法無差別)。這就是應用層的位置所在。

5.3 什麼是反模式?

這裡的反模式是指《領域驅動設計:軟體核心複雜性應對之道》這本書裡提到的與DDD相違背的模式,也是Eric極其反對的一種模式,即SmartUI模式(注意反模式不等於SmartUI,只是在本書中作為一個反模式的例子而已),這是一種什麼樣的模式呢,其實我很早之前做C++ Builder(和Delphi很像)的時候還不知道,C++ Builder就是一種SmartUI模式。但其實SmartUI並沒有錯,對於小規模的PC本地應用開發來說也是有很多好處的。舉個例子,C++ Builder中在窗體上新增一個按鈕,然後雙擊按鈕新增事件,這樣就跟實際操作的時候有機的結合了起來。換句話說就是使用介面驅動業務開發。在大型系統的開發上,這種模式是害人精,我很理解Eric為啥這麼討厭它。曾有一次我帶領著一個團隊在浦東一別墅做封閉式開發,在過完產品需求後,家裡出了點事我請了假,回來後發現產品經理竟然直接讓開發按照UI原型來設計資料庫,我稽核的時候發現這些開發設計的表有極其多的冗餘,而有一些重要的過程變數值卻沒有考慮到。比如他們會為每個頁面建幾個表,這顯然是行不通的,科學的方法是拆分領域,每個領域自己建立自己的表。UI只是應用層整合了各領域服務的資料並且處理後輸出的一種展示。

5.4 分層設計的開發步驟是怎樣的?

假設我們以一個標準的SaaS專案為主,也就是表示層是前端頁面(可以是APP,H5,M站,小程式,PC站等),那麼高效的一種開發步驟可以是這樣的:

  1. 業務、產品、開發PM進行需求評審(可行性等)
  2. 產品準備好原型
  3. 產品、開發(前後端)、架構師(或有架構師能力的資深開發)開會過PRD,瞭解要做什麼
  4. 架構師開始設計領域(資深架構師一下午就能搞定),前端開始切圖,應用層開發開始按照UI和PRD設計前端每個頁面使用的Restful介面(比如直接Springfox程式碼生成Swagger)
  5. 架構師設計完領域後分工給領域層開發,進行領域邊界明確,然後領域層開發開始設計資料庫表等。
  6. 這樣前後端開發就同時開工了。
  7. 開發初步完成後,自測加連調。
  8. 後續就是測試釋出了。

這個開發模式使我們(15人團隊,包括產品2個人,開發10人,前端3人)僅用了2個月時間就從0開發出了一套4S行業的SaaS系統,當然不免會存在一些小BUG,微服務化後職責清晰,定位問題也會很快。

6.結語

其實很多技術大神都是某一個技術點的好手,但可能在整體架構設計上思考並不多,每個人都有自己的設計方法,大部分容易想到的設計方法處理一般的系統已經夠了,後面發生問題慢慢打補丁就行了,當我們面對各種需求陷入各種開發問題的時候我們就該想想了,咱們系統的體系設計上是否出了問題?

歡迎大家有任何軟體/系統/平臺設計方法方面的問題一起來留言區探討~

參考文獻

  1. 領域驅動設計:軟體核心複雜性應對之道。Eric Envas,2016年6月第二版,前言部分。
  2. 領域驅動設計:軟體核心複雜性應對之道。Eric Envas,2016年6月第二版,Page.44。
  3. 領域驅動設計:軟體核心複雜性應對之道。Eric Envas,2016年6月第二版,Page.45。
  4. 領域驅動設計:軟體核心複雜性應對之道。Eric Envas,2016年6月第二版,Page.46。
  5. 領域驅動設計:軟體核心複雜性應對之道。Eric Envas,2016年6月第二版,Page.48。