服務熔斷、服務降級、服務限流
服務熔斷
在介紹熔斷機制之前,我們需要了解微服務的雪崩效應。在微服務架構中,微服務是完成一個單一的業務功能,這樣做的好處是可以做到解耦,每個微服務可以獨立演進。但是,一個應用可能會有多個微服務組成,微服務之間的資料互動通過遠端過程呼叫完成。這就帶來一個問題,假設微服務A呼叫微服務B和微服務C,微服務B和微服務C又呼叫其它的微服務,這就是所謂的“扇出”。如果扇出的鏈路上某個微服務的呼叫響應時間過長或者不可用,對微服務A的呼叫就會佔用越來越多的系統資源,進而引起系統崩潰,所謂的雪崩效應。
熔斷機制是應對雪崩效應的一種微服務鏈路保護機制。我們在各種場景下都會接觸到熔斷這兩個字。高壓電路中,如果某個地方的電壓過高,熔斷器就會熔斷,對電路進行保護。股票交易中,如果股票指數過高,也會採用熔斷機制,暫停股票的交易。同樣,在微服務架構中,熔斷機制也是起著類似的作用。當扇出鏈路的某個微服務不可用或者響應時間太長時,會進行服務的降級,進而熔斷該節點微服務的呼叫,快速返回錯誤的響應資訊。當檢測到該節點微服務呼叫響應正常後,恢復呼叫鏈路。
在Spring Cloud框架裡,熔斷機制通過Hystrix實現。Hystrix會監控微服務間呼叫的狀況,當失敗的呼叫到一定閾值,預設是5秒內20次呼叫失敗,就會啟動熔斷機制。
服務降級
降級是指自己的待遇下降了,從RPC呼叫環節來講,就是去訪問一個本地的偽裝者而不是真實的服務。
當雙11活動時,把無關交易的服務統統降級,如檢視螞蟻深林,檢視歷史訂單,商品歷史評論,只顯示最後100條等等。
熔斷和降級區別
相同點:
- 目的很一致,都是從可用性可靠性著想,為防止系統的整體緩慢甚至崩潰,採用的技術手段;
- 最終表現類似,對於兩者來說,最終讓使用者體驗到的是某些功能暫時不可達或不可用;
- 粒度一般都是服務級別,當然,業界也有不少更細粒度的做法,比如做到資料持久層(允許查詢,不允許增刪改);
- 自治性要求很高,熔斷模式一般都是服務基於策略的自動觸發,降級雖說可人工干預,但在微服務架構下,完全靠人顯然不可能,開關預置、配置中心都是必要手段;
區別:
- 觸發原因不太一樣,服務熔斷一般是某個服務(下游服務)故障引起,而服務降級一般是從整體負荷考慮;
- 管理目標的層次不太一樣,熔斷其實是一個框架級的處理,每個微服務都需要(無層級之分),而降級一般需要對業務有層級之分(比如降級一般是從最外圍服務開始)
- 實現方式不太一樣;服務降級具有程式碼侵入性(由控制器完成/或自動降級),熔斷一般稱為自我熔斷。
服務限流
在開發高併發系統時有三把利器用來保護系統:快取、降級和限流。快取的目的是提升系統訪問速度和增大系統能處理的容量,可謂是抗高併發流量的銀彈;而降級是當服務出問題或者影響到核心流程的效能則需要暫時遮蔽掉,待高峰或者問題解決後再開啟;而有些場景並不能用快取和降級來解決,比如稀缺資源(秒殺、搶購)、寫服務(如評論、下單)、頻繁的複雜查詢(評論的最後幾頁),因此需有一種手段來限制這些場景的併發/請求量,即限流
限流的目的是通過對併發訪問/請求進行限速或者一個時間視窗內的的請求進行限速來保護系統,一旦達到限制速率則可以拒絕服務(定向到錯誤頁或告知資源沒有了)、排隊或等待(比如秒殺、評論、下單)、降級(返回兜底資料或預設資料,如商品詳情頁庫存預設有貨)。
一般開發高併發系統常見的限流有:限制總併發數(比如資料庫連線池、執行緒池)、限制瞬時併發數(如nginx的limit_conn模組,用來限制瞬時併發連線數)、限制時間視窗內的平均速率(如Guava的RateLimiter、nginx的limit_req模組,限制每秒的平均速率);其他還有如限制遠端介面呼叫速率、限制MQ的消費速率。另外還可以根據網路連線數、網路流量、CPU或記憶體負載等來限流。
限流演算法
常見的限流演算法有:令牌桶、漏桶。計數器也可以進行粗暴限流實現。
漏桶(Leaky Bucket)演算法思路很簡單,水(請求)先進入到漏桶裡,漏桶以一定的速度出水(介面有響應速率),當水流入速度過大會直接溢位(訪問頻率超過介面響應速率),然後就拒絕請求,可以看出漏桶演算法能強行限制資料的傳輸速率.示意圖如下:
令牌桶演算法(Token Bucket)和 Leaky Bucket 效果一樣但方向相反的演算法,更加容易理解.隨著時間流逝,系統會按恆定1/QPS時間間隔(如果QPS=100,則間隔是10ms)往桶裡加入Token(想象和漏洞漏水相反,有個水龍頭在不斷的加水),如果桶已經滿了就不再加了.新請求來臨時,會各自拿走一個Token,如果沒有Token可拿了就阻塞或者拒絕服務
應用級限流
對於一個應用系統來說一定會有極限併發/請求數,即總有一個TPS/QPS閥值,如果超了閥值則系統就會不響應使用者請求或響應的非常慢,因此我們最好進行過載保護,防止大量請求湧入擊垮系統。
如果你使用過Tomcat,其Connector其中一種配置有如下幾個引數:
acceptCount:如果Tomcat的執行緒都忙於響應,新來的連線會進入佇列排隊,如果超出排隊大小,則拒絕連線;
maxConnections:瞬時最大連線數,超出的會排隊等待;
maxThreads:Tomcat能啟動用來處理請求的最大執行緒數,如果請求處理量一直遠遠大於最大執行緒數則可能會僵死。
詳細的配置請參考官方文件。另外如MySQL(如max_connections)、Redis(如tcp-backlog)都會有類似的限制連線數的配置。
池化技術
如果有的資源是稀缺資源(如資料庫連線、執行緒),而且可能有多個系統都會去使用它,那麼需要限制應用;可以使用池化技術來限制總資源數:連線池、執行緒池。比如分配給每個應用的資料庫連線是100,那麼本應用最多可以使用100個資源,超出了可以等待或者拋異常。
限流某個介面的總併發/請求數
如果介面可能會有突發訪問情況,但又擔心訪問量太大造成崩潰,如搶購業務;這個時候就需要限制這個介面的總併發/請求數總請求數了;因為粒度比較細,可以為每個介面都設定相應的閥值。可以使用Java中的AtomicLong進行限流:
try {
if(atomic.incrementAndGet() > 限流數) {
//拒絕請求
}
//處理請求
} finally {
atomic.decrementAndGet();
}
分散式限流
分散式限流最關鍵的是要將限流服務做成原子化,而解決方案可以使使用redis+lua或者nginx+lua技術進行實現,通過這兩種技術可以實現的高併發和高效能。
首先我們來使用redis+lua實現時間窗內某個介面的請求數限流,實現了該功能後可以改造為限流總併發/請求數和限制總資源數。Lua本身就是一種程式語言,也可以使用它實現複雜的令牌桶或漏桶演算法。
有人會糾結如果應用併發量非常大那麼redis或者nginx是不是能抗得住;不過這個問題要從多方面考慮:你的流量是不是真的有這麼大,是不是可以通過一致性雜湊將分散式限流進行分片,是不是可以當併發量太大降級為應用級限流;對策非常多,可以根據實際情況調節;像在京東使用Redis+Lua來限流搶購流量,一般流量是沒有問題的。
對於分散式限流目前遇到的場景是業務上的限流,而不是流量入口的限流;流量入口限流應該在接入層完成,而接入層筆者一般使用Nginx。
基於Redis功能的實現限流
簡陋的設計思路:假設一個使用者(用IP判斷)每分鐘訪問某一個服務介面的次數不能超過10次,那麼我們可以在Redis中建立一個鍵,並此時我們就設定鍵的過期時間為60秒,每一個使用者對此服務介面的訪問就把鍵值加1,在60秒內當鍵值增加到10的時候,就禁止訪問服務介面。在某種場景中新增訪問時間間隔還是很有必要的。
基於令牌桶演算法的實現
令牌桶演算法最初來源於計算機網路。在網路傳輸資料時,為了防止網路擁塞,需限制流出網路的流量,使流量以比較均勻的速度向外傳送。令牌桶演算法就實現了這個功能,可控制傳送到網路上資料的數目,並允許突發資料的傳送。
Java實現
我們可以使用Guava 的 RateLimiter 來實現基於令牌桶的流控,RateLimiter 令牌桶演算法是單桶實現。RateLimiter 對簡單的令牌桶演算法做了一些工程上的優化,具體的實現是 SmoothBursty。需要注意的是,RateLimiter 的另一個實現SmoothWarmingUp,就不是令牌桶了,而是漏桶演算法。也許是出於簡單起見,RateLimiter 中的時間視窗能且僅能為 1s。
SmoothBursty 有一個可以放 N 個時間視窗產生的令牌的桶,系統空閒的時候令牌就一直攢著,最好情況下可以扛 N 倍於限流值的高峰而不影響後續請求。RateLimite允許某次請求拿走超出剩餘令牌數的令牌,但是下一次請求將為此付出代價,一直等到令牌虧空補上,並且桶中有足夠本次請求使用的令牌為止。當某次請求不能得到所需要的令牌時,這時涉及到一個權衡,是讓前一次請求乾等到令牌夠用才走掉呢,還是讓它先走掉後面的請求等一等呢?Guava 的設計者選擇的是後者,先把眼前的活幹了,後面的事後面再說。