微服務(wù)-架構(gòu)模式和服務(wù)治理的實踐
1. 服務(wù)發(fā)現(xiàn)模式
第一個就是服務(wù)發(fā)現(xiàn)的模式,服務(wù)發(fā)現(xiàn)里面其實有兩種模式(邊車模式,Sidecar暫時范圍不是很廣),這兩種模式對應(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ù)端變更的時候,客戶端其實是不需要去感知,也不需要對此進(jìn)行任何變更,這樣對外網(wǎng)來說,其實用戶側(cè)的設(shè)備是不需要去關(guān)注信息的,這樣通知起來就比較簡單。但是它也會面臨一個問題,它會多一條的通信,從性能或者效率上來說,肯定是不如直連模式的。
2. 服務(wù)通信模式
服務(wù)通信模式里面主要有兩種,大家其實日常里面比較經(jīng)常會碰到就是同步的編程模式,這種模式比較簡單易懂,非常符合人類的思考習(xí)慣,它比較適用于時間比較敏感的、吞吐量也比較小的這種場景。但是這種通信的方式在吞吐量比較大、QPS 比較高的場景里面就會有一系列的問題,比如說可能會把你的資源耗盡,但其實這些資源都處于等待中。比如我們在 Java 里面可能會有線程池的資源,使用起來其實是比較低效的。然后在異步的這種場景里面,它其實比較適用于高吞吐、削峰填谷的作用。
其實這里面會有幾種,從我們的實踐上來看的話,比如說搜索系統(tǒng)它其實是一個非常高并發(fā)的場景,其實對于這種高吞吐的場景下是必須要用異步的,不然的話其實資源的損耗是非常高的,我們在某些系統(tǒng)上做過改造,由原來的同步改為異步的話,基本上可以節(jié)省掉 80% 左右的機(jī)器的資源。除此之外,交易系統(tǒng)的事件驅(qū)動也是比較適合異步的一個場景,因為交易系統(tǒng)的事件其實是非常關(guān)鍵的,但是它又不能每個人都去通知,因為很多人都需要關(guān)注這個事件,這個時候利用 MQ 等方式去做這種事件的驅(qū)動是比較合適的。
圖片
3. 設(shè)計模式
從設(shè)計模式上來說的話,我們其實可以知道在互聯(lián)網(wǎng)的架構(gòu)里面,特別是在高并發(fā)的模式里面,我們有很多折中,這些折中里面其實會有不同的模式和它的沉淀。比如說像 BASE 這樣的模式,它其實不追求強(qiáng)一致性,它是有這種基本的可用和軟狀態(tài)這樣的優(yōu)點,進(jìn)而去避免因為強(qiáng)一致導(dǎo)致的其他的不可用性。
圖片
第二個就是 CQRS,這個模式其實非常有用,至少我發(fā)現(xiàn)很多場景是能夠用上它的,換句話說其實只要是數(shù)據(jù)異構(gòu)的這種場景,都是比較適合去使用它的,當(dāng)然這取決于你的查詢模式。大家都知道查詢模式其實有很多種的,比如說像 KV 的查詢模式、復(fù)雜條件的 Query,除此之外,還有 Scan 這種掃描形式,不同的查詢形式會對應(yīng)著不同的存儲結(jié)構(gòu)是比較合適的。但是我們在對這些數(shù)據(jù)進(jìn)行操作的時候,其實它的數(shù)據(jù)載體是唯一的,那這個數(shù)據(jù)載體怎么樣才能支持多種的查詢模式呢?其實這里面就需要對這些數(shù)據(jù)進(jìn)行異構(gòu),比如說像我們的訂單、配置等等這些方式都需要去進(jìn)行一定的異構(gòu)。
服務(wù)治理實踐
常見的服務(wù)治理的四板斧是:
1. 常規(guī)四板斧
圖片
不可避免地,第一,我們一定要設(shè)置超時;第二,要在一些場景里面去考慮重試的邏輯;第三,考慮熔斷的邏輯,不要被下游拖死;第四,一定要有限流的邏輯,不要被上游打死。
2. 最終目標(biāo)
圖片
穩(wěn)定可用指的就是我們通過各類的防控手段去達(dá)到在可用的容量場景下,提供有效的服務(wù),這樣才能叫穩(wěn)定可用。第二個可觀測,就是我們從多個維度,比如說像關(guān)系、性能、異常、資源等維度對它進(jìn)行度量并且分析。第三個防腐化,我們的代碼和架構(gòu)其實不可避免地都是在腐化的一個過程之中,我們不停地往里面去添加?xùn)|西的過程中,其實也會缺乏一定的治理。我們服務(wù)治理的目標(biāo),其中一點就是要做到如何去對它進(jìn)行防腐,這個里面有一些考慮的維度,比如服務(wù)的層級,你的服務(wù)并不是越微越好,也不是層級越多越好,所以服務(wù)的層級一定要有所控制。
3. 保護(hù)機(jī)制
第二就是鏈路的分析,鏈路里面上下游的超時、串行、并行的調(diào)用等等之類的這些東西在編碼的過程中可能會被忽略掉的,這些我們其實可以通過偏后置一點的方式對它進(jìn)行一個分析和預(yù)警,這里面提一下我們在保護(hù)機(jī)制上做的一些工作,我們都知道在 RPC 的框架里面,其實特別是在直連的模式下,調(diào)用端 Consumer 端和 Provider 端其實是直連通信的。
對于注冊中心來說,它只負(fù)責(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 做了一定的改造,做了一個保護(hù)機(jī)制。這個保護(hù)機(jī)制就是當(dāng) Provider,特別是注冊中心上的 Provider 數(shù)少于一定的閾值的時候,我們的保護(hù)機(jī)制就會自動地啟用,它的生效是在 Consumer 端的,也就意味著 Consumer 端需要緩存這段時間內(nèi)所有歷史的 Provider 的列表。
大家可能在這里會有一點擔(dān)心,你緩存的 Provider 如果失效了怎么辦?它是真的失效了,比如說它被下線了,或者是它本身經(jīng)過遷移,像我們在容器場景里面,經(jīng)過了一定的發(fā)布,其實它對應(yīng)的信息都變化了,這個時候你再去通信不就有問題嗎?其實我們在保護(hù)機(jī)制里面也考慮了這個問題,我們在通信之前還是會做一個直連的檢查,Consumer 到 Provider 的連接存活是否是真正存在,如果不存在,我們會把這一個連接給扔掉,保證通信的時候使用的是一個可用的連接。
當(dāng)這個信息機(jī)制啟用了之后,注冊中心恢復(fù)到一定的狀態(tài)的,這個 Provider 又能重新注冊到注冊中心里面了,接著我們又會把保護(hù)機(jī)制自動關(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ù)進(jìn)行了調(diào)用,它的來源可能是不一樣的,X 為了保護(hù)自身的狀態(tài)是可用的,它不可避免就要對上游 A、B、C 的這些訪問分配固定的一些配額,誰超過了配額就不可用了。
圖片
比如說像 A 分配了 100、B 也分配 100、C 分配給了 50。當(dāng) A 超過了 100 的時候,其實它的一些請求是會被拒絕掉的,這個是基于容量的考慮,X 不可能具備無限的容量,這時它需要一定的保護(hù)措施。但是這地方就會有一個問題,假如 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ù)進(jìn)行配額分配的時候,其實不是一個固定的配額,而是一個動態(tài)的分配。動態(tài)的分配意思就是,我只有一個總的容量,并不給每一個服務(wù)進(jìn)行分配,總的容量我分配給所有人。但是我要對所有的調(diào)用方進(jìn)行一個排序,也就是說誰的價值高誰就排在前面,這樣的話就能得到一個比較有效的結(jié)果。你的限流模型是基于你的業(yè)務(wù)邏輯來的,也是基于你的業(yè)務(wù)價值來的,當(dāng)你發(fā)生限流的時候,優(yōu)先丟掉的一定是最沒有價值的那部分的業(yè)務(wù)請求。
當(dāng)然這里面也會有一個前提,你的請求來源是需要有差異化的。還有第二個點,你的這些 trace 連通性一定要高,也就意味著,你的這些標(biāo)志要能夠一路暢通地攜帶下去,如果只是基于某一層去做限流邏輯,其實是沒有意義的。
5. 防腐化
接著就是防腐化,這里面其實我們需要對架構(gòu)、應(yīng)用的分布、應(yīng)用的關(guān)系去做大量的分析,得出改進(jìn)的措施,我們在這上面改進(jìn)的措施其實有很多。比如我們會分析哪些應(yīng)用是頻繁修改的,這些頻繁修改的意思是不是所有的需求,這些應(yīng)用都相關(guān)地需要去做修改,那就意味著說它的業(yè)務(wù)域是一樣的。如果這些業(yè)務(wù)域一樣的情況下,你把它的微服務(wù)劃分得很細(xì),實際上它是一一綁定的話,其實并不符合微服務(wù)化的原則。
第二個是否存在重復(fù)的調(diào)用,這條鏈路里面,這些重復(fù)的調(diào)用是否能夠去緩存化,或者是避免它重復(fù)調(diào)用。
第三個大量的串行調(diào)用是不是能夠把它異步化,比如常見的,從數(shù)據(jù)庫里面拿出一批記錄,這一批記錄通過循環(huán)的方式,挨個去對它發(fā)起遠(yuǎn)程調(diào)用,這些過程里面其實比較有效的方式就是通過異步化、并行化的方式去把速度給提上來。
第四個異步的整個鏈路的這些超時配置里面,其實會有一定的相關(guān)的關(guān)系。比如上游的超時是不應(yīng)該比下游短的,如果下游的超時比上游的還長,那意味著說下游還在計算,上游可能已經(jīng)超時了,這個計算的結(jié)果其實有可能返回不了上游,這些就是無用的配置。除了這之外其實整個鏈路里面大量的超時可能是不合理的,比如剛才提到的大量重復(fù)的調(diào)用,這些重復(fù)的調(diào)用或者循環(huán)的調(diào)用,再乘以同樣的超時時間,可能就會比整個終端的操作時間要長很多,這些都需要去做一定的分析和考慮,才能達(dá)到它防腐化的目的。