在分佈式系統中使用 DDD

無論我們使用單體、SOA、微服務、中臺或者其他架構,都需要解決如何組織代碼這個問題,DDD 並不是一個技術,而是指導我們組織代碼的一種思想,這種思想也並不是憑空出現的。

在使用 DDD 的思想時,最讓人迷惑的就是如何組織代碼,也就是通常所說的系統架構的問題。在前面提到 DDD 可以很好地指導代碼組織,其中舉了兩個例子,單體和微服務架構下 DDD 如何指導代碼的組織方式。令人沮喪的是,大部分應用系統既不是完全的單體系統,也不是純粹的微服務架構,而是出於某種中間狀態。

無論我們使用單體、SOA、微服務、中臺或者其他架構,都需要解決如何組織代碼這個問題,DDD 並不是一個技術,而是指導我們組織代碼的一種思想,這種思想也並不是憑空出現的。

就代碼組織這個問題,看起來沒有什麼技術含量,但實際上非常重要,軟件工程發展過程中出現過三次危機,軟件危機泛指在計算機軟件的開發和維護過程中所遇到的一系列嚴重問題,代碼的組織和大規模協作是其重要的組成部分。

  1. **結構化程序設計解決了第一次軟件危機。**60 年代~ 70 年代計算機剛剛投入商業使用,主要的編程方式還是彙編語言在特定的機器上編寫程序。當軟件規模較小,基本上處於計算機科學家個人編碼設計、使用的方式。隨着軟件規模擴大,複雜度增加,依賴特定機器、無結構化的編程方式無法應對軟件的發展,帶來了第一次軟件危機。爲了克服這個問題,業界提出了” 軟件工程 “的概念,1972 年 C 語言的出現,解決了代碼結構化、抽象性、可移植的問題。

  2. **面向對象解決了第二次軟件危機。**隨着軟件在商業中大規模使用,軟件變得原來越複雜,即使結構化的 C 語言也無法滿足業界對可維護性、可拓展性的需求。標誌性的事件是 IBM 公司開發的 OS/360 系統失敗,該系統有 4000 多個模塊,約 100 萬條指令,以及大量的 bug。面向對象的編程語言,Java、C#、C++ 出現,面向對象帶來了更自然地代碼組織方式,軟件開發變得越像建築業。

  3. **第三次軟件危機。**第三次軟件危機還沒有一個明確定義,通常來說就是互聯網行業興起,軟件變得越來越複雜,需求越來越多變。軟件開發從建築業變成了服務業,需要隨時響應變化,在軟件行業表現爲瀑布開發越來越不可行,敏捷開發越來越重要。從技術上表現爲單機開發越來越不可行,分佈式系統是必然的趨勢。

每一次危機的解決,都是建立在前一次的基礎之上的。面向對象是建立在結構化程序設計之上的,敏捷也是建立在瀑布之上的,而不是推翻前者。DDD 還停留在面向對象這個階段,可以用來指導分佈式系統設計,應對越來越複雜的應用系統,DDD 也不是面向對象思想的替代者。

DDD 的代碼組織形式衆說紛紜,並沒有一個標準的代碼架構。爲什麼會這樣呢?實踐中我們發現,不同公司、項目的業務背景不一致,架構不一致,架構的演化層次不一樣(查看另外一篇文章《架構的演進》),標準的代碼架構並不適合每一個公司。

當我們的系統架構從單體往 SOA、微服務、中臺演變,無論名稱如何變化,實際上都是分佈式系統,只不過分佈式的程度不一致而已。所以我們需要將問題拓展到分佈式系統這個更大的概念上,再來談 DDD 的代碼組織形式纔有意義。

我們看一下分佈式系統下一個定義:

分佈式系統是一組電腦,透過網絡相互連接傳遞消息與通信後並協調它們的行爲而形成的系統。——維基百科

從廣義的分佈式系統定義上來看,現在的互聯網應用基本上沒有不是分佈式的了。分佈式系統不是軟件工程師主動選擇的結構,而是業務逼得這樣選擇。阿里巴巴帶動的去 IOE (去掉 IBM 的小型機、Oracle 數據庫、EMC 存儲設備,代之以自己在開源軟件基礎上開發的系統)就是一個很好的體現。

在這樣的一個思維方式下,單體系統是隻有一個計算節點的分佈式系統,那麼 DDD 在單體應用下的經驗也可以應用起來。我沒有找到一個專業術語描述分佈式系統程度,這裏請允許我創造一個新詞,分佈式級別

分佈式級別

爲了解決業務上的問題,用戶量大、業務規模大,當用戶量增長到無法被容忍時,我們引入分庫分表(分佈式數據庫)、垂直拆分業務(微服務)。

我們會將系統變得越來越複雜,然後不得不解決各種分佈式系統下的新問題,業務上面臨的問題被轉移到技術上,從而業務纔有可能持續性的發展。我們面臨的問題不會消失,只會從一個地方轉移到另外一個地方,轉移到我們能容忍的地方,比如轉移到雲上,然後通過購買服務解決。

系統中節點角色越少,需要解決的分佈式問題則越少,可以認爲這是低級別的分佈式系統。低級別的分佈式系統 架構基本上沒有什麼分佈式問題存在,目前主流的小項目通過 Nginx 讓應用水平拓展 + 主從數據庫的架構可以看做低級別的分佈式系統。

系統中節點的角色越多,應用垂直拆分,需要解決的分佈式問題就越多,遇到的技術挑戰也越多,我們可以認爲這是高級別的的系統。應用系統的例子就是微服務架構,另外一個例子就是大數據平臺。

我把分佈式級別做了如下劃分,基本上可以囊括目前互聯網應用系統的主流架構:

在微服務項目中經歷過痛苦的開發者應該所有體會,全世界開發者貢獻了大量的開源軟件嘗試解決這些問題,後面詳細介紹每一個問題如何具體解決。

清醒的使用 DDD

上面這些分佈式系統的問題,DDD 都解決不了。DDD 的作用只有一個:在單體中劃分模塊,在分佈式系統中劃分服務。服務劃分的良好,關聯查詢、授權、分佈式一致性等問題可以被很好的解決,也就是我們常常說的解耦

但是就這一個作用,對於做應用開發的業務系統來說至關重要,雖然對於專門解決技術複雜度問題的雲廠商來說用處不大,所以最好讓 DDD 在合適的地方發揮作用。高級別的微服務系統的修改成本如此之高,以至於服務劃分錯誤幾乎沒有能力調整回來,甚至導致很多互聯網公司就此走向失敗。

因此,如何劃分服務,這是 DDD 非常有價值的一個地方,在分佈式系統中,DDD 起到的作用實際上就是指導垂直拓展。值得慶幸的是,應用系統分佈式級別增加帶來很多技術挑戰,但是邏輯上的架構變化卻不大。

在每一個不同的演化層次下,談 DDD 的代碼架構纔有意義。例如單體系統沒有必要過多分層,避免樣板代碼大量出現;微服務系統則需要小心分層,並嚴格執行,否則修改成本非常高。另外也需要解決該層次下的技術問題,微服務需要解決分佈式事務問題、分佈式授權問題、分佈式緩存問題、性能問題等。

DDD 分層和職責

在 DDD 指導代碼設計部分,我們提到了三層架構和 DDD 的四層架構的區別,DDD 的四層架構被越來越多的認可,但是每層具體的職責很少有文章談到。根據實踐經驗,我把四層模型中具體的職責整理出來,用於團隊在做架構設計中能有共同的認識。

前面的 DDD 四層模型的圖爲了表達每層中的元素,丟失了一個重要的角度,每一層的組件可能有多個。還是以收銀機系統爲例,架構會是像下面這樣,業界大多數互聯網架構圖也是這樣畫的,只是使用術語略有不同。

實踐中我們發現,接入層是由應用場景解決的,因此接入層需要在特定應用場景下使用。收銀機應用下,接入層是 Restful API 以及 socket 連接實現的實時通信,商戶管理和平臺管理無需使用這些接入方法,在不前後端分離的情況下,模板引擎也足夠使用。

同樣的,基礎設施層是和領域層綁定到一起用於實現業務邏輯和規則,底層基礎設施的選擇由領域層決定。商品服務主要是和數據庫打交道,需要使用 Mybatis,但是用戶認證服務(圖上未體現)可能只需要 Redis 做分佈式會話即可。

接入層和技術設施層,更應該看做兩個亞層。結合 DDD 術語將示例圖調整如下:

應用層

餐飲系統是一個非常複雜,具有多端、多租戶的系統,往往有收銀機應用、手機點餐應用、商戶管理、平臺管理等應用,從而組合成一個系統。在有些公司的語境裏,應用層往往是根據用戶角色劃分的,被稱爲” 業務面 “。

應用層的特點:

接入層

對接入層來說,我們可以看到,實際上接入層是依附於應用層存在的,隨着前後端分離,Restful API 成了主流,對簡單的系統來說這一層越來越弱化。對於有終端接入的系統來說,接入層並不簡單,需要處理各種協議適配:XMPP、websocket、MQTT 等。在複雜度不高的情況下,我們往往把接入層和應用層合併部署,這裏往往憑經驗來決定。如果對分佈式級別有了認識,可以更爲科學的選擇是否要將接入層和應用部署到一起。

接入層的特點:

領域層

對於領域層來說,很多互聯網公司沒有這個概念,將這些實現混合在應用層隱藏實現了,造成業務規則不一致。隨着前後端分離的發展,2013 年左右我也開始前後端分離實踐,接入層剝離出去後,後端開發者開始審視是否需要抽象出一層來複用業務邏輯。當時大部分互聯網公司稱爲服務,也就是 SOA 架構,大量使用 XML 和 SOAP 技術。

領域層的特點:

基礎設施層

對於基礎設施層來說,技術設施層並不是指 MySQL、Redis 等外部組件,而是外部組件的適配器,Hibernate、Mybatis、Redis Template 等,因此在 DDD 中適配器模式被多次提到,基礎設施層往往不能單獨存在,還是要依附於領域層。技術設施層的適配器還包括了外部系統的適配,互聯網產品系統的外部系統非常多,常見的有活體監測、風控系統、稅務發票等。

技術設施層的特點:

DDD 分層的注意事項

DDD 分層架構需要認識到一點是,有時候我們在項目中找不到每層之間的明顯的界限,那是因爲我們使用的框架幫我們完成某一層。MVC 框架,Spring MVC、Jersey 幫我們搞定了接入層的事情,Hibernate、Redis Template 讓我們感覺不到基礎設施層。四層模型並不是一個刻板的教條,應該和你選用的框架做出調整,DDD 的作者也多次強調這一點。

另外,基礎設施層和接入層需要注意兩點:

DDD 分層到四種架構的映射

我們把這四層合到一起部署就是準單體系統,分開部署就是微服務、SOA。

更加有意思的是,在準單體系統中,如果我們嚴格限定領域層中模塊之間的耦合關係,應用層訪問領域層是通過本地方法調用的。當我們想改造成微服務實現時,只需要簡單的抽象一個接口,然後通過遠程調用實現它,無論是 RPC、還是 Restful 訪問都不是大問題。

當然我們得解決遠程調用後的一系列問題,以及領域層是解耦良好的。

準單體系統

準單體系統架構下,所有的代碼在一個代碼倉庫,四層架構依然,往往通過多模塊組織代碼。應用層通過不同的模塊實現,然後將領域服務抽出來一個公用模塊。很多小型項目依然保持這種形態,每層能保持良好的依賴關係非常重要。每層之間最好依次向下調用,DDD 的書中有一個不好的示例,上層可以跳過中間層直接調用下層。

很多內網部署的傳統項目單機就能滿足,小型公司的 OA 軟件、餐飲軟件、會員管理系統的單機版就是通過這種方式部署。

低級別分佈式系統

將應用水平拓展,數據庫進行主從拆分,Redis 使用主從或哨兵模式,本質上和準單體系統沒有區別,應用沒有垂直拓展複雜性不會有特別大的提升。

還有一種折中的方式,應用層各個模塊單獨部署,領域層的業務邏輯單獨部署或者通過 Jar 包的方式加載應用中,實現應用層的解耦,並且不會帶來分佈式的問題。

基於上面這種模式的變體,下面這種部署方式也有很多,通過這種部署方式,領域服務使用嚴謹的 Java 實現,接入層和應用層使用 PHP、Nodejs 等動態語言實現。

高級別分佈式系統

如果我們把應用和領域層都獨立部署,就得到了現在主流的微服務架構。只不過在微服務的語境下,應用層 + 接入層被稱爲 BFF (Backend for Frontend),領域層負責實現業務邏輯,應用層用於各種業務場景下的適配。

然而這種設計會受到一些批評,他們認爲這不是正宗的微服務,而像現在所說的中臺。部分微服務的工程師倡導使用 API Gateway 的方式將領域服務的 API 直接暴露給端側。

實際上這種做法應用層並沒有消失,編排領域服務 API 的職責被下放到端側,在一些特殊的業務場景下沒有問題,但是大多數場景下並不合適。業務邏輯容易造成碎片化,存在調用次數多,服務間最終一致性事務難以實現等問題。下面這張圖表達了這種設計方式,但大多數情況下並不推薦。

到此,領域層被垂直拆分,隨之而來的就是我們熟知的各種分佈式問題了,熔斷、負載均衡等問題屬於技術複雜度可以在業務無感知的情況下被解決,但下面幾個問題需要侵入業務才能被良好的解決,因此還需要 DDD 的幫助。

我們在後面的 《DDD 指導應用垂直拆分後的問題》部分回答。

複雜分佈式系統

高級別的分佈式系統已經是業界大的互聯網公司的主流做法,不過在一些極端複雜的系統中,依然不能滿足業務需要。倒不是技術上一定要拆的非常細,主要是參與開發的人數多、代碼量大,團隊協作、版本構建有很多問題。

一個最佳的敏捷團隊爲 10 到 15 人,除去測試、業務分析師,開發者一般在 10 人左右。因此在非常複雜的系統中儘可能把能拆分的都拆出去。繼續拆分往往有兩個方向:

  1. 變得複雜的接入層,在應用層裏面兜不住了。例如 socket 連接相當費資源,可以剝離出去單獨建立連接,然後和收銀機應用通信。

  2. 一些外部系統的適配層,例如短信網關、稅務系統適配服務。

某大型 lot 平臺將對接端側的服務根據接入協議拆分,HTTP、MQTT、XMPP 然後轉換數據格式後統一送入。不過,這種場景已經比較少見。

本文由 Readfog 進行 AMP 轉碼,版權歸原作者所有。
來源https://mp.weixin.qq.com/s/1HbUtm7NtCZx-hPq1rvVqw