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