
今天繼續談下在微服務架構設計中的一些實踐和思考。對于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接口這個粒度,這是我們在實施微服務架構的時候需要重點關注的一個點。