今天繼續談下在微服務架構設計中的一些實踐和思考。對於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介面服務粒度進行。
一個完整的微服務架構你可以看到。比如有三個獨立開發團隊進行自己的微服務開發,每個團隊本身又採用前後端分離的開發模式。那麼這個時候實際上每個團隊都可以啟用自己的註冊中心和微服務閘道器,但是多個團隊之間的介面協同則必須控制到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介面這個粒度,這是我們在實施微服務架構的時候需要重點關注的一個點。