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