歡迎光臨
每天分享高質量文章

如何做好服務拆分?

本文章為《網際網路高併發微服務化架構實踐》系列文章的第六篇,前五篇為:

服務拆分的前提

說到微服務,服務拆分是繞不過去的話題,但是微服務不是說拆就能拆的,有很多的前提條件,需要完成前面幾節所論述的部分。
首先要有一個持續整合的平臺,使得服務在拆分的過程中,功能的一致性,這種一致性不能透過人的經驗來,而需要經過大量的回歸測試集,並且持續的拆分,持續的演進,持續的整合,從而保證系統時刻處於可以驗證交付的狀態,而非閉門拆分一段時間,最終誰也不知道功能最終究竟有沒有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是SOA架構的微服務框架的標準,已經被大量使用,雖然中間中斷維護過一段時間,但是隨著微服務的興起,重新進行了維護,是很多熟悉Dubbo RPC開發人員的首選。

第二種是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雖然是容器平臺,但是他設計出來,就是為了跑微服務的,因而提供了微服務執行的很多元件。

很多Spring Cloud可以做的事情,Kubernetes也有相應的機制,而且由於是容器平臺,相對比較通用,可以支援多語言,對於業務無侵入,但是也正因為是容器平臺,對於微服務的執行生命週期的維護比較全面,對於服務之間的呼叫和治理,比較弱,Service只能滿足最最基本的服務發現需求。
因而實踐中使用的時候,往往是Kubernetes和Spring Cloud結合使用,Kubernetes負責提供微服務的執行環境,服務之間的呼叫和治理,由Spring Cloud搞定。

第四是Service Mesh,Service Mesh一定程度上彌補了kubernetes對於服務治理方面的不足,對業務程式碼0侵入,將服務治理下沉到平臺層,是服務治理的一個趨勢。
然而Service Mesh需要使用單獨的行程進行請求轉發,效能還不能讓人滿意,另外社群比較新,成熟度不足,暫時沒有達到大規模生產使用的標準。

本文轉載自公眾號: 劉超的通俗雲端計算,點選檢視原文

Kubernetes實戰培訓

Kubernetes應用實戰培訓將於2018年10月12日在深圳開課,3天時間帶你係統學習Kubernetes本次培訓包括:容器基礎、Docker基礎、Docker進階、Kubernetes架構及部署、Kubernetes常用物件、Kubernetes網路、儲存、服務發現、Kubernetes的排程和服務質量保證、監控和日誌、Helm、專案實踐等,點選下方圖片檢視詳情。

贊(0)

分享創造快樂