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