服務熔斷
在介紹熔斷機制之前,我們需要了解微服務的雪崩效應。在微服務架構中,微服務是完成一個單一的業務功能,這樣做的好處是可以做到解耦,每個微服務可以獨立演進。但是,一個應用可能會有多個微服務組成,微服務之間的數據交互通過遠程過程調用完成。這就帶來一個問題,假設微服務A調用微服務B和微服務C,微服務B和微服務C又調用其它的微服務,這就是所謂的“扇出”。如果扇出的鏈路上某個微服務的調用響應時間過長或者不可用,對微服務A的調用就會占用越來越多的系統資源,進而引起系統崩潰,所謂的“雪崩效應”。
熔斷機制是應對雪崩效應的一種微服務鏈路保護機制。我們在各種場景下都會接觸到熔斷這兩個字。高壓電路中,如果某個地方的電壓過高,熔斷器就會熔斷,對電路進行保護。股票交易中,如果股票指數過高,也會采用熔斷機制,暫停股票的交易。同樣,在微服務架構中,熔斷機制也是起着類似的作用。當扇出鏈路的某個微服務不可用或者響應時間太長時,會進行服務的降級,進而熔斷該節點微服務的調用,快速返回錯誤的響應信息。當檢測到該節點微服務調用響應正常后,恢復調用鏈路。
在Spring Cloud框架里,熔斷機制通過Hystrix實現。Hystrix會監控微服務間調用的狀況,當失敗的調用到一定閾值,缺省是5秒內20次調用失敗,就會啟動熔斷機制。
在dubbo中也可利用nio超時+失敗次數做熔斷。
dubbo可以通過擴展Filter的方式引入Hystrix,具體代碼如下:
package com.netease.hystrix.dubbo.rpc.filter; import com.alibaba.dubbo.common.Constants; import com.alibaba.dubbo.common.extension.Activate; import com.alibaba.dubbo.rpc.Filter; import com.alibaba.dubbo.rpc.Invocation; import com.alibaba.dubbo.rpc.Invoker; import com.alibaba.dubbo.rpc.Result; import com.alibaba.dubbo.rpc.RpcException; @Activate(group = Constants.CONSUMER) public class HystrixFilter implements Filter { @Override public Result invoke(Invoker invoker, Invocation invocation) throws RpcException { DubboHystrixCommand command = new DubboHystrixCommand(invoker, invocation); return command.execute(); } }
DubboHystrixCommand
package com.netease.hystrix.dubbo.rpc.filter; import org.apache.log4j.Logger; import com.alibaba.dubbo.common.URL; import com.alibaba.dubbo.rpc.Invocation; import com.alibaba.dubbo.rpc.Invoker; import com.alibaba.dubbo.rpc.Result; import com.netflix.hystrix.HystrixCommand; import com.netflix.hystrix.HystrixCommandGroupKey; import com.netflix.hystrix.HystrixCommandKey; import com.netflix.hystrix.HystrixCommandProperties; import com.netflix.hystrix.HystrixThreadPoolProperties; public class DubboHystrixCommand extends HystrixCommand { private static Logger logger = Logger.getLogger(DubboHystrixCommand.class); private static final int DEFAULT_THREADPOOL_CORE_SIZE = 30; private Invoker invoker; private Invocation invocation; public DubboHystrixCommand(Invoker invoker,Invocation invocation){ super(Setter.withGroupKey(HystrixCommandGroupKey.Factory.asKey(invoker.getInterface().getName())) .andCommandKey(HystrixCommandKey.Factory.asKey(String.format("%s_%d", invocation.getMethodName(), invocation.getArguments() == null ? 0 : invocation.getArguments().length))) .andCommandPropertiesDefaults(HystrixCommandProperties.Setter() .withCircuitBreakerRequestVolumeThreshold(20)//10秒鍾內至少19此請求失敗,熔斷器才發揮起作用 .withCircuitBreakerSleepWindowInMilliseconds(30000)//熔斷器中斷請求30秒后會進入半打開狀態,放部分流量過去重試 .withCircuitBreakerErrorThresholdPercentage(50)//錯誤率達到50開啟熔斷保護 .withExecutionTimeoutEnabled(false))//使用dubbo的超時,禁用這里的超時 .andThreadPoolPropertiesDefaults(HystrixThreadPoolProperties.Setter().withCoreSize(getThreadPoolCoreSize(invoker.getUrl()))));//線程池為30 this.invoker=invoker; this.invocation=invocation; } /** * 獲取線程池大小 * * @param url * @return */ private static int getThreadPoolCoreSize(URL url) { if (url != null) { int size = url.getParameter("ThreadPoolCoreSize", DEFAULT_THREADPOOL_CORE_SIZE); if (logger.isDebugEnabled()) { logger.debug("ThreadPoolCoreSize:" + size); } return size; } return DEFAULT_THREADPOOL_CORE_SIZE; } @Override protected Result run() throws Exception { return invoker.invoke(invocation); } }
線程池大小可以通過dubbo參數進行控制,當前其他的參數也可以通過類似的方式進行配置
代碼添加好后在,resource添加加載文本
|-resources
|-META-INF
|-dubbo
|-com.alibaba.dubbo.rpc.Filter (純文本文件,內容為:hystrix=com.netease.hystrix.dubbo.rpc.filter.HystrixFilter
由於Filter定義為自動激活的,所以啟動代碼所有消費者都被隔離起來啦!
熔段解決如下幾個問題:
當所依賴的對象不穩定時,能夠起到快速失敗的目的
快速失敗后,能夠根據一定的算法動態試探所依賴對象是否恢復
服務降級
降級是指自己的待遇下降了,從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可拿了就阻塞或者拒絕服務
令牌桶的另外一個好處是可以方便的改變速度. 一旦需要提高速率,則按需提高放入桶中的令牌的速率. 一般會定時(比如100毫秒)往桶中增加一定數量的令牌, 有些變種算法則實時的計算應該增加的令牌的數量.
應用級限流
對於一個應用系統來說一定會有極限並發/請求數,即總有一個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 的設計者選擇的是后者,先把眼前的活干了,后面的事后面再說。