微服務化很難?一文簡單理解服務拆分與服務發現
服務拆分的前提,首先要有一個持續集成的平臺,使得服務在拆分的過程中,保持功能的一致性。
這種一致性不能通過人的經驗來,而是需要經過大量的回歸測試集,并且持續的拆分,持續的演進,持續的集成,從而保證系統時刻處于可以驗證交付的狀態。
而非閉門拆分一段時間,最終誰也不知道功能最終究竟有沒有 Bug,因而需要另外一個月的時間專門修改 Bug。
其次在接入層,API 和 UI 要動靜分離,API 由 API 網關統一的管理,這樣后端無論如何拆分,可以保證對于前端來講,是統一的入口。
而且可以實現拆分過程中的灰度發布,路由分發,流量切分,從而保證拆分的平滑進行。
拆分后的微服務之間,為了高性能,是不建議每次調用都進行認證鑒權的,而是在 API 網關上做統一的認證鑒權,一旦進入網關,服務之間的調用就是可信的。
其三對于數據庫,需要進行良好的設計,不應該有大量的聯合查詢,而是將數據庫當成一個簡單的 key-value 查詢。
復雜的聯合查詢通過應用層,或者通過 Elasticsearch 進行。如果數據庫表之間的耦合非常嚴重,那么服務拆分是拆不出來的。
其四要做應用的無狀態化,只有無狀態的應用,才能橫向擴展,這樣拆分才有意義。
服務拆分的時機
滿足了服務拆分的前提之后,那先拆哪個模塊,后拆哪個模塊呢?什么情況下,一個模塊應該拆分出來呢?
微服務拆分絕非一個大躍進運動,由高層發起,把一個應用拆分的七零八落的,最終大大增加運維成本,但是并不會帶來收益。
微服務拆分的過程,應該是一個由痛點驅動的,是業務真正遇到了快速迭代和高并發的問題。
如果不拆分,將對于業務的發展帶來影響,只有這個時候,微服務的拆分是有確定收益的,增加的運維成本才是值得的。
微服務解決的問題之一:快速迭代
互聯網產品的特點就是迭代速度快,一般一年半就能決出勝負,***一統天下,第二被***收購,其他死翹翹。
所以快速上線,快速迭代,就是生命線,而且一旦成功就是百億身家,所以無論付出多大運維成本,使用微服務架構都是值得的。
這也就是為什么大部分使用微服務架構的都是互聯網企業,因為對于這些企業來講收益明顯。
而對于很多傳統的應用,半年更新一次,企業運營相對平穩,IT 系統的好壞對于業務沒有關鍵性影響,在他們眼中,微服務化改造帶來的效果,還不如開發多加幾次班。
微服務拆分時機一:提交代碼頻繁出現大量沖突
微服務對于快速迭代的效果,首先是開發獨立,如果是一單體應用,幾百人開發一個模塊,如果使用 Git 做代碼管理,則經常會遇到的事情就是代碼提交沖突。
同樣一個模塊,你也改,他也改,幾百人根本沒辦法溝通。所以當你想提交一個代碼的時候,發現和別人提交的沖突了,于是因為你是后提交的人,你有責任去 Merge 代碼。
好不容易 Merge 成功了,等再次提交的時候,發現又沖突了,你是不是很惱火。隨著團隊規模越大,沖突概率越大。
所以應該拆分成不同的模塊,每十個人左右維護一個模塊,也即一個工程,首先代碼沖突的概率小多了,而且有了沖突,一個小組一吼,基本上問題就解決了。
每個模塊對外提供接口,其他依賴模塊可以不用關注具體的實現細節,只需要保證接口正確就可以。
微服務拆分時機二:小功能要積累到大版本才能上線,上線開總監級別大會
微服務對于快速迭代的效果,首先是上線獨立。如果沒有拆分微服務,每次上線都是一件很痛苦的事情。
當你修改了一個邊角的小功能,但是你不敢馬上上線,因為你依賴的其他模塊才開發了一半,你要等他,等他好了,也不敢馬上上線,因為另一個被依賴的模塊也開發了一半。
當所有的模塊都耦合在一起,互相依賴,誰也沒辦法獨立上線,而是需要總監協調各個團隊,大家開大會,約定一個時間點,無論大小功能,死活都要這天上線。
這種模式導致上線的時候,單次上線的需求列表非常長,風險比較大,可能小功能的錯誤會導致大功能的上線不正常。
將如此長的功能,需要一點點 Check,且要非常小心,這樣上線時間長,影響范圍大。因而這種的迭代速度快不了,頂多一個月一次就不錯了。
服務拆分后,在接口穩定的情況下,不同的模塊可以獨立上線。這樣上線的次數增多,單次上線的需求列表變小,可以隨時回滾,風險變小,時間變短,影響面小,從而迭代速度加快。
對于接口要升級部分,保證灰度,先做接口新增,而非原接口變更,當注冊中心中監控到的調用情況,發現接口已經不用了,再刪除。
微服務解決的問題之二:高并發
互聯網一個產品的特點就是在短期內要積累大量的用戶,這甚至比營收和利潤還重要,如果沒有大量的用戶基數,融資都會有問題。
因而對于并發量不大的系統,進行微服務化的驅動力差一些,如果只有不多的用戶在線,多線程就能解決問題,最多做好無狀態化,前面部署個負載均衡,單體應用部署多份。
微服務拆分時機三:橫向擴展流程復雜,主要業務和次要業務耦合
單體應用無狀態化之后,雖然通過部署多份,可以承載一定的并發量,但是資源非常浪費。
因為有的業務是需要擴容的,例如下單和支付,有的業務是不需要擴容的,例如注冊。如果一起擴容,消耗的資源可能是拆分后的幾倍,成本可能多出幾個億。
而且由于配置復雜,在同一個工程里面,往往在配置文件中是這樣組織的:這一塊是這個模塊的,下一塊是另一個模塊的。
這樣擴容的時候,一些邊角的業務,也是需要對配置進行詳細審核,否則不敢貿然擴容。
微服務拆分時機四:熔斷降級全靠 if-else
在高并發場景下,我們希望一個請求如果不成功,不要占用資源,應該盡快失敗,盡快返回,而且希望當一些邊角的業務不正常的情況下,主要業務流程不受影響。
這就需要熔斷策略,也即當 A 調用 B,而 B 總是不正常的時候,為了讓 B 不要波及到 A,可以對 B 的調用進行熔斷,也即 A 不調用 B,而是返回暫時的 fallback 數據,當 B 正常的時候,再放開熔斷,進行正常的調用。
有時候為了保證核心業務流程,邊角的業務流程,如評論,庫存數目等,人工設置為降級的狀態,也即默認不調用,將所有的資源用于大促的下單和支付流程。
如果核心業務流程和邊角業務流程在同一個進程中,就需要使用大量的 if-else 語句,根據下發的配置來判斷是否熔斷或者降級,這會使得配置異常復雜,難以維護。
如果核心業務和邊角業務分成兩個進程,就可以使用標準的熔斷降級策略,配置在某種情況下,放棄對另一個進程的調用,可以進行統一的維護。
服務拆分的方法
好了,當你覺得要將一個程序的某個部分拆分出來的時候,有什么方法可以保障平滑嗎?
首先要做的,就是原有工程代碼的標準化,我們常稱為“任何人接手任何一個模塊都能看到熟悉的面孔”。
例如打開一個 Java 工程,應該有以下的 package:
- API 接口包:所有的接口定義都在這里,對于內部的調用,也要實現接口,這樣一旦要拆分出去,對于本地的接口調用,就可以變為遠程的接口調用。
- 訪問外部服務包:如果這個進程要訪問其他進程,對于外部訪問的封裝都在這里,對于單元測試來講,對于這部分的 Mock,可以使得不用依賴第三方,就能進行功能測試。對于服務拆分,調用其他的服務,也是在這里。
- 數據庫 DTO:如果要訪問數據庫,在這里定義原子的數據結構。
- 訪問數據庫包:訪問數據庫的邏輯全部在這個包里面。
- 服務與商務邏輯:這里實現主要的商業邏輯,拆分也是從這里拆分出來。
- 外部服務:對外提供服務的邏輯在這里,對于接口的提供方,要實現在這里。
另外是測試文件夾,每個類都應該有單元測試,要審核單元測試覆蓋率,模塊內部應該通過 Mock 的方法實現集成測試。
接下來是配置文件夾,配置 profile,配置分為幾類:
- 內部配置項。(啟動后不變,改變需要重啟)
- 集中配置項。(配置中心,可動態下發)
- 外部配置項。(外部依賴,和環境相關)
當一個工程的結構非常標準化之后,接下來在原有服務中,先獨立功能模塊 ,規范輸入輸出,形成服務內部的分離。
在分離出新的進程之前,先分離出新的 jar,只要能夠分離出新的 jar,基本也就實現了松耦合。
接下來,應該新建工程,新啟動一個進程,盡早的注冊到注冊中心,開始提供服務,這個時候,新的工程中的代碼邏輯可以先沒有,只是轉調用原來的進程接口。
為什么要越早獨立越好呢?哪怕還沒實現邏輯先獨立呢?因為服務拆分的過程是漸進的。
伴隨著新功能的開發,新需求的引入,這個時候,對于原來的接口,也會有新的需求進行修改。
如果你想把業務邏輯獨立出來,獨立了一半,新需求來了,改舊的,改新的都不合適。
新的還沒獨立提供服務,舊的如果改了,會造成從舊工程遷移到新工程,邊遷移邊改變,合并更加困難。
如果盡早獨立,所有的新需求都進入新的工程,所有調用方更新的時候,都改為調用新的進程,對于老進程的調用會越來越少,最終新進程將老進程全部代理。
接下來就可以將老工程中的邏輯逐漸遷移到新工程,由于代碼遷移不能保證邏輯的完全正確,因而需要持續集成,灰度發布,微服務框架能夠在新老接口之間切換。
最終當新工程穩定運行,并且在調用監控中,已經沒有對于老工程的調用的時候,就可以將老工程下線了。
服務拆分的規范
微服務拆分之后,工程會比較的多,如果沒有一定的規范,將會非常混亂,難以維護。
首先人們經常問的一個問題是,服務拆分之后,原來都在一個進程里面的函數調用,現在變成了 A 調用 B 調用 C 調用 D 調用 E,會不會因為調用鏈路過長而使得調用相應變慢呢?
服務拆分的規范一:服務拆分最多三層,兩次調用
服務拆分是為了橫向擴展,因而應該橫向拆分,而非縱向拆成一串。也即應該將商品和訂單拆分,而非下單的十個步驟拆分,然后一個調用一個。
縱向的拆分最多三層:
- 基礎服務層:用于屏蔽數據庫,緩存層,提供原子的對象查詢接口。有了這一層,當數據層做一定改變的時候,例如分庫分表,數據庫擴容,緩存替換等。對于上層透明,上層僅僅調用這一層的接口,不直接訪問數據庫和緩存。
- 組合服務層:這一層調用基礎服務層,完成較為復雜的業務邏輯,實現分布式事務也多在這一層。
- Controller 層:接口層,調用組合服務層對外。
服務拆分的規范二:僅僅單向調用,嚴禁循環調用
微服務拆分后,服務之間的依賴關系復雜,如果循環調用,升級的時候就很頭疼,不知道應該先升級哪個,后升級哪個,難以維護。
因而層次之間的調用規定如下:
- 基礎服務層主要做數據庫的操作和一些簡單的業務邏輯,不允許調用其他任何服務。
- 組合服務層,可以調用基礎服務層,完成復雜的業務邏輯,可以調用組合服務層,不允許循環調用,不允許調用 Controller 層服務。
- Controller 層,可以調用組合業務層服務,不允許被其他服務調用。
如果出現循環調用,例如 A 調用 B,B 也調用 A,則分成 Controller 層和組合服務層兩層,A 調用 B 的下層,B 調用 A 的下層。也可以使用消息隊列,將同步調用,改為異步調用。
服務拆分的規范三:將串行調用改為并行調用,或者異步化
如果有的組合服務處理流程的確很長,需要調用多個外部服務,應該考慮如何通過消息隊列,實現異步化和解耦。
例如下單之后,要刷新緩存,要通知倉庫等,這些都不需要再下單成功的時候就要做完,而是可以發一個消息給消息隊列,異步通知其他服務。
而且使用消息隊列的好處是,你只要發送一個消息,無論下游依賴方有一個,還是有十個,都是一條消息搞定,只不過多幾個下游監聽消息即可。
對于下單必須同時做完的,例如扣減庫存和優惠券等,可以進行并行調用,這樣處理時間會大大縮短,不是多次調用的時間之和,而是最長的那個系統調用時間。
服務拆分的規范四:接口應該實現冪等
微服務拆分之后,服務之間的調用當出現錯誤的時候,一定會重試,但是為了不要下兩次單,支付兩次,需要所有的接口實現冪等。
冪等一般需要設計一個冪等表來實現,冪等表中的主鍵或者唯一鍵可以是 transaction id,或者 business id,可以通過這個 id 的唯一性標識一個唯一的操作。
也有冪等操作使用狀態機,當一個調用到來的時候,往往觸發一個狀態的變化,當下次調用到來的時候,發現已經不是這個狀態,就說明上次已經調用過了。
狀態的變化需要是一個原子操作,也即并發調用的時候,只有一次可以執行。可以使用分布式鎖,或者樂觀鎖 CAS 操作實現。
服務拆分的規范五:接口數據定義嚴禁內嵌,透傳
微服務接口之間傳遞數據,往往通過數據結構,如果數據結構透傳,從底層一直到上層使用同一個數據結構。
或者上層的數據結構內嵌底層的數據結構,當數據結構中添加或者刪除一個字段的時候,波及的面會非常大。
因而接口數據定義,在每兩個接口之間約定,嚴禁內嵌和透傳,即便差不多,也應該重新定義。
這樣接口數據定義的改變,影響面僅僅在調用方和被調用方,當接口需要更新的時候,比較可控,也容易升級。
服務拆分的規范六:規范化工程名
微服務拆分后,工程名非常多,開發人員,開發團隊也非常多,如何讓一個開發人員看到一個工程名,或者 jar 的名稱,就大概知道是干什么的,需要一個規范化的約定。
例如出現 pay 就是支付,出現 order 就是下單,出現 account 就是用戶。
再如出現 compose 就是組合層,controller 就是接口層,basic 就是基礎服務層。
出現 api 就是接口定義,impl 就是實現。pay-compose-api 就是支付組合層接口定義。account-basic-impl 就是用戶基礎服務層的實現。
服務發現的選型
微服務拆分后,服務之間的調用需要服務發現和注冊中心進行維護。主流的有如下幾種方法:
Dubbo
***是 Dubbo。Dubbo 是 SOA 架構的微服務框架的標準,已經被大量使用。
雖然中間中斷維護過一段時間,但是隨著微服務的興起,重新進行了維護,是很多熟悉 Dubbo RPC 開發人員的***。
Spring Cloud
第二種是 Spring Cloud。Spring Cloud 為微服務而生,在 Dubbo 已經沒有人維護的情況下,推出了支撐微服務的成熟框架。
Dubbo VS Spring Cloud 的對比:Dubbo 更加注重服務治理,原生功能不夠全面,而 Spring Cloud 注重整個微服務生態,工具鏈非常全面。
Spring Cloud 可定制性強,通過各種組件滿足各種微服務場景,使用 Spring Boot 統一編程模型,能夠快速構建應用,基于注解,使用方便,但是學習門檻比較高。
Dubbo 注冊到 ZooKeeper 里面的是接口,而 Spring Cloud 注冊到 Eureka 或者 Consul 里面的是實例。
在規模比較小的情況下沒有分別,但是規模一旦大了,例如實例數目萬級別,接口數據就算十萬級別,對于 ZooKeeper 中的樹規模比較大。
而且 ZooKeeper 是強一致性的,當一個節點掛了的時候,節點之間的數據同步會影響線上使用,而 Spring Cloud 就好很多,實例級別少一個量級,另外 Consul 也非強一致的。
Kubernetes
第三是 Kubernetes。Kubernetes 雖然是容器平臺,但是他設計出來,就是為了跑微服務的,因而提供了微服務運行的很多組件。
很多 Spring Cloud 可以做的事情,Kubernetes 也有相應的機制,而且由于是容器平臺,相對比較通用,可以支持多語言,對于業務無侵入。
但是也正因為是容器平臺,對于微服務的運行生命周期的維護比較全面,對于服務之間的調用和治理,比較弱,Service 只能滿足最最基本的服務發現需求。
因而實踐中使用的時候,往往是 Kubernetes 和 Spring Cloud 結合使用,Kubernetes 負責提供微服務的運行環境;服務之間的調用和治理,由 Spring Cloud 搞定。
Service Mesh
第四是 Service Mesh。Service Mesh 一定程度上彌補了 Kubernetes 對于服務治理方面的不足,對業務代碼 0 侵入,將服務治理下沉到平臺層,是服務治理的一個趨勢。
然而 Service Mesh 需要使用單獨的進程進行請求轉發,性能還不能讓人滿意,另外社區比較新,成熟度不足,暫時沒有達到大規模生產使用的標準。
福利來啦
結合自身情況請談談您對微服務架構的理解。掃描下方二維碼,關注51CTO技術棧公眾號。歡迎在技術棧微信公眾號留言探討。小編將選出留言最精彩的 10 名網友,送出《Spring Cloud微服務架構開發實戰》圖書一本~活動截止時間 9 月 20 日十二時整,特別鳴謝機械工業出版社為本次活動提供的圖書贊助。等不及送書的小伙伴,可以點擊閱讀原文直接購買。
書籍簡介
本書首先從微服務架構興起的背景講起,探討了為何在分布式系統開發中微服務架構將逐漸取代單體架構;然后對 Spring Cloud 所提供的微服務組件及解決方案進行了一一講解,從而讓讀者不但可以系統地學習 Spring Cloud 的相關知識,而且還可以全掌握微服務架構應用的設計、開發、部署和運維等知識。