去哪兒旅行微服務(wù)架構(gòu)實(shí)踐
你好,我是朱仕智,在去哪兒網(wǎng)負(fù)責(zé)基礎(chǔ)架構(gòu),主要包含后端架構(gòu)、大前端架構(gòu)、質(zhì)量保障、基礎(chǔ)云平臺(tái)等工作,近期主要在公司落地云原生和數(shù)字化管理。
今天我?guī)淼闹黝}是去哪兒旅行微服務(wù)架構(gòu)實(shí)踐。我將從以下幾個(gè)方面進(jìn)行介紹:
- 背景介紹
- 微服務(wù)架構(gòu)模式的最佳實(shí)踐
- 微服務(wù)開發(fā)效率的提升實(shí)踐
- 微服務(wù)治理的實(shí)踐
- ServiceMesh 嘗試
一、背景介紹
首先介紹一下去哪兒網(wǎng)的業(yè)務(wù)。去哪兒網(wǎng)是一個(gè)典型的在線旅游平臺(tái),它上面的業(yè)務(wù)繁多,有機(jī)票、酒店、度假、火車票、汽車票等等。
這些業(yè)務(wù)都有不同的業(yè)務(wù)流程,其中機(jī)票的標(biāo)準(zhǔn)化和線上化是最高的,但是像酒店這樣的業(yè)務(wù),在線化和標(biāo)準(zhǔn)化就比較低,同樣的名字可能是不一樣的酒店。這些業(yè)務(wù)在從商品、庫存到整個(gè)交易過程其實(shí)都是不一樣的,所以這些業(yè)務(wù)從背后來看還是相對(duì)比較復(fù)雜的。
我們?yōu)槭裁匆x擇微服務(wù),其實(shí)有以下幾個(gè)方面的原因。第一個(gè)就是業(yè)務(wù)逐漸復(fù)雜,最早去哪兒網(wǎng)其實(shí)只有機(jī)票的比價(jià),而且是一個(gè)搜索比價(jià),是沒有交易環(huán)節(jié)的。后來業(yè)務(wù)擴(kuò)展就慢慢地發(fā)展出來了包含機(jī)票、酒店、火車票、度假、汽車票等等其他的業(yè)務(wù)。
所以業(yè)務(wù)是逐漸復(fù)雜的一個(gè)過程,那按照康威定律大家都知道,業(yè)務(wù)變化了之后,組織結(jié)構(gòu)要進(jìn)行相應(yīng)的調(diào)整,組織架構(gòu)其實(shí)也會(huì)跟著相應(yīng)的膨脹,膨脹也會(huì)帶來協(xié)作上和分工上的一定損耗,這也是我們要選擇微服務(wù)的原因之一。
第三個(gè)就是開發(fā)效率的低下,我們之前開發(fā)的時(shí)候大部分都是以最早的模式,也就是通過 HTTP 協(xié)議,加上 JSON 這樣的數(shù)據(jù)結(jié)構(gòu),然后使用 Nginx 作為網(wǎng)關(guān),把服務(wù)治理的這些動(dòng)作全部耦合在業(yè)務(wù)代碼里面,比如重試的邏輯等等。這樣的話就會(huì)導(dǎo)致我們每一個(gè)服務(wù)做對(duì)應(yīng)開發(fā)的時(shí)候,都需要重復(fù)性地去考慮這些問題,開發(fā)效率相對(duì)就會(huì)比較低下。
第四個(gè)就是服務(wù)質(zhì)量是比較失控的,因?yàn)檫@些服務(wù)質(zhì)量很難能在統(tǒng)一的一個(gè)地方去得到比較有效、及時(shí)地處理,就像剛才說的治理的邏輯其實(shí)是放在了業(yè)務(wù)代碼里面,有一些治理邏輯可能會(huì)放在 Nginx 里面,但是 Nginx 是一個(gè)大統(tǒng)一的網(wǎng)關(guān),這就意味著當(dāng)我們想要去對(duì)它進(jìn)行修改的時(shí)候,其實(shí)是需要非常謹(jǐn)慎的,這就面臨了一個(gè)運(yùn)維和開發(fā)訴求不對(duì)等的問題。使用微服務(wù)我們認(rèn)為是可以比較有效地解決這些問題的。
接著介紹一下我們?nèi)ツ膬壕W(wǎng)的在線數(shù)據(jù)。我們現(xiàn)在的應(yīng)用數(shù)據(jù)是這樣的:活躍的、在線跑著的應(yīng)用大概有 3000 多個(gè);提供了 18,000 多個(gè) Dubbo 的 RPC 服務(wù)接口;有超過 3500 個(gè) HTTP 域名;13,000 多個(gè) MQ 的主題;公司內(nèi)部大概有 5 種語言的技術(shù)棧,當(dāng)然主要是以 Java 和 Node 為主。
二、微服務(wù)架構(gòu)模式的最佳實(shí)踐
接下來介紹一下架構(gòu)模式,架構(gòu)模式里面有幾個(gè)方面不同的范疇。
1. 服務(wù)發(fā)現(xiàn)模式
第一個(gè)就是服務(wù)發(fā)現(xiàn)的模式,服務(wù)發(fā)現(xiàn)里面其實(shí)有三種模式,這三種模式對(duì)應(yīng)不同的適用場(chǎng)景會(huì)有不同的效果。
直聯(lián)模式,客戶端從注冊(cè)中心發(fā)現(xiàn)服務(wù)端的列表并緩存在本地,這種模式適合于語言統(tǒng)一的這種內(nèi)網(wǎng)通信,為什么呢?因?yàn)橹边B模式里面大部分 RPC 采用的這樣的模式,主要是比較簡(jiǎn)單、高效,而且在統(tǒng)一語言的內(nèi)網(wǎng)通信里面,這種服務(wù)端的實(shí)例的變更通知是比較簡(jiǎn)單的。
代理模式,服務(wù)端注冊(cè)到網(wǎng)關(guān)上,客戶端對(duì)一個(gè)服務(wù)端其實(shí)是無感知的,這種模式比較適合于外網(wǎng)服務(wù),為什么呢?是因?yàn)楫?dāng)你的服務(wù)端變更的時(shí)候,客戶端其實(shí)是不需要去感知,也不需要對(duì)此進(jìn)行任何變更,這樣對(duì)外網(wǎng)來說,其實(shí)用戶側(cè)的設(shè)備是不需要去關(guān)注信息的,這樣通知起來就比較簡(jiǎn)單。但是它也會(huì)面臨一個(gè)問題,它會(huì)多一跳的通信,從性能或者效率上來說,肯定是不如直連模式的。
最后一個(gè)就是邊車模式,Sidecar 去負(fù)責(zé)注冊(cè)和發(fā)現(xiàn),應(yīng)用程序是無感知的,這種比較適合于多語言、多協(xié)議的這種內(nèi)網(wǎng)通信,它其實(shí)跟直連模式相對(duì)來說是比較相似的,但是它其實(shí)是由邊車的模式替代了業(yè)務(wù)程序里面混入的這種基礎(chǔ)功能,所以簡(jiǎn)單來看其實(shí)就是直連模式里面把公共的基礎(chǔ)設(shè)施的邏輯下沉到了邊車?yán)锩?。這樣的話邊車就可以統(tǒng)一地配合我們的灰度發(fā)布或者是其他的熱更新的機(jī)制,能夠做到比較容易地去對(duì)這些邊車進(jìn)行升級(jí)。
2. 服務(wù)通信模式
接下來我們說一下服務(wù)通信的模式,服務(wù)通信模式里面主要有兩種,大家其實(shí)日常里面比較經(jīng)常會(huì)碰到就是同步的編程模式,這種模式比較簡(jiǎn)單易懂,非常符合人類的思考習(xí)慣,它比較適用于時(shí)間比較敏感的、吞吐量也比較小的這種場(chǎng)景。但是這種通信的方式在吞吐量比較大、QPS 比較高的場(chǎng)景里面就會(huì)有一系列的問題,比如說可能會(huì)把你的資源耗盡,但其實(shí)這些資源都處于等待中。比如我們?cè)?Java 里面可能會(huì)有線程池的資源,使用起來其實(shí)是比較低效的。然后在異步的這種場(chǎng)景里面,它其實(shí)比較適用于高吞吐、削峰填谷的作用。
其實(shí)這里面會(huì)有幾種,從我們的實(shí)踐上來看的話,比如說搜索系統(tǒng)它其實(shí)是一個(gè)非常高并發(fā)的場(chǎng)景,其實(shí)對(duì)于這種高吞吐的場(chǎng)景下是必須要用異步的,不然的話其實(shí)資源的損耗是非常高的,我們?cè)谀承┫到y(tǒng)上做過改造,由原來的同步改為異步的話,基本上可以節(jié)省掉 80% 左右的機(jī)器的資源。除此之外,交易系統(tǒng)的事件驅(qū)動(dòng)也是比較適合異步的一個(gè)場(chǎng)景,因?yàn)榻灰紫到y(tǒng)的事件其實(shí)是非常關(guān)鍵的,但是它又不能每個(gè)人都去通知,因?yàn)楹芏嗳硕夹枰P(guān)注這個(gè)事件,這個(gè)時(shí)候利用 MQ 等方式去做這種事件的驅(qū)動(dòng)是比較合適的。
封裝異步 HttpClient
然后在異步的這個(gè)場(chǎng)景里面,去哪兒網(wǎng)其實(shí)做了一些自己內(nèi)部的一些支持,比如說我們封裝了異步的 HttpClient,把公司內(nèi)部其他的組件類似于 QTrace,還有一些其他基礎(chǔ)的監(jiān)控、日志等等之類的組件都做了統(tǒng)一的封裝埋點(diǎn)。
改善 Dubbo 異步通信
第二個(gè)我們對(duì) Dubbo 的異步通信進(jìn)行了改善,Dubbo 里面原有的幾種通信方式,其實(shí)是調(diào)用端和被調(diào)用端,是會(huì)存在一定的耦合邏輯的。比如說像參數(shù)回調(diào)這樣的方式,其實(shí)是調(diào)用端需要進(jìn)行異步,但是被調(diào)用端不得不配合這個(gè)方式進(jìn)行改造,所以在這種背景下,我們對(duì) Dubbo 的異步通信進(jìn)行了魔改,其實(shí)現(xiàn)在的最新版的 Dubbo 的模式里面,跟這個(gè)是比較相似的。
自研可靠事務(wù)消息隊(duì)列 QMQ
第三個(gè)就是我們其實(shí)內(nèi)部做了一個(gè)自研的消息隊(duì)列叫 QMQ,它其實(shí)支持可靠的事務(wù)消息,廣泛地應(yīng)用在我們?nèi)ツ膬壕W(wǎng)的交易系統(tǒng)里面。
3. 協(xié)議
第三個(gè)主要提一下協(xié)議這部分,我們?cè)诠纠锩嬷饕腥N協(xié)議。第一種私有協(xié)議,主要負(fù)責(zé) App 和外網(wǎng)網(wǎng)關(guān)之間的通信協(xié)議;第二個(gè) HTTP 協(xié)議,主要是外網(wǎng)網(wǎng)關(guān)到 Node、Node 到 Java 之間,甚至有一些 Java 到 Java 之間也會(huì)有自己使用的這種 HTTP 協(xié)議,不過這種量其實(shí)是比較少的;第三個(gè) Dubbo 協(xié)議,后端的 Java 服務(wù)之間的通信基本上都是用 Dubbo 為主,只有少量的使用 HTTP。
4. 設(shè)計(jì)模式
從設(shè)計(jì)模式上來說的話,我們其實(shí)可以知道在互聯(lián)網(wǎng)的架構(gòu)里面,特別是在高并發(fā)的模式里面,我們有很多折中,這些折中里面其實(shí)會(huì)有不同的模式和它的沉淀。比如說像 BASE 這樣的模式,它其實(shí)不追求強(qiáng)一致性,它是有這種基本的可用和軟狀態(tài)這樣的優(yōu)點(diǎn),進(jìn)而去避免因?yàn)閺?qiáng)一致導(dǎo)致的其他的不可用性。
第二個(gè)就是 CQRS,這個(gè)模式其實(shí)非常有用,至少我發(fā)現(xiàn)很多場(chǎng)景是能夠用上它的,換句話說其實(shí)只要是數(shù)據(jù)異構(gòu)的這種場(chǎng)景,都是比較適合去使用它的,當(dāng)然這取決于你的查詢模式。大家都知道查詢模式其實(shí)有很多種的,比如說像 KV 的查詢模式、復(fù)雜條件的 Query,除此之外,還有 Scan 這種掃描形式,不同的查詢形式會(huì)對(duì)應(yīng)著不同的存儲(chǔ)結(jié)構(gòu)是比較合適的。但是我們?cè)趯?duì)這些數(shù)據(jù)進(jìn)行操作的時(shí)候,其實(shí)它的數(shù)據(jù)載體是唯一的,那這個(gè)數(shù)據(jù)載體怎么樣才能支持多種的查詢模式呢?其實(shí)這里面就需要對(duì)這些數(shù)據(jù)進(jìn)行異構(gòu),比如說像我們的訂單、配置等等這些方式都需要去進(jìn)行一定的異構(gòu)。
比如說像去哪兒網(wǎng)內(nèi)部的話,代理商在去哪兒網(wǎng)上就可以進(jìn)行一定的調(diào)價(jià),調(diào)價(jià)的配置其實(shí)就是一個(gè)比較適合去做數(shù)據(jù)異構(gòu)的場(chǎng)景。代理商去錄入的時(shí)候是比較復(fù)雜的,但其實(shí)是從航空公司拿到的一個(gè)配置,當(dāng)它放到平臺(tái)上來的時(shí)候,也是用同樣的方式去放,但是對(duì)于檢索來說的話,用戶其實(shí)關(guān)心的是這個(gè)城市,到這個(gè)城市的時(shí)候,你的調(diào)價(jià)規(guī)則是什么樣子,他并不需要一個(gè)大一統(tǒng)的調(diào)價(jià)規(guī)則。所以這里面就會(huì)面臨一個(gè)數(shù)據(jù)異構(gòu)的過程,我們?cè)谶@個(gè)過程里面其實(shí)也使用了 CQRS 這個(gè)模式來解決問題。
三、微服務(wù)開發(fā)效率提升實(shí)踐
然后我來說一下效率提升的這部分,大家都知道業(yè)界 Spring Cloud 在近期或者是近幾年來說是一個(gè)最佳實(shí)踐,特別是在微服務(wù)比較火之后,大家亟需一套成型的解決方案。這個(gè)里面包含不同的功能,比如說像分布式的配置、服務(wù)的注冊(cè)、發(fā)現(xiàn)、通信,還有服務(wù)的熔斷、服務(wù)調(diào)用、負(fù)載均衡、分布式消息等等。其實(shí)大家可以看到官方的一個(gè)實(shí)現(xiàn),當(dāng)然實(shí)現(xiàn)基本上都是來源于 Netflix 的,這里面會(huì)有不同的這些組件,但這些組件其實(shí)很多時(shí)候可能有一些已經(jīng)不再維護(hù)了。
對(duì)應(yīng)地可以看到 Spring Cloud Alibaba 也有自己的實(shí)現(xiàn),像 Nacos、Sentinel、Dubbo、RocketMQ 等等。我們其實(shí)就在思考著去哪兒網(wǎng)自己有這么多自研的組件,是否能夠適配 Spring Cloud 這樣的一套標(biāo)準(zhǔn),進(jìn)而去達(dá)到開發(fā)提效、互相串通組件的目的?
1.Spring Cloud Qunar
我們做了一個(gè)嘗試,基于 Spring Cloud 做了配置中心、注冊(cè)中心、服務(wù)治理等等之類的組件的串通,這樣的話能夠做到比較好的開發(fā)模式。然后值得一提的是我們?cè)?Spring Cloud Qunar 里面,其實(shí)提供了兩種通信的模式,一種是前面提到的直聯(lián)模式,就是由應(yīng)用本身包含的 SDK 來負(fù)責(zé)注冊(cè)、發(fā)現(xiàn)和通信。除此之外,我們還有一個(gè)模式是基于 Sidecar 的這種 Mesh 模式,我們也可以由 Mesh 的 Sidecar 去負(fù)責(zé)注冊(cè)、發(fā)現(xiàn)和通信,這兩者之間的開啟其實(shí)是比較簡(jiǎn)單的,只需要有一些特定的注解就可以開啟 Mesh 模式。
大家可以看到這里面,比如上面的代碼,有 Dubbo Service 這樣的一個(gè)服務(wù)的提供,下面就會(huì)有 Dubbo Reference 這樣的一個(gè)服務(wù)的引用,并且在注解里大家可以看到 Qunar Mesh 這樣的一個(gè)注解,這個(gè)注解就是用于開啟我們的 Mesh 功能的,是對(duì)于 Dubbo 這個(gè)協(xié)議的。對(duì)于 HTTP 協(xié)議的話,其實(shí)跟官方的也是非常類似,我們也是使用了 OpenFeign 這樣的一個(gè)組件來進(jìn)行通信,下面也同樣會(huì)有 Qunar Mesh 組件進(jìn)行 Mesh 化。
2. 開發(fā)插件
下面說一下開發(fā)插件,我們?yōu)槭裁匆鲩_發(fā)插件,以及開發(fā)插件為什么能夠做到效率上的提升呢?其實(shí)這里面的話,我們分析了大量的業(yè)務(wù)研發(fā)的開發(fā)模式,能夠發(fā)現(xiàn)存在一些重復(fù)性或者是低效的環(huán)節(jié),比如說像手動(dòng)編寫很多的調(diào)用代碼,甚至可能會(huì)出現(xiàn)要手寫這些反序列化類等等。
第二個(gè)就是在交互的過程中大量地去使用類似于文檔,或者是內(nèi)部的 IM,甚至比如說大家做的比較好的場(chǎng)景下是有 apiDoc 這樣的方式去溝通這些接口的語義和細(xì)節(jié)。
第三個(gè)就是服務(wù)上線之后才去考慮治理,這個(gè)里面就會(huì)面臨開發(fā)和運(yùn)維的不對(duì)等。你的服務(wù)上線了后,它不出問題時(shí),其實(shí)你是很少會(huì)去考慮治理的,只有在你開發(fā)的時(shí)候可能會(huì)有一定的考慮,但是這個(gè)考慮其實(shí)不是基于真實(shí)數(shù)據(jù)的。比如說你設(shè)置一個(gè)超時(shí)時(shí)間,大家經(jīng)常能夠在代碼里面看到 1 秒、30 秒、60 秒等等之類的數(shù)字,這些數(shù)據(jù)真的有意義嗎?不一定,只是大家習(xí)慣性地這么寫,然后還有成百上千個(gè) HttpClient Wrapper,就是自己不停地去實(shí)現(xiàn)這些 HttpClient,這些都是一些開發(fā)比較低效的場(chǎng)景,我們?cè)趺唇鉀Q這個(gè)問題呢?
我們其實(shí)做了一個(gè)基于 idea 的 IDE 的開發(fā)插件。開發(fā)插件它可以滿足以下的幾個(gè)功能,比如像服務(wù)調(diào)用的代碼自動(dòng)生成,這個(gè)是一個(gè)什么樣的場(chǎng)景?是說當(dāng)你在 IDE 里面打開我這個(gè)插件,你就可以選擇對(duì)方的應(yīng)用、對(duì)方提供的服務(wù),直接就一鍵生成調(diào)用的代碼,甚至包括一些其他 jar 包的引入,比如如果它是 Dubbo 協(xié)議的,它會(huì)自動(dòng)引入這些 Dubbo 的 SDK 和對(duì)方提供的這些 API 的 jar 包等等。
第二它可以快速地發(fā)現(xiàn)這些應(yīng)用接口方法,集成對(duì)應(yīng)的文檔服務(wù),這個(gè)就是剛才提到的我們其實(shí)打開了這個(gè)插件,就能快速地去檢索它對(duì)應(yīng)的應(yīng)用和提供的服務(wù),是比個(gè)人溝通要高效很多的。
第三它打通了服務(wù)治理。在編碼生成的過程中,你需要去配置這些治理的參數(shù),然后這些治理的參數(shù)通過上報(bào)的方式,把它統(tǒng)一地注冊(cè)到我們的服務(wù)治理平臺(tái),然后跟 Mesh 的模式去進(jìn)行打通。這樣的話有一個(gè)非常有效的方式,在你去生成這些調(diào)用代碼的時(shí)候,你就可以參考一些對(duì)應(yīng)的指標(biāo)、參數(shù),比如對(duì)方提供的接口的監(jiān)控是什么樣子的,以及其他人設(shè)置的指標(biāo)是什么樣的,做一定的智能化推薦,這樣能夠保證我們的這些指標(biāo)相對(duì)來說是配置的比較合理的。
第四個(gè)就是代碼規(guī)范的最佳實(shí)踐是能夠比較好去落地的。我們都知道,很多時(shí)候這些代碼規(guī)范是需要靠文檔,比如我們出一個(gè)什么樣的規(guī)范,什么樣的標(biāo)準(zhǔn)去保障,或者是類似利用這些代碼檢查工具,比如 Sonar 等等之類的方式去保證我們的代碼規(guī)范的落地。但是其實(shí)通過這種生成代碼的方式,我們直接就可以把最佳實(shí)踐嵌入到生成的過程里面,來保證它生成的代碼一定是符合最佳實(shí)踐的。
除了上面這四個(gè)方面之外,我們其實(shí)還在插件上做了大量的工作,比如說像 CI/CD 的左移,這個(gè)左移包含了我們可以在本地去跟遠(yuǎn)程的環(huán)境打通,以及它還提供了對(duì)應(yīng)的 CI/CD 流水線的功能,還有代碼覆蓋率的功能等等。通過這樣的一個(gè)開發(fā)插件,我們可以把日常的一些重復(fù)性的、低效性的工作就可以被完成掉,是一個(gè)比較好的提效方式,推薦大家去使用。
四、服務(wù)治理實(shí)踐
然后在服務(wù)治理這里面,我們其實(shí)也做了一些自己的思考。首先我們來看一下,常規(guī)的這些服務(wù)治理的四板斧是什么樣子。
1. 常規(guī)四板斧
不可避免地,第一,我們一定要設(shè)置超時(shí);第二,要在一些場(chǎng)景里面去考慮重試的邏輯;第三,考慮熔斷的邏輯,不要被下游拖死;第四,一定要有限流的邏輯,不要被上游打死。
2. 最終目標(biāo)
這些都是非常普遍,也是非常有效的一些措施,但是有效建立在于你的配置,或者是你的這個(gè)動(dòng)作是有效的場(chǎng)景,但實(shí)際上我們很大程度上其實(shí)是在濫用這四種技術(shù)。我認(rèn)為服務(wù)治理的一個(gè)最終的目標(biāo)就是穩(wěn)定可用、可觀測(cè)、防腐化,這是什么意思呢?
穩(wěn)定可用指的就是我們通過各類的防控手段去達(dá)到在可用的容量場(chǎng)景下,提供有效的服務(wù),這樣才能叫穩(wěn)定可用。第二個(gè)可觀測(cè),就是我們從多個(gè)維度,比如說像關(guān)系、性能、異常、資源等維度對(duì)它進(jìn)行度量并且分析。第三個(gè)防腐化,我們的代碼和架構(gòu)其實(shí)不可避免地都是在腐化的一個(gè)過程之中,我們不停地往里面去添加?xùn)|西的過程中,其實(shí)也會(huì)缺乏一定的治理。我們服務(wù)治理的目標(biāo),其中一點(diǎn)就是要做到如何去對(duì)它進(jìn)行防腐,這個(gè)里面有一些考慮的維度,比如服務(wù)的層級(jí),你的服務(wù)并不是越微越好,也不是層級(jí)越多越好,所以服務(wù)的層級(jí)一定要有所控制。
3. 保護(hù)機(jī)制
第二就是鏈路的分析,鏈路里面上下游的超時(shí)、串行、并行的調(diào)用等等之類的這些東西在編碼的過程中可能會(huì)被忽略掉的,這些我們其實(shí)可以通過偏后置一點(diǎn)的方式對(duì)它進(jìn)行一個(gè)分析和預(yù)警,這里面提一下我們?cè)诒Wo(hù)機(jī)制上做的一些工作,我們都知道在 RPC 的框架里面,其實(shí)特別是在直連的模式下,調(diào)用端 Consumer 端和 Provider 端其實(shí)是直連通信的。
對(duì)于注冊(cè)中心來說,它只負(fù)責(zé)一個(gè)注冊(cè)和變更通知的作用,但是在有一些特定的場(chǎng)景里面并不是這樣子的。舉個(gè)例子來說,當(dāng)一個(gè)注冊(cè)中心因?yàn)樽陨淼脑蛱幱谝粋€(gè)半死不活的狀態(tài),它一會(huì)兒能服務(wù)、一會(huì)兒不能服務(wù)的時(shí)候,就會(huì)發(fā)生一個(gè)比較恐怖的事情,Provider 端因?yàn)樗?cè)中心去保持心跳判活的狀態(tài),所以需要和注冊(cè)中心保持長(zhǎng)期有效的連接。如果是失效的情況,作業(yè)中心就會(huì)判斷這個(gè) Provider 是不存活了。不存活的時(shí)候,注冊(cè)中心就會(huì)把這個(gè)消息通知給 Consumer 端,Consumer 端只要接收過一次下線通知,Consumer 就會(huì)從它的列表里面把這個(gè) Provider 從本地的緩存里面去移除掉。
如果注冊(cè)中心處于一個(gè)半死不活的狀態(tài),最后會(huì)處于一個(gè)什么狀態(tài)呢?Consumer 端慢慢地會(huì)把所有的 Provider 都移除掉,這樣就會(huì)導(dǎo)致我們的 Consumer 端到 Provider 端其實(shí)是不可通信的。對(duì)于這個(gè)問題,我們其實(shí)基于 Dubbo 做了一定的改造,做了一個(gè)保護(hù)機(jī)制。這個(gè)保護(hù)機(jī)制就是當(dāng) Provider,特別是注冊(cè)中心上的 Provider 數(shù)少于一定的閾值的時(shí)候,我們的保護(hù)機(jī)制就會(huì)自動(dòng)地啟用,它的生效是在 Consumer 端的,也就意味著 Consumer 端需要緩存這段時(shí)間內(nèi)所有歷史的 Provider 的列表。
大家可能在這里會(huì)有一點(diǎn)擔(dān)心,你緩存的 Provider 如果失效了怎么辦?它是真的失效了,比如說它被下線了,或者是它本身經(jīng)過遷移,像我們?cè)谌萜鲌?chǎng)景里面,經(jīng)過了一定的發(fā)布,其實(shí)它對(duì)應(yīng)的信息都變化了,這個(gè)時(shí)候你再去通信不就有問題嗎?其實(shí)我們?cè)诒Wo(hù)機(jī)制里面也考慮了這個(gè)問題,我們?cè)谕ㄐ胖斑€是會(huì)做一個(gè)直連的檢查,Consumer 到 Provider 的連接存活是否是真正存在,如果不存在,我們會(huì)把這一個(gè)連接給扔掉,保證通信的時(shí)候使用的是一個(gè)可用的連接。
當(dāng)這個(gè)信息機(jī)制啟用了之后,注冊(cè)中心恢復(fù)到一定的狀態(tài)的,這個(gè) Provider 又能重新注冊(cè)到注冊(cè)中心里面了,接著我們又會(huì)把保護(hù)機(jī)制自動(dòng)關(guān)閉掉,這樣的話 Consumer 就只會(huì)調(diào)用注冊(cè)中心上存活的這些 Provider,就可以避免掉因?yàn)樽?cè)中心半死不活,導(dǎo)致所有的這些分布式的應(yīng)用里面的 RPC 調(diào)用是不可用的。
這其實(shí)是一個(gè)比較有效的方式,因?yàn)槿绻霈F(xiàn)了這種場(chǎng)景,其實(shí)你內(nèi)網(wǎng)里面的大部分應(yīng)用通信其實(shí)是處于一個(gè)不可用的狀態(tài),甚至你想讓它恢復(fù)都是非常困難的事情。比如你想啟動(dòng)的時(shí)候,其實(shí) Consumer 發(fā)現(xiàn) Provider 都不存活了,這也會(huì)導(dǎo)致啟動(dòng)失敗等等各方面的問題。
4. 動(dòng)態(tài)限流
接著我來介紹一下限流里面我們做的一些工作,這里面我們做的模式我把它叫做動(dòng)態(tài)限流。普通的一個(gè)限流里面,通常來說是這樣的一個(gè)方式,我們有 A、B、C 的服務(wù)都對(duì) X 這個(gè)服務(wù)進(jìn)行了調(diào)用,它的來源可能是不一樣的,X 為了保護(hù)自身的狀態(tài)是可用的,它不可避免就要對(duì)上游 A、B、C 的這些訪問分配固定的一些配額,誰超過了配額就不可用了。
比如說像 A 分配了 100、B 也分配 100、C 分配給了 50。當(dāng) A 超過了 100 的時(shí)候,其實(shí)它的一些請(qǐng)求是會(huì)被拒絕掉的,這個(gè)是基于容量的考慮,X 不可能具備無限的容量,這時(shí)它需要一定的保護(hù)措施。但是這地方就會(huì)有一個(gè)問題,假如 A、B、C 里面,比如說 B 服務(wù),它其實(shí)是從 App 過來的,它的價(jià)值不可避免來說的話,要更高一點(diǎn)。比如說第三個(gè)服務(wù) C,它是從 Web 里面來,它的價(jià)值相對(duì)來說比較低一點(diǎn)。這個(gè)價(jià)值是基于你的業(yè)務(wù)形態(tài)來的,比如說你的 App 的成單、轉(zhuǎn)化更高,那就意味著它的請(qǐng)求更珍貴。
這個(gè)里面就會(huì)出現(xiàn)一個(gè)問題,服務(wù) B 和服務(wù) C 自己都得到了一定數(shù)量的配額,但是假如 App 的流量上漲了,Web 的流量沒有上漲,這時(shí)就會(huì)面臨一個(gè)問題,服務(wù) C 的配額沒用完,但是服務(wù) B 的配額又不夠用,這個(gè)場(chǎng)景下怎么解決呢?就需要靠人工來不停地去調(diào)整它,而且這個(gè)調(diào)整需要相當(dāng)實(shí)時(shí)才可以,我們有沒有辦法能夠相對(duì)統(tǒng)一地解決這個(gè)問題呢,其實(shí)我們做了一個(gè)探索,這個(gè)探索從實(shí)踐結(jié)果來看的話是比較有效的。
我們對(duì)這些服務(wù)進(jìn)行配額分配的時(shí)候,其實(shí)不是一個(gè)固定的配額,而是一個(gè)動(dòng)態(tài)的分配。動(dòng)態(tài)的分配意思就是,我只有一個(gè)總的容量,并不給每一個(gè)服務(wù)進(jìn)行分配,總的容量我分配給所有人。但是我要對(duì)所有的調(diào)用方進(jìn)行一個(gè)排序,也就是說誰的價(jià)值高誰就排在前面,這樣的話就能得到一個(gè)比較有效的結(jié)果。你的限流模型是基于你的業(yè)務(wù)邏輯來的,也是基于你的業(yè)務(wù)價(jià)值來的,當(dāng)你發(fā)生限流的時(shí)候,優(yōu)先丟掉的一定是最沒有價(jià)值的那部分的業(yè)務(wù)請(qǐng)求。
當(dāng)然這里面也會(huì)有一個(gè)前提,你的請(qǐng)求來源是需要有差異化的。還有第二個(gè)點(diǎn),你的這些 trace 連通性一定要高,也就意味著,你的這些標(biāo)志要能夠一路暢通地?cái)y帶下去,如果只是基于某一層去做限流邏輯,其實(shí)是沒有意義的。
5. 防腐化
接著就是防腐化,這里面其實(shí)我們需要對(duì)架構(gòu)、應(yīng)用的分布、應(yīng)用的關(guān)系去做大量的分析,得出改進(jìn)的措施,我們?cè)谶@上面改進(jìn)的措施其實(shí)有很多。比如我們會(huì)分析哪些應(yīng)用是頻繁修改的,這些頻繁修改的意思是不是所有的需求,這些應(yīng)用都相關(guān)地需要去做修改,那就意味著說它的業(yè)務(wù)域是一樣的。如果這些業(yè)務(wù)域一樣的情況下,你把它的微服務(wù)劃分得很細(xì),實(shí)際上它是一一綁定的話,其實(shí)并不符合微服務(wù)化的原則。
第二個(gè)是否存在重復(fù)的調(diào)用,這條鏈路里面,這些重復(fù)的調(diào)用是否能夠去緩存化,或者是避免它重復(fù)調(diào)用。
第三個(gè)大量的串行調(diào)用是不是能夠把它異步化,比如常見的,從數(shù)據(jù)庫里面拿出一批記錄,這一批記錄通過循環(huán)的方式,挨個(gè)去對(duì)它發(fā)起遠(yuǎn)程調(diào)用,這些過程里面其實(shí)比較有效的方式就是通過異步化、并行化的方式去把速度給提上來。
第四個(gè)異步的整個(gè)鏈路的這些超時(shí)配置里面,其實(shí)會(huì)有一定的相關(guān)的關(guān)系。比如上游的超時(shí)是不應(yīng)該比下游短的,如果下游的超時(shí)比上游的還長(zhǎng),那意味著說下游還在計(jì)算,上游可能已經(jīng)超時(shí)了,這個(gè)計(jì)算的結(jié)果其實(shí)有可能返回不了上游,這些就是無用的配置。除了這之外其實(shí)整個(gè)鏈路里面大量的超時(shí)可能是不合理的,比如剛才提到的大量重復(fù)的調(diào)用,這些重復(fù)的調(diào)用或者循環(huán)的調(diào)用,再乘以同樣的超時(shí)時(shí)間,可能就會(huì)比整個(gè)終端的操作時(shí)間要長(zhǎng)很多,這些都需要去做一定的分析和考慮,才能達(dá)到它防腐化的目的。
五、ServiceMesh 嘗試
最后一個(gè)介紹一下我們?cè)?ServiceMesh 上的嘗試。
1. 背景
先簡(jiǎn)單介紹一下背景,我們公司內(nèi)部其實(shí)還是存在多語言、多協(xié)議的這樣一個(gè)場(chǎng)景。
第二個(gè)它在多語言、多協(xié)議的場(chǎng)景里面不可避免地就會(huì)出現(xiàn)治理平臺(tái)比較分散,比如像 Dubbo 的話,我們其實(shí)會(huì)有一個(gè) RPC 的服務(wù)治理平臺(tái);HTTP 的話我們其實(shí)有類似于網(wǎng)關(guān) Nginx 或者是 OpenResty 去對(duì)它進(jìn)行治理;其他的也會(huì)相應(yīng)的治理,甚至可能是在配置中心去對(duì)它進(jìn)行治理等等。
第三個(gè)組件的新功能迭代是相對(duì)比較慢的,因?yàn)檫@些組件都是嵌入在應(yīng)用代碼里面,因此它的迭代就需要跟隨著業(yè)務(wù)代碼去迭代,才能夠去比較好地迭代,而且這些迭代里面其實(shí)需要付出一定的人工成本,其實(shí)業(yè)務(wù)的開發(fā)是不太愿意去主動(dòng)地做這種組件的迭代的,在 ServiceMesh 的選型里面,我們也考量了一下當(dāng)時(shí)業(yè)界里的選擇。
2. 技術(shù)選型
其實(shí)從數(shù)據(jù)面上來看,envoy 還是占大頭的,但是我們最終其實(shí)沒有選擇 envoy,主要是因?yàn)槲覀冊(cè)?C++ 技術(shù)棧里面儲(chǔ)備的人才是不夠多的。第二個(gè)在控制面上,大家基本上都是基于 Istio 模式去做的,當(dāng)然也大部分都做了二次的開源,我們最終也是選擇這樣的一個(gè)模式。
3. 整體架構(gòu)
我們最終的選擇是,數(shù)據(jù)面上我們選擇了 MOSN,而不是 envoy,MOSN 是基于 Go 開發(fā)的一個(gè)阿里巴巴官方出品的組件,這個(gè)組件其實(shí)是一個(gè)偏網(wǎng)關(guān)代理型的一個(gè)組件,但是在上面去實(shí)現(xiàn) Mesh 的邏輯,其實(shí)是比較方便的,特別是針對(duì)基于 Dubbo 這個(gè)協(xié)議的 Mesh,MOSN 支持得是比較好的;在控制面上,我們也是基于 Istio 去做了二次開發(fā),也有一定的自研組件,比如說 mcpServer、配置中心、注冊(cè)中心這些都是我們自研的。在運(yùn)維面的話,我們也是自研了一套運(yùn)維相關(guān)的組件,比如 Sidecar 的部署、灰度的升級(jí)等等,還有一些規(guī)則治理、監(jiān)控報(bào)警等。
4. 注冊(cè)模型
ServiceMesh 里面我主要介紹一下幾個(gè)關(guān)鍵點(diǎn):第一個(gè)就是注冊(cè)模型,因?yàn)樗且粋€(gè)多協(xié)議、多語言的方式,其實(shí)比如 Dubbo 或者 HTTP,它在服務(wù)層面其實(shí)是不統(tǒng)一的,在注冊(cè)中心我們想要以一個(gè)統(tǒng)一的注冊(cè)中心去服務(wù)發(fā)現(xiàn)的時(shí)候,不可避免地就需要把它的維度統(tǒng)一掉,我們是怎么做到的呢?我們其實(shí)是參考了業(yè)界現(xiàn)在比較火的,或者基本上應(yīng)該是事實(shí)上的標(biāo)準(zhǔn),通過服務(wù) - 實(shí)例這樣的維度去抹除掉了類似 RPC 這種 Dubbo,這種接口的維度,與原來的注冊(cè)中心去進(jìn)行雙寫,來保證 Mesh 化的和非 Mesh 化的都能支持。
5. 配置模型
然后第二個(gè)就是配置的模型,這里面就是服務(wù)治理平臺(tái),我們其實(shí)自定義了一些存儲(chǔ)的格式,然后通過 MCP 的方式,Server 的組件去轉(zhuǎn)換 Istio 需要的數(shù)據(jù)格式。Istio 拿到了之后,通過標(biāo)準(zhǔn)的 XDS 的數(shù)據(jù)格式下發(fā)到 MOSN 里面,這一段我們基本上就是依賴原有的一個(gè)功能,主要是在左側(cè)這部分,我們自定義的這部分組件的數(shù)據(jù)格式是比較關(guān)鍵的。
6. 路由模型
第三個(gè)說一下路由的模型,路由模型里面,大家其實(shí)見過非常多,但是我對(duì)這些治理的功能或者路由的功能,其實(shí)偏保守一點(diǎn)的觀點(diǎn)。因?yàn)樵谖铱磥碓届`活越可能會(huì)用錯(cuò),這里面就需要我們?nèi)コ橄笠欢ǖ臉I(yè)務(wù)模式,把業(yè)務(wù)模式落地到或者固化到組件里面來。通過這個(gè)方式,我們其實(shí)發(fā)現(xiàn)只需要以應(yīng)用和環(huán)境集群為主體,并且在這個(gè)場(chǎng)景上支持 trace 匹配的控制,就可以保證滿足我們絕大部分的業(yè)務(wù)場(chǎng)景。
因?yàn)槲覀兙€上經(jīng)常會(huì)出現(xiàn)應(yīng)用不同的環(huán)境集群,其實(shí)是為了不同的訴求去用的,比如像搜索集群和交易集群,它們需要進(jìn)行物理隔離,然后比如上線的時(shí)候,可能需要做一定的灰度驗(yàn)證等等。這樣的話我們就可以基于 trace 的參數(shù)匹配去控制它,只要以這樣兩種方式作為路由模型的支持,是滿足絕大部分的業(yè)務(wù)訴求的。
7. 控制面和運(yùn)維面
在控制面與運(yùn)維面上,我們做了什么樣的方式呢?其實(shí)我們當(dāng)時(shí)也并不想要在這上面做自研,而我們參考了業(yè)界很多的解決方案,其實(shí)發(fā)現(xiàn)在配置中心和 MCP 的 Server 里面,是缺少開源方案的,特別是配置中心,我們發(fā)現(xiàn)基本上很少有可用的配置,基本上就是一個(gè)查看可觀測(cè)的方式而已,但其實(shí)你想要對(duì)它進(jìn)行一些服務(wù)的治理是不夠用的。
第二個(gè) Sidecar 運(yùn)維,這里面無損的升級(jí)和切換非常關(guān)鍵,會(huì)涉及到不同組件之間的依賴關(guān)系和它的檢測(cè),比如 Consumer 對(duì) MOSN Sidecar 的檢測(cè),和 MOSN 逆過來對(duì) Consumer 的檢測(cè),這些邏輯都是不一樣的,而且細(xì)節(jié)會(huì)比較多,有興趣的話大家可以線下溝通一下。
第三個(gè)就是可觀測(cè)性,參考了非 Mesh 化需要的一些指標(biāo),我們可以比較好地去把 Mesh 化的過程里面大量的可觀性指標(biāo)都內(nèi)置地埋點(diǎn)進(jìn)去。但是在 trace 鏈路里面,最好把 Mesh 的 Sidecar 的 span 給精簡(jiǎn)掉,不然你會(huì)發(fā)現(xiàn)所有的節(jié)點(diǎn)都比原來多了兩跳,這樣無疑會(huì)把 trace 因?yàn)橹虚g件的邏輯,把它復(fù)雜化掉了。
第四個(gè)就是健康檢查,這里面剛才提到的 Consumer 對(duì) Sidecar 的可用性的檢查,其實(shí)是一個(gè)非常關(guān)鍵的重點(diǎn),因?yàn)槿Q于它需要怎么降級(jí)以及它能不能降級(jí)。
8. 性能優(yōu)化
最后一個(gè)就是性能的優(yōu)化,這里面主要有兩點(diǎn),在業(yè)界大部分的方案里面其實(shí)都會(huì)面臨一個(gè)問題,因?yàn)檫@些調(diào)用關(guān)系是動(dòng)態(tài)化的,就意味著運(yùn)行時(shí)才能知道我需要調(diào)用哪一些服務(wù),它對(duì)應(yīng)的規(guī)則是什么,也就是說我需要把所有的服務(wù)信息都下發(fā)到 Sidecar 里面,這不可避免就會(huì)占用大量的內(nèi)存,它的匹配效率都是非常低的,我們?cè)谶@上面怎么去做優(yōu)化呢?
其實(shí)配合前面 Spring Cloud Qunar 能夠做到比較友好的方式,當(dāng)它做了 Spring Cloud Qunar 這樣的 Qunar Mesh 注解之后,我們其實(shí)可以把這部分在編譯期就采集上來,或者在啟動(dòng)的時(shí)候去把這些信息都給它上報(bào)上來,這樣我們就只需要訂閱我們需要的一些部分?jǐn)?shù)據(jù)就好了,能夠做到大量的數(shù)據(jù)減少。
第二個(gè)就是在服務(wù)通信里面,因?yàn)槎嗔?Sidecar 的兩跳,那就意味著說 Sidecar 的通信是帶來一定時(shí)間、效率和性能損耗的,這里面的關(guān)鍵點(diǎn)就在于應(yīng)用程序和 Sidecar 的通信是否能夠存在優(yōu)化空間。我們經(jīng)過實(shí)驗(yàn)發(fā)現(xiàn),使用 UDS 的通信來替代原有的這種要經(jīng)過網(wǎng)卡的通信其實(shí)要高效不少的,把它在這兩跳上帶來的損耗降到足夠低。
六、總結(jié)
總結(jié)來看的話,整個(gè)微服務(wù)的過程里面,我們最佳的實(shí)踐其實(shí)存在好幾個(gè)方面。
第一個(gè)是在發(fā)現(xiàn)模式、通信模式上的,我們需要去因地制宜做一定的最佳實(shí)踐;在架構(gòu)模式里面,比如說像 BASE 模式和 CQRS 模式,我們都可以在合適的場(chǎng)景里面放心大膽,或是盡可能去啟用它們的。
開發(fā)效率先行,微服務(wù)的初衷其實(shí)是提效,那問題復(fù)雜化了以后,就需要有這些有力的配套,比如開發(fā)插件等來解決我們開發(fā)的問題,否則微服務(wù)可能只會(huì)帶來一地的雞毛。
第三個(gè)就是有效的服務(wù)治理,簡(jiǎn)單的管控手段意義是不大的,它的手段雖然有效,但真實(shí)業(yè)務(wù)的意義是不大的,類似于動(dòng)態(tài)限流這樣的模式才能真正解決業(yè)務(wù)問題。
第四,ServiceMesh 不可避免地,或者說現(xiàn)在基本上已經(jīng)成為事實(shí)上的下一代微服務(wù)通信的架構(gòu)模式,這個(gè)里面模型的設(shè)計(jì)和性能優(yōu)化就非常關(guān)鍵。
最后對(duì)于微服務(wù)里面的一些要點(diǎn)再進(jìn)行一下簡(jiǎn)單的總結(jié)。
業(yè)務(wù)的拆分就是借鑒業(yè)界成熟的模型,本地化為最適合公司現(xiàn)狀的業(yè)務(wù)結(jié)構(gòu)。比如剛才提到的去哪兒網(wǎng),它其實(shí)也是一個(gè)線上的電商系統(tǒng)結(jié)構(gòu),但是它又有旅游、民航或者酒店領(lǐng)域的特殊性,就不可避免地要本地化。
還有就是架構(gòu)模式里面,不同場(chǎng)景下的架構(gòu)模式的支持是不一樣的,交易系統(tǒng)的事件驅(qū)動(dòng),異構(gòu)數(shù)據(jù)的 CQRS 都是比較有效的方式。然后開發(fā)模式、開發(fā)支撐里面需要對(duì)微服務(wù)進(jìn)行完善的工具支持。
在服務(wù)度量里面,我們關(guān)系、性能、異常、資源,還有剛才提到的防腐都需要比較有效。第五個(gè)就是治理的管控,限流、熔斷這種方式需要實(shí)時(shí)生效,最好是把它統(tǒng)一化而且進(jìn)行業(yè)務(wù)有效化。最后一個(gè)就是演進(jìn)式,架構(gòu)的演進(jìn)需要平滑有序,避免大量的應(yīng)用改造。
最后送給你一句話:架構(gòu)演進(jìn),以提升效率為目標(biāo)。
作者介紹
朱仕智 去哪兒旅行 基礎(chǔ)架構(gòu)部高級(jí)技術(shù)總監(jiān)
去哪兒網(wǎng)高級(jí)總監(jiān)。負(fù)責(zé)過公共業(yè)務(wù)、國際機(jī)票、基礎(chǔ)技術(shù)等團(tuán)隊(duì),擅長(zhǎng)復(fù)雜實(shí)時(shí)業(yè)務(wù)的高并發(fā)、高可用、高性能的系統(tǒng)設(shè)計(jì)和落地。目前負(fù)責(zé)基礎(chǔ)架構(gòu)團(tuán)隊(duì),包含后端架構(gòu)、大前端架構(gòu)、質(zhì)量保障、基礎(chǔ)云平臺(tái)等領(lǐng)域。近期主要投入在公司整體技術(shù)演進(jìn)和數(shù)字化技術(shù)運(yùn)營(yíng)方向。