1. 程式人生 > >微服務化之服務拆分與服務發現

微服務化之服務拆分與服務發現


作者:劉超


一、服務拆分的前提


說到微服務,服務拆分是繞不過去的話題,但是微服務不是說拆就能拆的,有很多的前提條件,需要完成前面幾節所論述的部分。

首先要有一個持續整合的平臺,使得服務在拆分的過程中,功能的一致性,這種一致性不能通過人的經驗來,而需要經過大量的迴歸測試集,並且持續的拆分,持續的演進,持續的整合,從而保證系統時刻處於可以驗證交付的狀態,而非閉門拆分一段時間,最終誰也不知道功能最終究竟有沒有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開發人員的首選。

201809121135341dbfb9f9-f2ce-4fe1-bd1a-31f0350dab0a.jpg  

第二種是springcloud,springcloud為微服務而生,在dubbo已經沒有人維護的情況下,推出了支撐微服務的成熟框架。201809121135425eb17b5f-3a3c-40f6-8221-8731160efa69.jpg

dubbo vs. springcloud的對比,dubbo更加註重服務治理,原生功能不夠全面,而springcloud注重整個微服務生態,工具鏈非常全面。

20180912113548c8a92e13-2f90-4533-9646-029c2be1f81e.jpg

springcloud可定製性強,通過各種元件滿足各種微服務場景,使用springboot統一程式設計模型,能夠快速構建應用,基於註解,使用方便,但是學習門檻比較高。

Dubbo註冊到zookeeper裡面的是介面,而springcloud註冊到Eureka或者consul裡面的是例項,在規模比較小的情況下沒有分別,但是規模一旦大了,例如例項數目萬級別,介面資料就算十萬級別,對於zookeeper中的樹規模比較大,而且zookeeper是強一致性的,當一個節點掛了的時候,節點之間的資料同步會影響線上使用,而springcloud就好很多,例項級別少一個量級,另外consul也非強一致的。

第三是kubernetes,Kubernetes雖然是容器平臺,但是他設計出來,就是為了跑微服務的,因而提供了微服務執行的很多元件。

201809121136118a877a67-beb6-4a68-891c-c92c00c31de8.jpg

很多springcloud可以做的事情,kubernetes也有相應的機制,而且由於是容器平臺,相對比較通用,可以支援多語言,對於業務無侵入,但是也正因為是容器平臺,對於微服務的執行生命週期的維護比較全面,對於服務之間的呼叫和治理,比較弱,service只能滿足最最基本的服務發現需求。

因而實踐中使用的時候,往往是kubernetes和springcloud結合使用,kubernetes負責提供微服務的執行環境,服務之間的呼叫和治理,由springlcoud搞定。

20180912113626142f48f7-6de5-44d0-87e0-4422b56e3ca3.jpg

第四是service mesh,service mesh一定程度上彌補了kubernetes對於服務治理方面的不足,對業務程式碼0侵入,將服務治理下沉到平臺層,是服務治理的一個趨勢。

然而service mesh需要使用單獨的程序進行請求轉發,效能還不能讓人滿意,另外社群比較新,成熟度不足,暫時沒有達到大規模生產使用的標準。

20180912113654cde219cd-4ede-44e8-a14a-0eaa8a7e0de5.jpg  

 



 


網易雲輕舟微服務是圍繞應用和微服務打造的一站式 PaaS 平臺,幫助使用者快速實現易接入、易運維的微服務解決方案。



 

相關文章:
【推薦】 移動端推廣APP防作弊機制之依我見
【推薦】 kubernetes大概的工作原理