一文揭祕 DDD 到底解決了什麼問題

DDD 作爲架構設計思想幫助微服務控制規模複雜度,那它是怎麼做到的呢?

一、架構設計是爲了解決系統複雜度

談到架構,相信每個技術人員都是耳熟能詳,但如果深入探討一下,“爲何要做架構設計?”或者 “架構設計目的是什麼?” 類似的問題,大部分人可能從來沒有思考過,或者即使有思考,也沒有太明確可信的答案。

1.1 架構設計的誤區

1.1.1 每個系統都要做架構設計 / 公司流程要求有架構設計

知其然更要知其所以然,不能僅僅因爲其他公司都在做架構設計而盲目跟從,而應該深入理解架構設計的目的和必要性,根據實際需求進行合理的設計。如果架構師或設計師只是爲了找點事做而進行架構設計,不僅會浪費時間和人力,還會拖慢整體開發進度。此外,其他公司的架構設計並不一定適用於當前項目,如果強行引入,很可能會導致架構水土不服、運行不流暢等問題,最終需要不斷重構或者推倒重來。因此,架構師或設計師應該深刻理解爲何要進行架構設計,避免生搬硬套,針對具體需求進行合理的設計,才能保證項目的順利進行。

1.1.2 架構設計是爲了追求高性能、高可用、可擴展性等單一目標

持有這類觀點的人看似具備一定架構經驗或基礎,但實際上他們無論面對什麼系統或業務,都會不顧一切地追求這些目標,導致架構設計變得複雜、項目實施時間拖延、團隊內部不和等問題的出現。這些問題使得整個項目進展緩慢,系統穩定性差,出現問題難以解決,甚至加入新功能也需要花費大量時間。這些情況不是危言聳聽,而是廣泛出現的現象。因此,架構師或設計師必須深入瞭解系統和業務需求,根據實際需要合理設計,不可盲目追求 “高 XX” 目標,才能確保項目開發進度和系統的穩定性。

1.2 架構設計的真正目的

整個軟件技術發展的歷史,其實就是一部與 “複雜度” 鬥爭的歷史。架構也是爲了應對軟件系統複雜度而提出的一個解決方案,其主要目的是爲了解決軟件系統複雜度帶來的問題。

那到底什麼是複雜度呢?John Ousterhout 教授在 A Philosophy of Software Design 書中提到,複雜度就是任何使得軟件難於理解和修改的因素

複雜的系統有一些非常明顯的特徵,John 教授將它抽象爲變更放大(Change amplification)、認知負荷(Cognitive load)與未知的未知(Unknown unknowns)這 3 類。

變更放大(Change amplification)指得是看似簡單的變更需要在許多不同地方進行代碼修改。系統開發者之前沒有及時重構代碼,提取公共邏輯,而是省時間 Ctrl-C,Ctrl-V 式代碼開發(這樣做不會影響已有的穩定模塊,不需要做比較多的迴歸測試,上線風險小)。當需求變化時,需要改動多處代碼。

認知負荷(Cognitive load)是指系統的學習與理解成本高,開發人員的研發效率大大降低。

未知的未知(Unknown unknowns)是指不知道修改哪些代碼才能使系統功能正確的運行,也不知道這行代碼的改動是否會引發線上問題。這一項是複雜性中最糟糕的一個表現形式。

1.3 系統複雜度的六個來源及通用解法

本段參考 參考 5

  1. 高性能 2. 高可用 3. 可擴展性 4. 低成本 5. 安全 6. 規模

1.3.1 高性能

軟件系統中高性能帶來的複雜度主要體現在兩方面:

  1. 一方面是單臺計算機內部爲了高性能帶來的複雜度;

  2. 另一方面是多臺計算機集羣爲了高性能帶來的複雜度。

1.3.1.1 單機複雜度

計算機內部複雜度最關鍵的地方就是操作系統,計算機性能的發展本質上是由硬件發展驅動的,尤其是 CPU 的性能發展。而將硬件性能充分發揮出來的關鍵就是操作系統,所以操作系統本身也是隨硬件的發展而發展的,操作系統是軟件系統的運行環境,操作系統的複雜度直接決定了軟件系統的複雜度。

操作系統和性能最相關的就是進程和線程。

操作系統發展到現在,如果要完成一個高性能的軟件系統,需要考慮如多進程、多線程、進程間通信、多線程併發等技術點,而且這些技術並不是最新的就是最好的,也不是非此即彼的選擇。

在做架構設計的時候,需要花費很大的精力來結合業務進行分析、判斷、選擇、組合,這個過程同樣很複雜。例如,下面的系統都實現了高性能,但是內部實現差異很大:

1.3.1.2 集羣複雜度

讓多臺機器配合起來達到高性能的目的,是一個複雜的任務,常見的方式有:

任務可以指完整的業務處理,也可以指某個具體的任務。

  1. 任務分配:每臺機器都可以處理完整的業務任務,不同的任務分配到不同的機器上執行。

  2. 任務分解:業務越來越複雜,單臺機器處理的性能會越來越低。爲了能夠繼續提升性能,採用任務分解。

1.3.1.2.1 任務分配



業務量繼續提升,需要增加任務分配器的數量。

1.3.1.2.2 任務分解

微服務架構就採用了這種思路,通過任務分配的方式,能夠突破單臺機器處理性能的瓶頸,通過增加更多的機器來滿足業務的性能需求,但如果業務本身也越來越複雜,單純只通過任務分配的方式來擴展性能,收益會越來越低。

通過這種任務分解的方式,能夠把原來大一統但複雜的業務系統,拆分成小而簡單但需要多個系統配合的業務系統。從業務的角度來看,任務分解既不會減少功能,也不會減少代碼量(事實上代碼量可能還會增加,因爲從代碼內部調用改爲通過服務器之間的接口調用),任務分解能夠提升性能的主要原因是:

  1. 簡單的系統更容易做到高性能:系統的功能越簡單,影響性能的點就越少,就更加容易進行有針對性的優化。

  2. 可以針對單個任務進行擴展:當各個邏輯任務分解到獨立的子系統後,整個系統的性能瓶頸更加容易發現,而且發現後只需要針對有瓶頸的子系統進行性能優化或者提升,不需要改動整個系統,風險會小很多。

最終決定業務處理性能的還是業務邏輯本身,業務邏輯本身沒有發生大的變化下,理論上的性能是有一個上限的,系統拆分能夠讓性能逼近這個極限,但無法突破這個極限。

1.3.2 高可用

系統無中斷地執行其功能的能力,代表系統的可用性程度,是進行系統設計時的準則之一。

本質上都是通過 “冗餘” 來實現高可用。高可用的 “冗餘” 解決方案,單純從形式上來看,和高性能是一樣的,都是通過增加更多機器來達到目的,但其實本質上是有根本區別的:高性能增加機器目的在於 “擴展” 處理性能;高可用增加機器目的在於 “冗餘” 處理單元。

通過冗餘增強了可用性,但同時也帶來了複雜性。

1.3.2.1 計算高可用

計算的特點是無論從哪臺機器上進行計算,同樣的算法和輸入數據,產出的結果都是一樣的,所以將計算從一臺機器遷移到另一臺對業務沒有影響。

1.3.2.2 存儲高可用

對於需要存儲數據的系統而言,整個系統的高可用設計的難點和關鍵點在於 “存儲高可用”。存儲和計算的本質區別在於將數據從一臺機器搬移到另一臺機器時需要通過線路進行傳輸,而線路傳輸是存在延遲的,速度在毫秒級別,距離越遠,延遲越高。加之各種異常情況(如傳輸中斷、丟包、擁塞),會導致延遲更高。對於高可用系統來說,在某個時間點通信中斷就意味着整個系統的數據不一致。按照“數據 + 邏輯 = 業務” 的公式,數據不一致將導致最終業務表現不同。如果不做冗餘備份,系統的整體高可用性無法保證。因此,存儲高可用的難點不在於如何備份數據,而在於如何減少或規避數據不一致對業務造成的影響。

分佈式領域內著名的 CAP 定理從理論上證實了存儲高可用的複雜度。存儲高可用不可能同時滿足 “一致性、可用性、分區容忍性”,最多隻能滿足其中兩個。因此,在進行架構設計時需要結合業務進行取捨。

1.3.3 可擴展性

可擴展性指系統爲了應對將來需求變化而提供的一種擴展能力,當有新的需求出現時,系統不需要或者僅需要少量修改就可以支持,無須整個系統重構或者重建。

在軟件開發領域,面向對象思想的提出,就是爲了解決可擴展性帶來的問題;設計模式更是將可擴展性做到了極致。

設計具備良好可擴展性的系統,有兩個基本條件:

1.3.3.1 預測變化

“唯一不變的是變化”,按照這個標準衡量,架構師每個設計方案都要考慮可擴展性。預測變化的複雜性在於:

如何把握預測的程度和提升預測結果的準確性,是一件很複雜的事情,而且沒有通用的標準,更多是靠經驗、直覺。

1.3.3.2 應對變化

預測變化是一回事,採取什麼方案來應對變化,又是另外一個複雜的事情。即使預測很準確,如果方案不合適,則系統擴展一樣很麻煩。

微服務架構中的各層進行封裝和隔離也是一種應對變化的解決方式。

1.3.3.2.1 變化層 VS 穩定層

第一種應對變化的常見方案是將 “變化” 封裝在一個“變化層”,將不變的部分封裝在一個獨立的“穩定層”。

無論是變化層依賴穩定層,還是穩定層依賴變化層都是可以的,需要根據具體業務情況來設計。

無論採取哪種形式,通過剝離變化層和穩定層的方式應對變化,都會帶來兩個主要的複雜性相關的問題。

  1. 系統需要拆分出變化層和穩定層(如何拆分)

  2. 需要設計變化層和穩定層之間的接口(穩定層接口越穩定越好,變化層接口從差異中找到共同點)

1.3.3.2.2 抽象層 VS 實現層

第二種常見的應對變化的方案是提煉出一個 “抽象層” 和一個“實現層”。

抽象層是穩定的,實現層可以根據具體業務需要定製開發,當加入新的功能時,只需要增加新的實現,無須修改抽象層。這種方案典型的實踐就是策略模式。

1.3.4 低成本

在設計高性能、高可用的架構方案時,如果涉及到數百、數千甚至數萬臺服務器,成本就會成爲一個非常重要的考慮點。爲了控制成本,需要減少服務器的數量,這與增加更多服務器來提升性能和可用性的通用做法相沖突。因此,低成本往往不是架構設計的首要目標,而是一個附加約束。爲了解決這個問題,需要先設定一個成本目標,然後根據高性能和高可用的要求設計方案,並評估是否能夠滿足成本目標。如果不能,就需要重新設計架構;如果無論如何都無法設計出滿足成本要求的方案,那隻能找老闆調整成本目標了。低成本給架構設計帶來的主要複雜度體現在,往往只有 "創新" 才能達到低成本目標。"創新" 的含義是開創一個全新的技術領域,或者引入新技術來解決問題。如果沒有找到能夠解決自己問題的新技術,那麼就需要自己創造新技術了。例如,NoSQL(如 Memcache、Redis 等)是爲了解決關係型數據庫無法應對高併發訪問帶來的訪問壓力;全文搜索引擎(如 Sphinx、Elasticsearch、Solr)是爲了解決關係型數據庫 like 搜索的低效問題;Hadoop 則是爲了解決傳統文件系統無法應對海量數據存儲和計算的問題。創造新技術的主要複雜度在於需要創造全新的理念和技術,並且新技術需要與舊技術相比有質的飛躍。

1.3.5 安全

從技術的角度來講,安全可以分爲兩類:

1.3.5.1 功能安全

常見的 XSS 攻擊、CSRF 攻擊、SQL 注入、Windows 漏洞、密碼破解等,本質上是因爲系統實現有漏洞,黑客有了可乘之機,功能安全其實就是 “防小偷”。

從實現的角度來看,功能安全更多地是和具體的編碼相關,與架構關係不大。開發框架會內嵌常見的安全功能,但是開發框架本身也可能存在安全漏洞和風險。

所以功能安全是一個逐步完善的過程,而且往往都是在問題出現後纔能有針對性的提出解決方案,我們永遠無法預測系統下一個漏洞在哪裏,也不敢說自己的系統肯定沒有任何問題。

換句話講,功能安全其實也是一個 “攻” 與“防”的矛盾,只能在這種攻防大戰中逐步完善,不可能在系統架構設計的時候一勞永逸地解決。

1.3.5.2 架構安全

如果說功能安全是 “防小偷”,那麼架構安全就是 “防強盜”。

架構設計時需要特別關注架構安全,尤其是互聯網時代,理論上來說系統部署在互聯網上時,全球任何地方都可以發起攻擊。

傳統的架構安全主要依靠防火牆,防火牆最基本的功能就是隔離網絡,通過將網絡劃分成不同的區域,制定出不同區域之間的訪問控制策略來控制不同信任程度區域間傳送的數據流。

防火牆的功能雖然強大,但性能一般,所以在傳統的銀行和企業應用領域應用較多。但在互聯網領域,防火牆的應用場景並不多。

互聯網系統的架構安全目前並沒有太好的設計手段來實現,更多地是依靠運營商或者雲服務商強大的帶寬和流量清洗的能力,較少自己來設計和實現。

1.3.6 規模

規模帶來複雜度的主要原因就是 “量變引起質變”,當數量超過一定的閾值後,複雜度會發生質的變化。常見的規模帶來的複雜度有:

  1. 功能越來越多,系統複雜度指數級上升

  2. 數據越來越多,系統複雜度發生質變

1.3.6.1 功能越來越多,系統複雜度指數級上升

例如,某個系統開始只有 3 大功能,後來不斷增加到 8 大功能,雖然還是同一個系統,但複雜度已經相差很大了,具體相差多大呢?我以一個簡單的抽象模型來計算一下,假設系統間的功能都是兩兩相關的,系統的複雜度 = 功能數量 + 功能之間的連接數量,通過計算我們可以看出:3 個功能的系統複雜度爲 3+3=6

8 個功能的系統複雜度爲 8+(7+0)*8/2=36 可以看出,具備 8 個功能的系統的複雜度不是比具備 3 個功能的系統的複雜度多 5,而是多了 30,基本是指數級增長的,主要原因在於隨着系統功能數量增多,功能之間的連接呈指數級增長。下圖形象地展示了功能數量的增多帶來了複雜度。

1.3.6.2 數據越來越多,系統複雜度發生質變

隨着數據量的不斷增長,傳統的數據處理和管理方式已經無法適應,因此 “大數據” 這一概念應運而生。大數據的誕生主要是爲了解決數據規模變得越來越大時,傳統的數據收集、存儲、分析等方式無法勝任的問題。Google 的三篇技術論文,即 Google File System、Google Bigtable 和 Google MapReduce 則分別開創了大數據文件存儲、列式數據存儲和大數據運算的技術領域。即便數據規模沒有達到大數據的水平,數據增長仍然可能會給系統帶來複雜性。例如,在使用關係數據庫存儲數據時,當單表數據達到一定規模時,就會導致添加索引、修改表結構等操作變得很慢,可能需要幾個小時,這就會對業務造成不良影響。因此,必須考慮將單表拆分爲多表來解決這個問題,但這個過程也會引入更多的複雜性。

1.4 簡單的複雜度分析案例

我們來分析一個簡單的案例,一起來看看如何將 “架構設計的真正目的是爲了解決軟件系統複雜度帶來的問題” 這個指導思想應用到實踐中。

當我們設計一個大學的學生管理系統時,我們需要考慮該系統的複雜度以及如何解決這些複雜度帶來的問題。首先,我們可以將該系統的複雜度分爲以下幾個方面:

性能 該系統的訪問頻率並不高,因此性能並不是一個很大的問題。我們可以使用 MySQL 作爲存儲,Nginx 作爲 Web 服務器,無需考慮緩存。

可擴展性 該系統的功能比較穩定,可擴展的空間並不大,因此可擴展性方面也不是一個很大的問題。

高可用 數據丟失是不可接受的,故該系統的高可用性方面需要考慮多種異常情況,如機器故障、機房故障等。爲此,我們需要設計 MySQL 同機房主備方案和 MySQL 跨機房同步方案。

安全性 該系統存儲的信息涉及到學生的隱私,因此需要考慮安全性。我們可以使用 Nginx 提供的 ACL 控制、用戶賬號密碼管理和數據庫訪問權限控制來保證系統的安全性。

成本 由於該系統比較簡單,基本上幾臺服務器就可以搞定,因此成本方面並不需要太多關注。

規模 同上,規模複雜度無需過度關注。

總體來說,我們需要在架構設計中充分考慮系統的複雜度,同時根據不同問題選擇合適的解決方案,以提高系統的可靠性和安全性。

1.5 總結

第一章提出了架構的根本目是解決系統複雜度,並簡要說明系統複雜度的六個來源及通用解法,爲我們設計架構提供了清晰可執行的操作思路。

二、微服務架構解決了高可用、可擴展問題,但性能下降、成本 & 規模複雜度暴增

我們知道,這些年來隨着設備和新技術的發展,軟件的架構模式發生了很大的變化。軟件架構模式大體來說經歷了從單機、集中式到分佈式微服務架構三個階段的演進 。隨着分佈式技術的快速興起,我們已經進入到了微服務架構時代。

2.1 微服務架構的優點

與傳統單體應用架構相比,微服務架構有很多優點,具體表現如下:

2.1.1 高可用

當架構中的某一組件發生故障時,在單一進程的傳統架構下,故障很有可能在進程內擴散,導致整個應用不可用。在微服務架構下,故障會被隔離在單個服務中。若設計良好,其他服務可通過重試、平穩退化等機制實現應用層面的容錯。

2.1.4 可擴展

單個服務應用也可以實現橫向擴展,這種擴展可以通過將整個應用完整的複製到不同的節點中實現。當應用的不同組件在擴展需求上存在差異時,微服務架構便體現出其靈活性,因爲每個服務可以根據實際需求獨立進行擴展。

2.2 微服務的缺點

2.2.1 複雜度高

與單體式架構相比,微服務會導致複雜性上升,因爲多個團隊會在更多地方創建更多服務。若管理不當,則會導致開發速度和效率降低。

2.2.2 基礎設施成本呈指數級增長

每個新的微服務都有自己的成本,例如測試工具、託管基礎架構和監控工具等方面。

2.2.3 性能下降

微服務之間通過 REST、RPC 等形式進行交互,通信的時延會受到較大的影響。

三、DDD 幫助微服務控制規模複雜度

3.1 控制成本 & 規模複雜度需要明晰微服務的邊界

由 1.3.6.1 所述,隨着微服務數量上升,規模複雜度呈指數級上升,那麼我們理應控制微服務的數量,這就帶來了爭論和疑惑:微服務的粒度應該多大呀?微服務到底應該如何拆分和設計呢?

長期以來,微服務架構缺乏一套系統的理論和方法來指導其拆分,這導致了一些人對微服務架構的理解出現了一些曲解。有人簡單地認爲微服務只是將原來的單體應用程序拆分爲多個部署包或更換爲支持微服務架構的技術框架,便可成爲微服務。還有人認爲,微服務越小越好。

然而,在過去的幾年中,一些項目由於在前期過度拆分微服務,導致項目複雜度過高,無法進行上線和運維的情況已經出現。綜合來看,我認爲微服務拆分困境的根本原因在於不清楚業務或微服務的邊界在哪裏。換句話說,只有確定了業務邊界和應用邊界,這個困境才能迎刃而解。

3.2 DDD 能夠幫我們設計出清晰的領域和應用邊界

DDD 包括戰略設計和戰術設計兩部分。戰略設計主要從業務視角出發,建立業務領域模型,劃分領域邊界,建立通用語言的限界上下文,這些限界上下文可以作爲微服務設計的參考邊界。而戰術設計則從技術視角出發,着重於領域模型的技術實現,包括聚合根、實體、值對象、領域服務、應用服務和資源庫等代碼邏輯的設計和實現。

在戰略設計過程中,領域模型的建立是重中之重。爲此,DDD 提出了事件風暴這一建立領域模型的方法。事件風暴是一個從發散到收斂的過程,通常採用用例分析、場景分析和用戶旅程分析,全面分解業務領域,梳理領域對象之間的關係,這是一個發散的過程。在事件風暴過程中,會產生很多實體、命令、事件等領域對象,我們將這些領域對象從不同的維度進行聚類,形成如聚合、限界上下文等邊界,建立領域模型,這就是一個收斂的過程。

因此,DDD 可以幫助軟件工程師建立清晰的領域模型,劃分業務和應用邊界,以指導微服務的設計和拆分。事件風暴是建立領域模型的主要方法,通過其發散的過程和聚合的過程,建立起合理的領域模型,從而實現高效的軟件開發和落地。

我們可以用三步來劃定領域模型和微服務的邊界。

在從業務模型向微服務落地的過程中,即從戰略設計向戰術設計的實施過程中,我們會將領域模型中的領域對象與代碼模型中的代碼對象建立映射關係,將業務架構和系統架構進行綁定。當調整業務架構和領域模型以響應業務變化時,系統架構也會同時發生調整,並同步建立新的映射關係。這種方式可以幫助我們實現高效的軟件開發和落地,從而更好地應對業務需求變化。

因此,通過領域驅動設計的戰略設計和戰術設計,我們可以清晰地劃定領域邊界,建立領域模型,幫助我們實現微服務的設計和拆分,同時也能夠有效地響應業務變化,提高軟件開發和落地的效率。

3.3 微服務與 DDD 的異同

DDD 是一種面向複雜業務領域的設計方法論,而微服務是一種面向分佈式系統的架構風格。它們的共同目標都是通過將系統分解爲更小,更易於管理的組件來提高系統的可維護性和可擴展性。

參考

1.01 架構、複雜度與三原則:https://promacanthus.netlify.app/computer-science/architecture/01-%E6%9E%B6%E6%9E%84%E5%A4%8D%E6%9D%82%E5%BA%A6%E4%B8%8E%E4%B8%89%E5%8E%9F%E5%88%99/#011-%E7%B3%BB%E7%BB%9F%E4%B8%8E%E5%AD%90%E7%B3%BB%E7%BB%9F

2.https://zhuanlan.zhihu.com/p/372225207

3.https://www.itcast.cn/news/20220329/17575248138.shtml

4.https://zq99299.github.io/note-book2/ddd/01/01.html#%E8%BD%AF%E4%BB%B6%E6%9E%B6%E6%9E%84%E6%A8%A1%E5%BC%8F%E7%9A%84%E6%BC%94%E8%BF%9B

5.https://time.geekbang.org/column/article/6990

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