淺談微服務中的熔斷- 限流- 降級
簡介
在開發高併發系統時有三把利器用來保護系統:緩存、降級和限流。
緩存的目的是提升系統訪問速度和增大系統能處理的容量,可謂是抗高併發流量的銀彈;
降級是當服務出問題或者影響到核心流程的性能則需要暫時屏蔽掉,待高峯或者問題解決後再打開;
而有些場景並不能用緩存和降級來解決,比如稀缺資源(秒殺、搶購)、寫服務(如評論、下單)、頻繁的複雜查詢(評論的最後幾頁),因此需有一種手段來限制這些場景的併發 / 請求量,即限流。
概念介紹
雪崩效應
一個應用可能會有多個微服務組成,微服務之間的數據交互通過遠程過程調用完成。
這就帶來一個問題,
假設微服務 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