分布式架構原理與實踐讀書筆記
IT 軟件架構的更迭:從單體架構,到集群架構,到現在的分布式和微服務架構。
分布式架構具有分布性、自治性、并行性、全局性等特點。
為了應對請求的高并發和業務的復雜性,需要對應用服務進行合理拆分,將其從原來的大而集中變成小而分散;
要想讓這些分散的服務共同完成計算任務,就需要解決它們之間的通信與協同問題;
和服務一樣,負責存儲的數據庫也會有分散的情況,因此需要考慮分散存儲;
如果說所有的服務、數據庫都需要硬件資源作為支撐,那么對資源的管理和調度也是必不可少的;
此外,軟件系統上線以后,還需要對關鍵指標進行監控。
高并發、高可用、可伸縮、可擴展、夠安全一直都是架構設計所追求的目標。
架構的演進過程
1、應用與數據一體模式
2、應用與數據分離模式
隨著業務的發展,用戶數量和請求數量逐漸上升,服務器的性能便出現了問題。一個比較簡單的解決方案是增加資源,將業務應用和數據分開存儲。
3、緩存技術引入
隨著信息化系統的發展和互聯網使用人數的增多,業務量、用戶量、數據量都在增長。同時我們還發現,用戶對某些數據的請求量特別大,例如新聞、商品信息和熱門消息。在之前的模式下,獲取這些信息的方式是依靠數據庫,因此會受到數據庫 IO 性能的影響,久而久之,數據庫便成為了整個系統的瓶頸。而且即使再增加服務器的數量,恐怕也很難解決這個問題,于是緩存技術就登場了。
緩存技術分為客戶端瀏覽器緩存、應用服務器本地緩存和緩存服務器緩存。
● 客戶端瀏覽器緩存:
如果將每次HTTP 請求都緩存下來,就可以極大地減小應用服務器的壓力。
● 應用服務器本地緩存:
這種緩存使用的是進程內緩存,又叫托管堆緩存。以 Java 為例,這部分緩存存放在 JVM 的托管堆上面,會受到托管堆回收算法的影響。由于它運行在內存中,對數據的響應速度很快,因此通常用于存放熱點數據。當進程內緩存沒有命中時,會到緩存服務器中獲取信息,如果還是沒有命中,才會去數據庫中獲取。
● 緩存服務器緩存:
這種緩存相對于應用服務器本地緩存來說,就是進程外緩存,既可以和應用服務部署在同一服務器上,也可以部署在不同的服務器上。一般來說,為了方便管理和合理利用資源,會將其部署在專門的緩存服務器上。由于緩存會占用內存空間,因此這類服務器往往會配置比較大的內存。
加入緩存技術后,系統性能得到了提高。這是因為緩存位于內存中,而內存的讀取速度要比磁盤快得多,能夠很快響應用戶請求。特別針對一些熱點數據,優勢尤為明顯。同時,在可用性方面也有明顯改善,即使數據服務器出現短時間的故障,在緩存服務器中保存的熱點數據或者核心數據依然可以滿足用戶暫時的訪問。
4、服務器集群:處理并發
可隨著用戶請求量的增加,另外一個問題又出現了,那就是并發。
把這兩個字拆開了來看:并,可以理解為“一起并行”,有同時的意思;發,可以理解為“發出調用”,也就是發出請求的意思。
合起來,并發就是指多個用戶同時請求應用服務器。
如果說原來的系統面對的只是大數據量,那么現在就需要面對多個用戶同時請求。
服務器集群說白了,就是多臺服務器扎堆的意思,用更多服務器來分擔單臺服務器的負載壓力,提高性能和可用性。再說白一點,就是提高單位時間內服務處理請求的數量。原來是一臺服務器處理多個用戶的請求,現在是一堆服務器處理,就好像銀行柜臺一樣,通過增加柜員的人數來服務更多的客戶。
此時需要注意負載均衡器采用的均衡算法(例如輪詢和加權輪詢)要能保證用戶請求均勻地分布到多臺服務器上、屬于同一個會話的所有請求在同一個服務器上處理,以及針對不同服務器資源的優劣能夠動態調整流量。
5、數據庫讀寫分離
加入緩存可以解決部分熱點數據的讀取問題,但緩存的容量畢竟有限,那些非熱點的數據依然要從數據庫中讀取。數據庫對于寫入和讀取操作的性能是不一樣的。在寫入數據時,會造成鎖行或者鎖表,此時如果有其他寫入操作并發執行,就會出現排隊現象。而讀取操作不僅比寫入操作更加快捷,并且可以通過索引、數據庫緩存等方式實現。因此,推出了數據庫讀寫分離的方案:
設置了主從數據庫,主庫(master)主要用來寫入數據,然后通過同步binlog 的方式,將更新的數據同步到從庫(slave)中。對于應用服務器而言,在寫數據時只需要訪問主庫,在讀數據時只用訪問從庫就好了。
體會到數據庫讀寫分離帶來的益處的同時,架構設計也需要考慮可靠性的問題。例如,如果主庫掛掉,從庫如何接替主庫進行工作;之后主庫恢復了,是成為從庫還是繼續擔任主庫,以及主從庫如何同步數據。
6、反向代理和 CDN
隨著互聯網的逐漸普及,人們對網絡安全和用戶體驗的要求也越來越高。
之前用戶都是通過客戶端直接訪問應用服務器獲取服務,這使得應用服務器暴露在互聯網中,容易遭到攻擊。
如果在應用服務器與互聯網之間加上一個反向代理服務器,由此服務器來接收用戶的請求,然后再將請求轉發到內網的應用服務器,相當于充當外網與內網之間的緩沖,就可以解決之前的問題。
反向代理服務器只對請求進行轉發,自身不會運行任何應用,因此當有人攻擊它的時候,是不會影響到內網的應用服務器的,這在無形中保護了應用服務器,提高了安全性。同時,反向代理服務器也在互聯網與內網之間起適配和網速轉換的作用。
CDN,它的全稱是Content Delivery Network,也就是內容分發網絡。如果把互聯網想象成一張大網,那么每臺服務器或者每個客戶端就是分布在這張大網中的節點。節點之間的距離有遠有近,用戶請求會從一個節點跳轉到另外一個節點,最終跳轉到應用服務器獲取信息。跳轉的次數越少,越能夠快速地獲取信息,因此可以在離客戶端近的節點中存放信息。這樣用戶通過客戶端,只需要跳轉較少的次數就能夠觸達信息。
由于這部分信息更新頻率不高,因此推薦存放一些靜態數據,例如 JavaScript 文件、靜態的HTML、圖片文件等。
這樣客戶端就可以從離自己最近的網絡節點獲取資源,大大提升了用戶體驗和傳輸效率。
CDN 的加入明顯加快了用戶訪問應用服務器的速度,同時減輕了應用服務器的壓力,原來必須直接訪問應用服務器的請求,現在不需要經過層層網絡,只要找到最近的網絡節點就可以獲取資源。但從請求資源的角度來看,這種方式也有局限性,即它只對靜態資源起作用,而且需要定時對 CDN 服務器進行資源更新。反向代理和 CDN 的加入解決了安全性、可用性和高性能的問題。
7、分布式數據庫與分庫分表
隨著系統運行時間的增加,數據庫中累積的數據越來越多,同時系統還會記錄一些過程數據,例如操作數據和日志數據,這些數據也會加重數據庫的負擔。即便數據庫設置了索引和緩存,但在進行海量數據查詢時還是會表現得捉襟見肘。
如果說讀寫分離是對數據庫資源從讀寫層面進行分配,那么分布式數據庫就需要從業務和數據層面對數據庫進行分配。
● 對于數據表來說,當表中包含的記錄過多時,可將其分成多張表來存儲。
● 對于數據庫來說,每個數據庫能夠承受的最大連接數和連接池是有上限的。為了提高數據訪問效率,會根據業務需求對數據庫進行分割,讓不同的業務訪問不同的數據庫。當然,也可以將相同業務的不同數據放到不同的數據庫中存儲。
**如果將數據庫資源分別放到不同的數據庫服務器中,就是分布式數據庫設計。**由于數據存儲在不同的表/庫中,甚至在不同的服務器上面,因此在進行數據庫操作的時候會增加代碼的復雜度。此時可以加入數據庫中間件來實現數據同步,從而消除不同存儲載體間的差異。
數據庫的分布式設計以及分表分庫,會給系統帶來性能的提升,同時也增大了數據庫管理和訪問的難度。原來只需訪問一張表和一個庫就可以獲取數據,現在需要跨越多張表和多個庫。
從軟件編程的角度來看,有一些數據庫中間件提供了最佳實踐,例如 MyCat 和 Sharding JDBC。
此外,從數據庫服務器管理的角度來看,需要監控服務器的可用性。
從數據治理的角度來看,需要考慮數據擴容和數據治理的問題。
8、業務拆分
通過對前面幾個階段的學習,我們知道系統提升依靠的基本都是以空間換取時間,使用更多的資源和空間處理更多的用戶請求。隨著業務的復雜度越來越高,以及高并發的來臨,一些大廠開始對業務應用系統進行拆分,將應用分開部署。
如果說前面的服務器集群模式是將同一個應用復制到不同的服務器上,那么業務拆分就是將一個應用拆成多個部署到不同的服務器中。此外,還有的是對核心應用進行水平擴展,將其部署到多臺服務器上。
應用雖然做了拆分,但應用之間仍舊有關聯,存在相互之間的調用、通信和協調問題。
由此引入了隊列、服務注冊發現、消息中心等中間件,這些中間件可以協助系統管理分布到不同服務器、網絡節點上的應用。
業務拆分以后會形成一個個應用服務,既有基于業務的服務,例如商品服務、訂單服務,也有基礎服務,例如消息推送和權限驗證。這些應用服務連同數據庫服務器分布在不同的容器、服務器、網絡節點中,它們之間的通信、協調、管理和監控都是我們需要解決的問題。
9、分布式與微服務
微服務架構它對業務應用進行了更加精細化的切割,使之成為更小的業務模塊,能夠做到模塊間的高內聚低耦合,每個模塊都可以獨立存在,并由獨立的團隊維護。
每個模塊內部可以采取特有的技術,而不用關心其他模塊的技術實現。
模塊通過容器的部署運行,各模塊之間通過接口和協議實現調用。可以將任何一個模塊設為公開,以供其他模塊調用,也可以熱點模塊進行水平擴展,增強系統的整體性能,這樣當其中某一個模塊出現問題時,就能由其他相同的模塊代替其工作,增強了可用性。
大致總結下來,微服務擁有以下特點:業務精細化拆分、自治性、技術異構性、高性能、高可用。它像極了分布式架構,從概念上理解,二者都做了“拆”的動作,但在下面這幾個方面存在區別:
微服務架構與分布式架構的區別
1、拆分目的不同
提出分布式設計是為了解決單體應用資源有限的問題,一臺服務器無法支撐更多的用戶訪問,因此將一個應用拆解成不同的部分,然后分別部署到不同服務器上,從而分擔高并發的壓力。
微服務是對服務組件進行精細化,目的是更好地解耦,讓服務之間通過組合實現高性能、高可用、可伸縮、可擴展。
2、拆分方式不同
分布式服務架構將系統按照業務和技術分類進行拆分,目的是讓拆分后的服務負載原來單一服務的業務。
微服務則是在分布式的基礎上進行更細的拆分,它將服務拆成更小的模塊,不僅更專業化,分工也更為精細,并且每個小模塊都能獨立運行。
3、部署方式不同
分布式架構將服務拆分以后,通常會把拆分后的各部分部署到不同服務器上。
而微服務既可以將不同的服務模塊部署到不同服務器上,也可以在一臺服務器上部署多個微服務或者同一個微服務的多個備份,并且多使用容器的方式部署。
雖然分布式與微服務具有以上區別,但從實踐的角度來看,它們都是基于分布式架構的思想構建的。
可以說微服務是分布式的進化版本,也是分布式的子集。
分布式架構的一個簡單例子
此訂單業務架構分為四層:
客戶端、負載均衡器(可以稱為接入層)、應用服務器(可以稱為應用層)、數據服務器(可以稱為存儲層)
客戶端:用戶與系統之間的接口,用戶在這里進行商品瀏覽、下單等。
接入層:負載均衡器可以通過用戶 IP 將用戶的請求路由到不同的服務器集群。另外還可以進行流量控制和身份驗證等操作。
應用層:用于部署主要的應用服務,例如商品服務、訂單服務、支付服務、庫存服務和通知服務。
存儲層:數據的讀寫,主、備數據庫。
存儲可以采用分布式存儲,所謂分布式存儲就是:電商系統中商品信息的數據量比較大,為了提高訪問效率,通常會將數據分片存放,被拆分以后的商品表會分布到不同的數據庫或者服務器中。
分布式架構的特征
分布性:
將分布兩字分開來看,“分”指的是拆分,可以理解為服務的拆分、存儲數據的拆分、硬件資源的拆分。
布”指的是部署,也指資源的部署。既有計算資源,也有存儲資源。簡單來說,
分布性就是拆開了部署。
自治性:
分布性導致了自治性。簡單來說,自治性就是每個應用服務都有管理和支配自身任務和資源的能力。
并行性/并發性:
自治性導致每個應用服務都是一個獨立的個體,擁有獨立的技術和業務,占用獨立的物理資源。這種獨立能夠減小服務之間的耦合度,增強架構的可伸縮性,為并行性打下基礎。
全局性:
分布性使得服務和資源都是分開部署的,自治性說明單個服務擁有單獨的業務和資源,多個服務通過并行的方式完成大型任務。
多個分布在不同網絡節點的服務應用在共同完成一個任務時,需要有全局性的考慮。
說白了,就是分散的資源要想共同完成一件大事,需要溝通和協作,也就是擁有大局觀。
分布式架構的問題
分布式架構需要解決的問題的順序:
(1) 分布式是用分散的服務和資源代替幾種服務和資源,所以先根據業務進行應用服務拆分。
(2) 由于服務分布在不同的服務器和網絡節點上,所以要解決分布式調用的問題。
(3) 服務能夠互相感知和調用以后,需要共同完成一些任務,這些任務或者共同進行,或者依次進行,因此需要解決分布式協同問題。
(4) 在協同工作時,會遇到大規模計算的情況,需要考慮使用多種分布式計算的算法來應對。
(5) 任何服務的成果都需要保存下來,這就要考慮存儲問題。和服務一樣,存儲的分布式也可以提高存儲的性能和可用性,因此需要考慮分布式存儲的問題。
(6) 所有的服務與存儲都可以看作資源,因此需要考慮分布式資源管理和調度。
(7) 設計分布式架構的目的是實現高性能和可用性。為了達到這個目的,一起來看看高性能與可用性的最佳實踐,例如緩存的應用、請求限流、服務降級等。
(8) 最后,系統上線以后需要對性能指標進行有效的監控才能保證系統穩定運行,此時指標與監控就是我們需要關注的問題。
1、應用服務拆分
技術的實現來源于業務,那么對業務的分析就需要放在第一位。我們可以利用 DDD(Domain-Driven Design,領域驅動設計)的方法定義領域模型,確定業務和應用服務的邊界,最終引導技術的實現。按照 DDD 方法設計出的應用服務符合“高內聚、低耦合”的標準。
DDD 并不是架構,而是一種架構設計的方法論,它通過邊界劃分將業務轉化成領域模型,領域模型又形成應用服務的邊界,協助架構落地。
DDD 是一種專注于復雜領域的設計思想,其圍繞業務概念構建領域模型,并對復雜的業務進行分隔,再對分隔出來的業務與代碼實踐做映射。
主要包括:
● 領域驅動設計的模型結構:包括領域、領域分類、子域、領域事件、聚合、聚合根、實體和值對象的介紹。
● 分析業務需求形成應用服務:包括業務場景分析、抽象領域對象、劃定限界上下文。
● 領域驅動設計分層架構:包括分層原則、每層內容和特征,以及分層實例。
A、基于 DDD 思想的業務拆分實踐
完成整個應用服務的拆分需要三步,分別是分析、抽取和構建。
建立任何一個軟件架構都是為了完成業務需求,而業務需求是用來完成商業目標的。這里以構建一個學生選課系統為例,講解服務分析和抽取的整個過程。學生選課系統的業務背景如下。
● 學生可以通過系統選擇選修課,并且提交選擇選修課的申請,之后教務處負責審核。
● 教務處的老師收到選修課的申請以后,根據審批規則進行核對,最終產生審批結果:通過或者不通過。
● 獲得上選修課資格的學生,去上課的時候需要簽到,老師會檢查簽到情況,并在課程結束的時候生成簽到明細。同時學生也可以查看自己的簽到情況。
基本上可以總結為:學生申請選修課,教務處審批選修課,學生簽到并且上課老師查詢簽到記錄。
1、拆分思路
(1) 根據不同的業務場景創建業務流程,在每個業務流程的節點上標注參與者、命令和事件信息。
(2) 根據標注的參與者、命令和事件信息生成領域對象,包括實體、值對象、聚合、領域事件等。領域專家和技術團隊通過通用語言,對相關的領域對象進行進一步劃分,形成聚合并找到聚合根。
(3) 通過聚合劃定限界上下文,這里需要依賴通用語言,因為同樣一個事務在不同的限界上下文中所指的內容和含義可能有所不同。限界上下文就是服務的邊界,根據它來創建服務或者應用。
2、拆分流程
通過上面對業務需求的描述,我們可以把業務需求分為三個場景,分別是申請選修課場景、審批選修課場景和選修課簽到場景。接下來我們分別畫出這三個場景對應的業務流程圖,并且標注參與者、命令和事件信息。
3、抽取領域對象和生成聚合
通過分析業務,將需求分成了參與者、業務流程、命令和事件。然后將它們對應領域對象,生成了領域對象之間的關系。
抽取的目的是觀察領域對象之間的關聯和共性,最終對它們進行聚合和限界上下文劃分
用不同的形狀表示三個場景中的領域對象:
圓形表示實體,
長方形表示命令,
五邊形表示事件。
注意,這里只粗略地劃分領域對象,不做細分,因為目的是劃分服務和聚合的邊界。
通過抽取領域對象,可以看到:
1、選修課申請 這個實體在 申請選修課場景 和 審批選修課場景中都存在,且含義相同。
同樣,審批規則實體 和 登錄命令也都存在于申請選修課場景 和 審批選修課場景中。
2、簽到明晰 實體單獨存在于選修課簽到場景,且學生和老師都存在于三個場景中則屬于通用實體。
雖然選修課實體在三個場景中都存在,但申請選修課場景和審批選修課場景中的選修課描述的是課程本身,包括課程內容、學分;
而選修課簽到場景中的選修課更多的是關心上下課的時間、上課的位置等信息。這正是上下文不一致導致的,同一事物在不同上下文中的含義出現了偏差。
聚合是邏輯上的邊界,為限界上下文的劃分提供依據。要想生成聚合,首先需要考慮聚合的邏輯獨立性,即能否在聚合內部完成一個完整的業務邏輯。
對于前面提到的申請選修課場景、審批選修課場景、選修課簽到場景,當然可以生成三個聚合。但是考慮到前兩個場景都是在完成申請審批的業務流程,因此可以合并為一個聚合。
當然,如果業務合并在一起后顯得比較復雜,也可以進行再次拆分。
同時,選修課簽到場景可以自己生成一個聚合,其中學生、老師實體屬于組織關系,比較通用,系統中的其他地方應該也會用到這樣的概念,所以可以抽取出來作為單獨的聚合。
4、劃定限界上下文
生成的聚合劃分限界上下文,也就是生成服務的邊界。
如果說聚合是服務的邏輯邊界,那么限界上下文就是服務的物理邊界。
從完成業務的角度來看,選修課申請聚合和簽到聚合分屬不同的語義環境。
將三個聚合劃分為選修課申請、簽到、人員組織三個限界上下文。人員組織可以作為通用域,協助另外兩個子域。
選修課申請作為單獨的限界上下文,承載大部分實體、命令和事件,可以考慮將其稱為核心域。
簽到則可以作為支撐域,用來支撐核心域。
如上所示的三個限界上下文,可以由三個應用服務對應實現,分別是選修課申請服務、簽到服務、人員組織服務。
這里也體現了我們想表達的分布式應用服務的拆分概念。
當然,這種劃分不是唯一的選擇,例如選修課申請本身就是一個聚合,可以將這個聚合繼續拆分成申請和審批兩個聚合。
隨著業務的發展和變化,也可能衍生出新的限界上下文。這些需要不斷利用領域驅動設計的思想去迭代。
關于限界上下文之間的通信,可以通過領域事件的方式進行。
B、領域驅動設計分層理論及其對應的項目代碼結構
領域驅動設計分層能夠幫助我們把領域對象轉化為軟件架構。
在分解復雜的軟件系統時,分層是最常用的一種手段。
在領域驅動設計的思想中,分層代表軟件框架,是整個分布式架構的“骨架”;
領域對象是業務在軟件中的映射,好比“血肉”。
分層不僅讓我們能夠站在一個更高的位置看待軟件設計,還給整個架構帶來了高內聚、低耦合、可擴展、可復用等優勢。
1、如何分層
架構分層看上去,就是按照功能對每層進行分割和堆疊。但在具體落地時還需要考慮清楚,每層的職責以及層與層之間的依賴關系。
架構有分成三層的,也有分成四層、五層的。
業務情況、技術背景,以及團隊架構不同,分層也會有所不同。這里通過領域驅動設計的分層方式,給分布式架構提供分層思路。
從上往下分別是用戶接口層、應用層、領域層和基礎層。箭頭表示層和層之間的依賴與被依賴關系。例如,箭頭從用戶接口層指向應用層,表示用戶接口層依賴于應用層。從圖中可以看到,基礎層被其他所有層依賴,位于最核心的位置。
但這種分法和業務領導技術的理念是相沖突的,搭建分布式架構時是先理解業務,然后對業務進行拆解,最后將業務映射到軟件架構。這么看來,領域層才是架構的核心,所以上圖中的四層架構的依賴關系是有問題的。
于是出現了 DIP(Dependency Inversion Principle,依賴倒置原則),DIP 的思想指出:高層模塊不應該依賴于底層模塊,這兩者都應該依賴于抽象;抽象不應該依賴于細節,細節應該依賴于抽象。因此,作為底層的基礎層應該依賴于用戶接口層、應用層和領域層提供的接口。高層是根據業務展開的,通過對業務抽象產生了接口,底層依賴這些接口為高層提供服務。
1、用戶接口層
也稱為表現層,包括用戶界面、Web 服務和遠程調用三部分。該層負責向用戶顯示信息和解釋用戶指令。該層的主要職責是與外部用戶、系統交互,接受反饋,展示數據。
2、應用層
應用層比較簡單,不包含業務邏輯,用來協調領域層的任務和工作。應用層負責組織整個應用流程,是面向用例設計的。
通常,應用服務是運行在應用層的,負責服務組合、服務編排和服務轉發,組合業務執行順序以及拼裝結果。
并不能說應用層和業務完全無關,它以粗粒度的方式對業務做簡單組合。
具體功能有信息安全認證、用戶權限校驗、事務控制、消息發送和消息訂閱等。
3、領域層
領域層實現了應用服務的核心業務邏輯,并保證業務的正確性。這層體現了系統的業務能力,用來表達業務概念、業務狀態和業務規則。
領域層包含領域驅動設計中的領域對象,例如聚合、聚合根、實體、值對象、領域服務。領域模型的業務邏輯由實體和領域服務實現。
領域服務描述了業務操作的過程,可以對領域對象進行轉換,處理多個領域對象,產生一個結果。
領域服務和應用服務的區別是,它具有更多的業務相關性。
4、基礎層
基礎層為其他三層提供通用的技術和基礎服務,包括數據持久化、工具、消息中間件、緩存等。
例如在基礎層實現的數據庫訪問,就是面向領域層接口的。
領域層只是根據業務向基礎層發出命令,告訴它需要提供的數據規格(數據規格包括用戶名字、身份證、性別、年齡等信息),基礎層負責獲取對應的數據并交給領域層。具體如何獲取數據、從什么地方獲取數據,這些問題全部都是基礎層需要考慮的,領域層是不關心的。
領域層都面向同一個抽象的接口,這個接口就是數據規格。當數據庫的實現方式發生更換時,例如從 Oracle 數據庫換成了 MySQL 數據庫,只要基礎層把獲取數據的實現方式修改一下即可;領域層則還是遵循之前的數據規格,進行數據獲取,不受任何影響。
2、分層結構圖
從上往下看。
首先是用戶接口層,包括用戶界面、Web 服務以及信息通信功能。作為系統的入口,用戶接口層下面是應用層,這一層主要包括應用服務,但不包含具體的業務,只是負責對領域層中的領域服務進行組合、編排和轉發。
應用層下面是領域層,這一層包括聚合、實體、值對象等領域對象,負責完成系統的主要業務邏輯。領域服務負責對一個或者多個領域對象進行操作,從而完成需要跨越領域對象的業務邏輯。
用戶接口層、應用層、領域層下方和右方的是基礎層,這層就和它的名字一樣,為其他三層提供基礎服務,包括 API 網關、消息中間件、數據庫、緩存、基礎服務、通用工具等。除了提供基礎服務,基礎層還是針對通用技術的解耦。
3、服務內部的分層調用與服務間調用
將分層思想落地到分布式架構或者微服務架構,每個被拆分的應用或者服務都包含用戶接口層、應用層、領域層。那么服務內部以及服務之間是如何完成調用的呢?可以看下圖:
4、將分層映射到代碼結構
代碼結構是層次結構在代碼實現維度的映射。好的層次設計有助于設計代碼結構,好的代碼結構設計更容易讓人對整體軟件架構有清晰的理解。
1、用戶接口層的代碼結構
展示層的 VO(ViewObject)傳入到用戶接口層后,先通過 Assembler 轉換為 DTO,再由 Facade 往下傳遞。
● Assembler:起格式轉換的作用。傳入用戶接口層的數據和用戶接口層中的數據,格式有可能是不一樣的。例如展示層提交了一個表單,我們稱之為 VO(View Object,視圖對象),這個 VO 傳入用戶接口層之后需要經過 Assembler 轉換,形成用戶接口層能夠識別的 DTO 格式的數據。
● DTO(Data Transfer Object,數據傳輸對象):它是用戶接口層數據傳輸的載體,不包含業務邏輯,由 Assembler 轉換而得。DTO 可以將用戶接口層與外界隔離。
● Facade:門面,是服務提供給外界系統的接口,也是調用領域服務的入口。Facade 提供較粗粒度的調用接口,通常不包含業務邏輯,只是將用戶請求轉交給應用服務進行處理。一般地,提供API 服務的 Controller 就是一個 Facade。
2、應用層代碼結構
用戶接口層傳入的消息先轉換成 Command,然后交給 Application Service 做處理。
Application Service 負責連接領域層,調用領域服務、聚合(根)等領域對象,對業務邏輯進行編排和組裝。同時,Application Service 還協助領域層訂閱和發布 Event。
● Command:命令,可以理解為用戶所做的操作,例如下訂單、支付等,是應用服務的傳入參數。
● Application Service:應用服務,會調用和封裝領域層的 Aggregate、Service、Entity、Repository、Factory。其主要實現組合和編排,本身不實現業務,只對業務進行組合。
● Event:事件,這里主要存放事件相關的代碼,負責事件的訂閱和發布。事件的發起和響應則放在領域層處理。如果用訂報紙來舉例,那么應用層的Event 負責的是訂閱報紙和聯系發布報紙,閱讀訂閱的報紙和發布報紙的具體工作則由領域層的Event 完成。
3、領域層代碼結構
領域層的代碼結構包括一個或者多個 Aggregate(聚合)。每個 Aggregate 又包括 Entity、Event、Repository、Service、Factory 等,這些領域模型共同完成核心業務邏輯。
應用層依賴于領域層中的Aggregate 和 Service。
Aggregate 中包含Entity 和值對象。
Service 會對領域對象進行組合,完成復雜的業務邏輯。
Aggregate 中的方法和 Service 中的動作都會產生 Event。
所有領域對象的持久化和查詢都由 Repository 實現。
● Aggregate:聚合,聚合的根目錄通常由一個實體的名字來表示,例如訂單、商品。由于聚合定義了服務內部的邏輯邊界,因此聚合中的實體、值對象、方法都圍繞某一個邏輯功能展開,例如訂單聚合包括訂單項信息、下單方法、修改訂單的方法和付款方法等,其主要目的是實現業務的高內聚。由于一個服務由多個聚合組成,因此服務的拆分和擴容都可以根據聚合重新編排。
比如當服務 1 中的聚合 C 成為業務瓶頸時,可以將其擴展到服務 3 中。又或者由于業務重組,聚合 A 可以從服務 1 遷移到服務 2 中。
● Entity:實體,包括業務字段和業務方法。跨實體的業務邏輯代碼則可以放到 Service 中。
● Event:領域事件,包括與業務活動相關的邏輯代碼,例如訂單已創建、訂單已付款。作為負責聚合間溝通的工具,Event 需要實現發送和監聽事件的功能。建議將監聽事件的代碼單獨存放在listener 目錄中。
● Service:領域服務,包括需要由一個或者多個實體共同完成的服務,或者需要調用外部服務完成的功能,例如訂單系統需要調用外部的支付服務來完成支付操作。如果 Service 的業務邏輯比較復雜,可以針對每個 Service 分別設計類,遇到需要調用外部系統的地方最好采用代理類來實現,以做到最大程度的解耦。
● Repository:倉庫,其作用是持久化對象。針對數據的操作都放在這里,主要是讀取和查詢。一般來說,一個 Aggregate 對應一個 Repository。
4、基礎層
基礎層的代碼結構主要包括工具、算法、緩存、網關以及一些基礎通用類。這層的目錄存放比較隨意,根據具體情況具體決定。這里也不做具體的規定,僅給出一個例子以供參考。最上面是 infrastructure 目錄,它下面存放著config 和 util 文件夾,分別存放與配置和工具相關的代碼。
5、完整的分層結構及其代碼目錄
C、代碼分層示例
先介紹業務背景,我們要實現一個創建訂單的功能,其中每個訂單都有多個訂單項,每個訂單項分別對應一個產品,產品有對應的價格;可以根據訂單項和訂單的價格計算訂單總價,針對每個訂單設置對應的送單地址。
其中 userinterface 文件夾下面就是用戶接口層的內容,這里比較簡單,是一個Web API 的 controller,負責對外提供訪問接口,由于沒有對象轉換,所以 assembler 和 dto 文件夾是空的。
infrastructure 目錄里面存放的是基礎層的內容。由于需要定義聚合根,因此aggregate 目錄中存放的是聚合的基礎類。event 目錄中存放的是事件相關的基礎類。同樣,exception 目錄存放針對異常定義的基礎類,jackson 目錄存放針對序列化、反序列化的基礎類,repository 目錄存放數據倉庫的基礎類。
首先,請求由用戶接口層傳入,由于是創建訂單操作,所以會把 CreateOrderCommand 命令作為參數傳入 OrderController 類中。
接收到該命令以后,用戶接口層會調用應用層中的OrderApplicationService,其中的 createOrder 方法會分別調用領域層的 OrderFactory 和OrderRepository。
OrderFactory 的 create 方法可以生成聚合根 Order,然后調用 Order 中的create 方法生成訂單。
之后 Order 會調用raiseEvent 方法向其他服務發送OrderCreatedEvent,以通知其他服務訂單已創建。
createOrder 會調用 OrderRepository 中的save 方法,傳入參數是 Order,將 Order 保存到數據庫中。
應用層中的服務只負責生成聚合根 Order,然后將其保存下來。
在領域層的聚合根 Order 中,是通過 create 方法創建訂單的,在訂單生成以后才通過 raiseCreatedEvent 發送消息。
D、領域驅動設計的一些補充
如果說領域與子域的概念是從業務角度出發告訴我們如何對業務定義邊界,那么該如何劃分這個邊界,又如何將業務邊界定義到技術上呢?
答案是限界上下文。這又是什么?我們來拆分一下這個詞,“限”表示限制,“界”是邊界的意思,“限界”就是限制邊界;“上下文”是對話的語境,以一個產品為例,它在生產階段是“原料和配件”,在銷售階段是“商品”,在物流階段是“貨物”。同樣一個東西根據環境的不同被賦予了不同的意義,這個環境就是上下文。
2、分布式調用
服務與資源一旦分散開,要想調用就沒有那么簡單了。需要針對不同的用戶請求,找到對應的服務模塊,比如用戶下訂單就需要調用訂單服務。當大量用戶請求相同的服務,又存在多個服務的時候,需要根據資源分布將用戶請求均勻分配到不同服務上去。就好像用戶瀏覽商品時,有多個商品服務可供選擇,那么由其中哪一個提供服務呢?
針對調用的問題,在不同架構層面有不同的處理方式:
在用戶請求經過互聯網進入應用服務器之前,需要通過負載均衡和反向代理;
在內網的應用服務器之間需要 API 網關調用;
服務與服務之間可以通過服務注冊中心、消息隊列、遠程調用等方式互相調用。
因此可以將分布式調用總結為兩部分,
第一部分是感知對方,包括負載均衡、API 網關、服務注冊與發現、消息隊列;
第二部分是信息傳遞,包括 RPC、RMI、NIO 通信。
3、分布式協同
分布式協同顧名思義就是大家共同完成一件事,而且是一件大事。
在完成這件大事的過程中,難免會遇到很多問題。
例如,同時響應多個請求的庫存服務會對同一商品的庫存進行“扣減”,為了保證商品庫存這類臨界資源的訪問獨占性,引入了分布式鎖的概念,讓多個“扣減”請求能夠串行執行。
又例如,在用戶進行“下單”操作時,需要將“記錄訂單”(訂單服務)和“扣減庫存”(庫存服務)放在事務中處理,要么兩個操作都完成,要么都不完成。
再例如,對商品表做了讀寫分離之后,產生了主從數據庫,當主庫發生故障時,會通過分布式選舉的方式選舉出新的主庫,以替代原來主庫的工作。我們將這些問題歸納為以下幾點。
● 分布式系統的特性與互斥問題:集中互斥算法、基于許可的互斥算法、令牌環互斥算法。
● 分布式鎖:分布式鎖的由來和定義、緩存實現分布式鎖、ZooKeeper 實現分布式鎖、分段加鎖。
● 分布式事務:介紹分布式事務的原理和解決方案。包括 CAP、BASE、ACID 等的原理;DTP 模型;2PC、TCC 方案。
● 分布式選舉:介紹分布式選舉的幾種算法,包括Bully 算法、Raft 算法、ZAB 算法。
● 分布式系統的實踐: ZooKeeper
4、分布式計算
針對海量數據的計算,分布式架構通常采用水平擴展的方式來應對挑戰。在不同的計算場景下計算方式會有所不同,計算模式分為兩種:
針對批量靜態數據計算的 MapReduce 模式,
以及針對動態數據流進行計算的 Stream 模式。
5、分布式存儲
簡單理解,存儲就是數據的持久化。從參與者的角度來看,數據生產者生產出數據,然后將其存儲到媒介上,數據使用者通過數據索引的方式消費數據。
從數據類型上來看,數據又分為結構化數據、半結構化數據、非結構化數據。在分布式架構中,會對數據按照規則分片,對于主從數據庫還需要完成數據同步操作。如果要建立一個好的數據存儲方案,需要關注數據均勻性、數據穩定性、節點異構性以及故障隔離幾個方面。
● 數據存儲面臨的問題和解決思路:RAID 磁盤陣列。
● 分布式存儲的要素和數據類型分類。
● 分布式關系數據庫:分表分庫、主從復制、數據擴容。
● 分布式緩存:緩存分片算法、Redis 集群方案、緩存節點之間的通信、請求分布式緩存的路由、緩存節點的擴展和收縮、緩存故障的發現和恢復。
6、分布式資源管理與調度
如果把每個用戶請求都看成系統需要完成的任務,那么分布式架構要做的就是對任務與資源進行匹配。
● 分布式調度的由來與過程。
● 資源劃分和調度策略。
● 分布式調度架構。
● 中心化調度的特點是由一個網絡節點參與資源的管理和調度。
● 兩級調度在單體調度的基礎上將資源的管理和調度從一層分成了兩層,分別是資源管理層和任務分配層。
● 共享狀態調度,通過共享集群狀態、共享資源狀態和共享任務狀態完成調度工作。
● 資源調度的實踐:如使用 Kubernetes 的架構及其各組件的運行原理。
7、高性能與可用性
高性能和可用性本身就是分布式架構要達成的目的。分布式架構拆分和分而治之的思想也是圍繞著這個目的展開的。這部分主要從緩存、可用性兩個方面展開。
在分布式架構的每個層面和角度,都可以利用緩存技術提高系統性能。由于技術使用比較分散,
對于可用性來說,為了保證系統的正常運行會通過限流、降級、熔斷等手段進行干涉。
● 緩存的應用:HTTP 緩存、CDN 緩存、負載均衡緩存、進程內緩存、分布式緩存。
● 可用性的策略:請求限流、服務降級、服務熔斷。
8、指標與監控
判斷一個架構是好是壞時,有兩個參考標準,即性能指標和可用性指標,分布式架構也是如此。
性能指標又分為吞吐量、響應時間和完成時間。
由于系統的分布性,服務會分布到不同的服務器和網絡節點,因此監控程序需要在不同的服務器和網絡節點上對服務進行監控。在分布式監控中會提到監控系統的分類、分層以及 Zabbix、Prometheus、ELK 的最佳實踐。
● 性能指標:延遲、流量、錯誤、飽和度。
● 分布式監控系統:創建監控系統的步驟、監控系統的分類、監控系統的分層。
● 流行監控系統的最佳實踐:包含 Zabbix、Prometheus。
架構設計總結
架構設計思維、模型
代碼重構
務量的增加一般會導致系統架構的代碼量增加,代碼暴露的問題也隨之增多。原來為了搶著上線在代碼中留下的坑,終于要自己填了。為了提高代碼質量,讓業務走得更遠,設計師加大了代碼審核和重構的力度。
● 何時重構是一個有趣的問題。通常,在我們開始編碼的時候,就應該對代碼架構和組件模型進行設計。但是由于種種原因,基本上采取的都是牛仔式編程,即想到哪里就寫到哪里,之后踩了坑才明白應該時刻對代碼進行重構。針對這一點,可以有以下幾個方面的參考:
? ● 事不過三原則:
? 在你第 1 次寫代碼實現某業務功能的時候,沒有做設 計,姑且就這么寫了;第 2 次遇到相似的功能,發現這 個功能之前好像用過,于是又寫一遍;第 3 次又遇到了 同樣的功能,這時就要告訴自己需要重構了。這個原則 有大量的應用場景,特別是開發應用時,遇到一些通用 的業務組件或者系統組件抽取的時候。
? ● 添加功能時重構:
? 當你往舊模塊里添加新功能的時候,發現這個新功能原來 可以由幾個原有功能組合完成,但是那幾個原有功能的通 用性不太好,于是重構原有功能,讓其具有更強的復用性
? ● 修復 bug 時重構:
? 程序員在修復完 bug 之后,往往會有非常深的滿足感。如 果在修復以后能分析一下 bug 出現的原因,檢查一下其他 地方是否也存在相同的 bug,是否能夠通過通用組件抽取的 方式徹底解決 bug,那么代碼重構就顯得非常有意義了
? ● 審核代碼時重構:
? 這在極限編程和結對編程中比較多見。一個程序員寫代 碼,另一個程序員審核代碼,二人教學相長,共同進步。 不同的人擁有不同的背景、思路以及理解深度,因此協作 編寫同一段代碼會使代碼顯得更加立體,此時的重構是高 效的
性能測試與壓力測試
如果說性能測試的結果是系統的基準線,那么壓力測試的結果就是系統的上限或者高壓線。基準線到高壓線之間就是系統可以伸縮的范圍,我們通過這兩條線密切關注系統的負載情況。