今天繼續談下在微服務架構設計中的一些實踐和思考。對于SOA和微服務,我前面很多文章都進行了詳細的闡述,今天這篇文章重點還是放在一些架構設計和實踐的一些關鍵點思考上面。
微服務架構核心
再次強調,微服務架構核心是傳統單體應用大拆小,同時拆分為小的微服務后相互之間以輕量的API接口進行通信。而這個拆分本身又分了多個方面。
- 開發團隊的拆分
- 代碼層的拆分,可獨立構建打包
- 數據庫的拆分
在拆分后為了更加敏捷開發和集成,引入了DevOps和容器云技術。同時考慮和SOA,中臺思想的融合,考慮到API接口的復用性,進一步對單個微服務也進行了前后端分離開發。
從單微服務的概念來說,微服務不是指具體的Http API接口服務,而是指拆分后的微服務模塊,因此微服務可以理解為:拆分后DB+微服務模塊+API接口提供。
微服務架構思想符合當前復雜應用系統分而治之的思想,這個和微服務出來前的組件化開發思路是一致的,只是微服務思想出來后對于拆分的微服務更加高度解耦和獨立自治。
系統復雜性本身也分為了功能和非功能兩個層面。
比如一個傳統的大業務系統,類似ERP,合同管理等,業務系統足夠復雜,需要考慮進行分為治之方便后期管理和擴展。其次是非功能性需求導致的復雜性,比如一個業務系統功能并不多,但是文件存儲和獲取量巨大,那么文件服務就需要單獨拆分為微服務。
在很早以前我就強調過,微服務拆分后雖然降低了單個微服務開發實現的難度,但是增加了集成的難度,拆分的越細集成越復雜。因此如果本身不具備上面談到的復雜性需求,一個業務系統沒有必要進行微服務架構拆分和改造。
按劃分后的子域拆分數據庫
在我們實際的項目中,一個原來的單體業務系統,在進行微服務化后,實際拆分為了20個微服務模塊,那么按標準的微服務原則,應該后端也拆分為20個數據庫實例。但是這樣會導致巨大的集成復雜度和大量分布式事務處理問題。

顯然,在這種場景下我們引入業務域的概念,即應該按業務域或子域來拆分數據庫,可以多個微服務共享一個數據庫。在多個微服務共享一個數據庫實例的時候,微服務本身沒有做到完全解耦,但是也可以實現代碼層解耦。
比如某一個需求變更導致微服務A進行了變更,數據庫沒有變化,那么我們只需要持續集成和發布微服務A模塊即可。
同時在劃分業務域后也更加方便進行團隊的劃分,即開發團隊也按照業務域進行劃分,而不是一個開發團隊只負責一個微服務模塊。
微服務和微服務API接口
注意微服務和微服務模塊暴露的API接口是兩個概念,這本身也是進行微服務邊界劃分和微服務管控的兩種顆粒度。
在主流的微服務開發框架實現中,類似SpringCLoud的實現,對于Eureka,CloudGateway網關等實際都是到微服務這個粒度,也就是服務注冊和接入的是微服務模塊,而不是一個個獨立的API接口服務。一旦微服務注冊接入后,消費端通過注冊中心查找到可用的微服務后,那么該微服務通過聲明式方式暴露的所有API接口都處于可用狀態。
在微服務架構開發下,團隊實際應該有更加明確的邊界,更加粗粒度的接口暴露和交互,而不是簡單的團隊A在發現團隊B的微服務后,里面所有的API接口都可以隨意調用,這樣反而是導致了更多的內部規則外協,也加強了兩個微服務模塊之間的耦合性。
簡單來說,兩個微服務之間,不是通過API接口調用就真正解耦了,而是兩個微服務之間僅僅只有少量粗粒度的API接口交付才算真正解耦。
當前實際我們發現的一個關鍵問題就是微服務也拆分了,開發團隊也拆分了,但是多個微服務之間仍然是大量接口隨意調用,這本質仍然是一種緊耦合的架構而難以擴展。

如上圖,微服務A,D,E分別由不同的開發團隊開發,那么他們之間的邊界應該控制到具體的API接口粒度,而不是微服務粒度。比如對于微服務E只能消費微服務A暴露的第2個接口,而不能消費接口1。如果單純的采用微服務注冊中心方式,實際我們很難真正控制到API接口的粒度。或者說需要我們自己寫相應的代碼來做細粒度的安全控制。
面向API接口設計

這個是我在前面一直強調的觀點,即大型項目或傳統的大型單體應用在進行微服務化的時候,一定是架構設計先行。架構設計的關鍵工作就是:
- 微服務模塊拆分,包括數據庫拆分
- 微服務模塊暴露的API接口識別定義
當做完這兩件事情后,單個微服務才能夠真正傳遞給不同的開發團隊或小組進行獨立的設計開發工作。同時在微服務開發過程中,需要面向API接口而設計開發,要優先基于前面定義的接口契約來實現需要暴露給外部其他微服務使用的接口,其次再考慮內部功能邏輯實現。
接口先行的好處就是大家遵循同樣的一套接口契約,可以并行開始相關的設計和開發工作,只要接口契約相同,那么后續在多個微服務間集成的時候就應該沒有問題。
API接口的治理管控需要提升到相對重要的一個位置。
在微服務架構實踐中經常看到的情況就是前期架構設計不充分,相關的邊界劃分不明確,接口定義不明確,導致后期微服務在設計開發過程中持續大量的交互,同時隨意的增加和定義新的API接口,這種場景下必然帶來后續接口交互和管控治理的混亂。
比如后續在進行微服務變更的時候,我們很難快速的分析出該微服務或API接口變化究竟會影響到哪些其它的微服務模塊,微服務和API間的交互依賴關系我們也完全不清楚。
這也是我在很早就強調的一個觀點,不要期望通過后期的APM或服務鏈監控來解決微服務本身架構設計階段的不足,而是應該在前期就按自頂向下思路設計好。
構建獨立的領域組合微服務
在SOA分層架構里面可以看到,最底層是原子服務,在原子服務上面有組合服務,在組合服務上面還有流程服務。也就是說服務本身也是分層的,雖然越往上走,類似到了組合服務實際的復用度會降低,但是復用效率本身卻是加快。
在微服務架構實踐里面,原有的單體應用已經拆分為了不同的微服務,每個微服務都可以提供獨立的API接口服務能力給前端使用。
但是當前端需要的是多個微服務的組合能力的時候,這個能力究竟放在哪里?比如前面我們舉過一個例子,對于訂單提交這個操作,實際需要調用后端訂單中心,預算中心,庫存中心多個微服務接口才能夠完成。
在傳統方式下這個能力實際是在前端模塊完成組合和協同,但是你會發現你開發的應用既有傳統的BS端應用,也有APP應用,那么這個組合顯然就需要在兩個地方重復實現。同時這種組合規則本身也暴露到了前端不合理。

領域組合微服務實際上是一類比較特殊的微服務,即這類微服務本身完成多個微服務API接口的組合編排,完成分布式事務管理和協調,完成組合業務規則的實現和處理等。
這類微服務本身沒有自己獨立的Owner數據庫,也就是這類微服務不直接進行數據庫DB層的數據訪問和交互,而是直接復用已有的接口服務能力進行組合和組裝。
在DDD領域驅動設計的架構分層里面,在領域層上有一個獨立的應用層,這個應用層即和這類談到的領域組合微服務對應。而下層的領域層則由多個微服務提供粗粒度的API接口服務能力。
微服務網關和API網關
在前面我曾經專門寫過微服務網關。API網關一般具備獨立的服務注冊接入,負載均衡和路由能力,而微服務網關一般則是通過和服務注冊中心的集成來實現服務注冊發現,負載均衡和路由。
簡單來說如果當前微服務A模塊有100個接口服務。
在有服務注冊發現中心的情況下,微服務A模塊部署后會被注冊中心自動發現,并加入到可用集群列表中。因此在微服務網關和注冊中心集成后,所有的接口服務也自動的注冊和接入到了微服務網關中。
當用戶訪問網關提供的服務地址時候整體過程如下圖:

在這種場景下可以看到實際并不用一個個的API接口在網關上面注冊。但是也無法控制一個微服務哪些具體的接口要接入網關,哪些不接入。同時這里的微服務網關實際上本身也是整體微服務架構體系里面的一個微服務模塊,充當了服務消費方的角色。
也就是說APP應用無法受整體微服務框架管轄,那么對應的依賴包,代理SDK等無法下放到外部應用中,那么這部分內容實際是轉移到微服務網關上來幫助外部APP應用完成。而對于相對獨立的API網關來說,整體的注冊和接入過程是在API網關上面獨立完成的,而是是控制到API接口服務粒度進行。
當然,你也可以不采用微服務網關,直接采用類似Nginx來進行代理和路由轉發,但是這個時候需要手工進行微服務節點的配置和心跳檢測實現等。
一個完整的微服務架構你可以看到。比如有三個獨立開發團隊進行自己的微服務開發,每個團隊本身又采用前后端分離的開發模式。那么這個時候實際上每個團隊都可以啟用自己的注冊中心和微服務網關,但是多個團隊之間的接口協同則必須控制到API接口這個粒度,即多個團隊之間的接口協同采用API網關進行。
這個時候的API網關不屬于單個開發團隊管理,而屬于整個平臺層的集成能力。
共性基礎JAR包依賴

在微服務架構拆分后,各個微服務仍然會使用或依賴一些共性基礎組件,這些組件本身是獨立工程項目,可以獨立編譯構建。
同時各個微服務本身以黑盒Jar包的方式對基礎組件包進行依賴。
這類似于在各個微服務里面本身有一個基礎的內置SDK包,這個SDK包實現了一些基礎共性可復用的方法,或者對一些技術能力進行了統一封裝。
在這種場景下如果微服務B對Common包提出新需求,Common包分析后仍然是共性需求需要實現,那么Common包會重新編譯構建,并進行了版本升級。
在這種場景下,實際上微服務A和C兩個模塊的代碼沒有做任何修改,那么這個時候A和C是否需要重新進行編譯構建?
可以很明確的看到這個時候A和C不用進行編譯構建,而僅僅需要對微服務B進行編譯構建,B在構建的時候會自動獲取到最新的Common Jar包。
那么在這個場景下,實際的部署架構下是Common包多個版本共存。
為何要如此處理?
簡單來說微服務拆分后,需要做到的就是進行最小化的編譯構建和部署,來滿足業務需求的變化,能夠不重新構建的就不構建,不重新部署的就不部署。只有這樣才能夠更好的控制住變更范圍,也更加容易分析在版本部署后出現問題。
比如上圖,如果Common包升級后,微服務A也重新進行了部署構建,那么這個時候問題究竟出在哪里是很難馬上做出判斷的。
當然也存在其它的一些場景:
比如對于Common包的版本升級,雖然接口沒有變化,但是一個共性方法的實現邏輯出現了變化,這個時候必須觸發三個微服務部署目錄下的JAR包進行升級。而這個場景下本身也有兩種方式來做這個事情。
- 其一是三個微服務重新構建來獲取新版本Jar包
- 其二是將新的JAR包自動分發到三個微服務部署環境或容器中
就當前來說第一種方法很難做,往往都需要對微服務重新進行編譯構建,或者重新進行部署。也正是這個原因可以看到,當采用JAR包或SDK代理包這種方式,最大的一個問題就是版本變化的情況下的升級問題。
面向解耦而設計

前面已經談到,不是你用了Http API接口就是松耦合,如果兩個微服務模塊之間有大量的API接口交互,那么仍然是一種緊耦合的關系。
談微服務的時候你會發現,一個微服務要成功正常運行,有大量的底層技術組件或微服務依賴,也有大量的同層的其它微服務模塊API接口依賴。如果任何一個依賴的微服務出現問題,或者數據庫出現問題都會導致微服務無法正常運行。
不論現在談緩存,還是談消息中間件和事件驅動架構,你可以看到都是希望對微服務間進行解耦,對微服務和數據庫之間進行解耦。
- 對于核心的解耦思路實際在前面已經談到過,即:
- 對于查詢,采用緩存方式進行解耦
- 對于導入或CUD接口,采用消息中間件解耦

實際上面的思路和經常談到的CQRS命令查詢職責分離思路類似,通過CQRS一開始是為了更好的配合讀寫分離的數據庫使用。但是真正CQRS實現解耦的重點仍然是兩個。
其一是將命令作為事件推送到消息中間件處理,以避免出現長周期分布式事務。其次就是啟用單獨的R讀庫,可以是數據庫,也可以是緩存庫,來實現查詢功能獨立解耦和性能提升。
在實際的實踐中,不同開發團隊之間交互接口最好能夠通過消息中間件或緩存進行徹底解耦,以降低相互之間的依賴和影響。
比如對于微服務A需要推送數據到微服務B,同時需要從微服務C查詢數據。那么推送數據庫到B的接口可以實現為消息接口,先推送數據到消息中間件;而對于數據的查詢則可以在獲取數據后進行緩存等。
變更影響分析

在微服務架構實踐過程中,由于很多接口是采用Http API接口方式進行調用,很多接口修改實際并不會引起編譯構建期的錯誤。因此導致某個微服務接口修改后導致其它微服務模塊功能出現異常的情況。當出現問題后,我們才在事后進行修復。
對于服務鏈監控和鏈路跟蹤是一個事后的行為,重點是發現性能問題而不是幫你去分析服務之間的依賴關系。
因此提前梳理清楚微服務間的接口交互和依賴關系是必須的,如上圖。
通過上圖的接口交互矩陣,可以很清楚的看到當某個接口出現變化的時候,究竟會對哪些微服務模塊,哪些功能造成影響,那這些影響點就必須考慮配套的變更或者說在提交測試的時候,這些影響到的微服務模塊或功能也需要進行測試。
當然如果我們在微服務架構實施過程中,已經形成了完整的基于接口的單元測試和自動化測試,也可以更好的解決和提前發現問題。當你關注點在微服務模塊這個粒度的時候,很容易忽略微服務模塊間的交互和協同實際需要管控到API接口這個粒度,這是我們在實施微服務架構的時候需要重點關注的一個點。
原文地址:http://dockone.io/article/1282027