淺談微服務中的熔斷- 限流- 降級

簡介

在開發高併發系統時有三把利器用來保護系統:緩存、降級和限流。
緩存的目的是提升系統訪問速度和增大系統能處理的容量,可謂是抗高併發流量的銀彈;
降級是當服務出問題或者影響到核心流程的性能則需要暫時屏蔽掉,待高峯或者問題解決後再打開;
而有些場景並不能用緩存和降級來解決,比如稀缺資源(秒殺、搶購)、寫服務(如評論、下單)、頻繁的複雜查詢(評論的最後幾頁),因此需有一種手段來限制這些場景的併發 / 請求量,即限流。

概念介紹

雪崩效應

一個應用可能會有多個微服務組成,微服務之間的數據交互通過遠程過程調用完成。
這就帶來一個問題,
假設微服務 A 調用微服務 B 和微服務 C,微服務 B 和微服務 C 又調用其它的微服務,這就是所謂的 “扇出”。
如果扇出的鏈路上某個微服務的調用響應時間過長或者不可用,對微服務 A 的調用就會佔用越來越多的系統資源,進而引起系統崩潰,所謂的 “雪崩效應”。

服務熔斷

熔斷機制是應對雪崩效應的一種微服務鏈路保護機制。
當扇出鏈路的某個微服務不可用或者響應時間太長時,會進行服務的降級,進而熔斷該節點微服務的調用,快速返回錯誤的響應信息。當檢測到該節點微服務調用響應正常後,恢復調用鏈路。

服務降級

降級是指自己的待遇下降了,從 RPC 調用環節來講,就是去訪問一個本地的僞裝者而不是真實的服務。
當雙 11 活動時,把無關交易的服務統統降級,如查看螞蟻深林,查看歷史訂單,商品歷史評論,只顯示最後 100 條

服務熔斷和服務降級的區別

服務熔斷一般是某個服務(下游服務)故障引起,而服務降級一般是從整體負荷考慮;
熔斷其實是一個框架級的處理,每個微服務都需要(無層級之分),而降級一般需要對業務有層級之分(比如降級一般是從最外圍服務開始)
實現方式不太一樣;服務降級具有代碼侵入性 (由控制器完成 / 或自動降級),熔斷一般稱爲自我熔斷。

服務限流

限流的目的是通過對併發訪問 / 請求進行限速或者一個時間窗口內的的請求進行限速來保護系統,
一旦達到限制速率則可以拒絕服務(定向到錯誤頁或告知資源沒有了)、排隊或等待(比如秒殺、評論、下單)、降級(返回兜底數據或默認數據,如商品詳情頁庫存默認有貨)。

一般開發高併發系統常見的限流有:
限制總併發數(比如數據庫連接池、線程池)、
限制瞬時併發數(如 nginx 的 limit_conn 模塊,用來限制瞬時併發連接數)、
限制時間窗口內的平均速率(如 Guava 的 RateLimiter、nginx 的 limit_req 模塊,限制每秒的平均速率);
其他還有如限制遠程接口調用速率、限制 MQ 的消費速率。
另外還可以根據網絡連接數、網絡流量、CPU 或內存負載等來限流。

限流算法
常見的限流算法有:令牌桶、漏桶。計數器也可以進行粗暴限流實現。

應用級限流
對於一個應用系統來說一定會有極限併發 / 請求數,即總有一個 TPS/QPS 閥值,如果超了閥值則系統就會不響應用戶請求或響應的非常慢,因此我們最好進行過載保護,防止大量請求湧入擊垮系統。
MySQL(如 max_connections)、Redis(如 tcp-backlog)都會有類似的限制連接數的配置。

池化技術
如果有的資源是稀缺資源(如數據庫連接、線程),而且可能有多個系統都會去使用它,那麼需要限制應用;
可以使用池化技術來限制總資源數:連接池、線程池。
比如分配給每個應用的數據庫連接是 100,那麼本應用最多可以使用 100 個資源,超出了可以等待或者拋異常。

分佈式限流
分佈式限流最關鍵的是要將限流服務做成原子化,
而解決方案可以使使用 redis+lua 或者 nginx+lua 技術進行實現,通過這兩種技術可以實現的高併發和高性能。

首先我們來使用 redis+lua 實現時間窗內某個接口的請求數限流,
實現了該功能後可以改造爲限流總併發 / 請求數和限制總資源數。
Lua 本身就是一種編程語言,也可以使用它實現複雜的令牌桶或漏桶算法。

有人會糾結如果應用併發量非常大那麼 redis 或者 nginx 是不是能抗得住;
不過這個問題要從多方面考慮:
你的流量是不是真的有這麼大,是不是可以通過一致性哈希將分佈式限流進行分片,是不是可以當併發量太大降級爲應用級限流;
對策非常多,可以根據實際情況調節;像在京東使用 Redis+Lua 來限流搶購流量,一般流量是沒有問題的。

對於分佈式限流目前遇到的場景是業務上的限流,而不是流量入口的限流;流量入口限流應該在接入層完成,而接入層筆者一般使用 Nginx。

基於 Redis 功能的實現限流
基於令牌桶算法的實現

從微觀角度思考

超時(timeout)

如果這種情況頻度很高,那麼就會整體降低 consumer 端服務的性能。
這種響應時間慢的症狀,就會像一層一層波浪一樣,從底層系統一直湧到最上層,造成整個鏈路的超時。
所以,consumer 不可能無限制地等待 provider 接口的返回,會設置一個時間閾值,如果超過了這個時間閾值,就不繼續等待。
這個超時時間選取,一般看 provider 正常響應時間是多少,再追加一個 buffer 即可。

重試(retry)

有可能 provider 只是偶爾抖動,對於這種偶爾抖動,可以在超時後重試一下,重試如果正常返回了,那麼這次請求就被挽救了,能夠正常給前端返回數據,只不過比原來響應慢一點。
重試時的一些細化策略:
重試可以考慮切換一臺機器來進行調用,因爲原來機器可能由於臨時負載高而性能下降,重試會更加劇其性能問題,而換一臺機器,得到更快返回的概率也更大一些。
如果允許 consumer 重試,那麼 provider 就要能夠做到冪等。
即,同一個請求被 consumer 多次調用,對 provider 產生的影響 (這裏的影響一般是指某些寫入相關的操作) 是一致的。
而且這個冪等應該是服務級別的,而不是某臺機器層面的,重試調用任何一臺機器,都應該做到冪等。

熔斷(circuit break)

如果 provider 持續的響應時間超長呢?

如果 provider 是核心路徑的服務,down 掉基本就沒法提供服務了,那我們也沒話說。 如果是一個不那麼重要的服務,檢查出來頻繁超時,就把 consumer 調用 provider 的請求,直接短路掉,不實際調用,而是直接返回一個 mock 的值。
等 provider 服務恢復穩定之後,重新調用。
目前我們框架有通過註解使用的熔斷器,大家可以參考應用在項目中。

限流 (current limiting)

資源隔離
provider 可以對 consumer 來的流量進行限流,防止 provider 被拖垮。
同樣,consumer 也需要對調用 provider 的線程資源進行隔離。 這樣可以確保調用某個 provider 邏輯不會耗光整個 consumer 的線程池資源。

服務降級
降級服務既可以代碼自動判斷,也可以人工根據突發情況切換。

consumer 端
consumer 如果發現某個 provider 出現異常情況,比如,經常超時 (可能是熔斷引起的降級),數據錯誤,這是,consumer 可以採取一定的策略,降級 provider 的邏輯,基本的有直接返回固定的數據。

provider 端
當 provider 發現流量激增的時候,爲了保護自身的穩定性,也可能考慮降級服務。
比如,1,直接給 consumer 返回固定數據,2,需要實時寫入數據庫的,先緩存到隊列裏,異步寫入數據庫。

從宏觀角度重新思考

宏觀包括比 A -> B 更復雜的長鏈路。
長鏈路就是 A -> B -> C -> D 這樣的調用環境。
而且一個服務也會多機部署,A 服務實際會存在 A1,A2,A3 …
微觀合理的問題,宏觀未必合理。
下面的一些討論,主要想表達的觀點是:如果系統複雜了,系統的容錯配置要整體來看,整體把控,才能做到更有意義。

超時

如果 A 給 B 設置的超時時間,比 B 給 C 設置的超時時間短,那麼肯定不合理把,A 超時時間到了直接掛斷,B 對 C 支持太長超時時間沒意義。
R 表示服務 consumer 自身內部邏輯執行時間,TAB 表示 consumer A 開始調用 provider B 到返回的時間 。
那麼那麼 TAB > RB + TBC 纔對。

重試

重試跟超時面臨的問題差不多。
B 服務一般 100ms 返回,所以 A 就給 B 設置了 110ms 的超時,而 B 設置了對 C 的一次重試,最終 120ms 正確返回了,但是 A 的超時時間比較緊,所以 B 對 C 的重試被白白浪費了。

A 也可能對 B 進行重試,但是由於上一條我們講到的,可能 C 確實性能不好,每次 B 重試一下就 OK,但是 A 兩次重試其實都無法正確的拿到結果。
N 標示設置的重試次數
修正一下上面 section 的公式,TAB > RB+TBC * N。
雖然這個公式本身沒什麼問題,但是,如果站在長鏈路的視角來思考,我們需要整體規劃每個服務的超時時間和重試次數,而不是僅僅公式成立即可。
比如下面情況:
A -> B -> C。
RB = 100ms,TBC=10ms
B 是個核心服務,B 的計算成本特別大,那麼 A 就應該儘量給 B 長一點的超時時間,而儘量不要重試調用 B,而 B 如果發現 C 超時了,B 可以多調用幾次 C,因爲重試 C 成本小,而重試 B 成本則很高。 so …

熔斷

A -> B -> C,如果 C 出現問題了,那麼 B 熔斷了,則 A 就不用熔斷了。

3.4 限流
B 只允許 A 以 QPS<=5 的流量請求,而 C 卻只允許 B 以 QPS<=3 的 qps 請求,那麼 B 給 A 的設定就有點大,上游的設置依賴下游。
而且限流對 QPS 的配置,可能會隨着服務加減機器而變化,最好是能在集羣層面配置,自動根據集羣大小調整。

服務降級

服務降級這個問題,如果從整體來操作,
1,一定是先降級優先級低的接口,兩權相害取其輕
2,如果服務鏈路整體沒有性能特別差的點,比如就是外部流量突然激增,那麼就從外到內開始降級。
3 如果某個服務能檢測到自身負載上升,那麼可以從這個服務自身做降級。

漣漪

A -> B -> C,如果 C 服務出現抖動,而 B 沒有處理好這個抖動,造成 B 服務也出現了抖動,A 調用 B 的時候,也會出現服務抖動的情況。
這個暫時的不可用狀態就想波浪一樣從底層傳遞到了上層。
所以,從整個體系的角度來看,每個服務一定要儘量控制住自己下游服務的抖動,不要讓整個體系跟着某個服務抖動。

級聯失敗 (cascading failure)

系統中有某個服務出現故障,不可用,傳遞性地導致整個系統服務不可用的問題。
跟上面漣漪 (自造詞) 的區別也就是嚴重性的問題。
漣漪描述服務偶發的不穩定層層傳遞,而級聯失敗基本是導致系統不可用。 一般,前者可能會因爲短時間內恢復而未引起重視,而後者一般會被高度重視。

關鍵路徑

關鍵路徑就是,你的服務想正常工作,必須要完整依賴的下游服務鏈,比如數據庫一般就是關鍵路徑裏面的一個節點。
儘量減少關鍵路徑依賴的數量,是提高服務穩定性的一個措施。
數據庫一般在服務體系的最底層,如果你的服務可以會自己完整緩存使用的數據,解除數據庫依賴,那麼數據庫掛掉,你的服務就暫時是安全的。

最長路徑

想要優化你的服務的響應時間,需要看服務調用邏輯裏面的最長路徑,只有縮短最長時間路徑的用時,才能提高你的服務的性能。

本文由 Readfog 進行 AMP 轉碼,版權歸原作者所有。
來源https://blog.csdn.net/Edu_enth/article/details/103800426