降級特技之使用Hystrix實現降級和熔斷—《億級流量網站架構核心技術》
使用Hystrix實現降級
通過配置中心可以人工進行降級,而我們也需要根據服務的超時時間進行自動降級,本部分將演示使用Hystrix實現超時自動降級。Hystrix介紹請參考“第3章 隔離術”中的Hystrix簡介部分。
public class GetStockServiceCommand extends HystrixCommand<String> {
private StockService stockService;
public GetStockServiceCommand(StockService stockService) {
super(setter());
this.stockService= stockService;
}
private static Setter setter() {
//服務分組
HystrixCommandGroupKey groupKey = HystrixCommandGroupKey.Factory. asKey("stock");
……
//命令配置
HystrixCommandProperties.Setter commandProperties = HystrixCommandProperties.Setter()
……
.withExecutionIsolationStrategy(HystrixCommandProperties.ExecutionIsolationStrategy.THREAD)
.withFallbackEnabled(true)//預設true
.withFallbackIsolationSemaphoreMaxConcurrentRequests(100)//預設10
.withExecutionIsolationThreadInterruptOnFutureCancel(true) //預設false
.withExecutionIsolationThreadInterruptOnTimeout(true)//預設true
.withExecutionTimeoutEnabled(true) //預設true
.withExecutionTimeoutInMilliseconds(1000)//預設1000
;
return HystrixCommand.Setter
.withGroupKey(groupKey)
.andCommandPropertiesDefaults(commandProperties);
}
@Override
protectedString run() throws Exception {
return stockService.getStock();//可以通過丟擲異常,或Thread.sleep模擬超時
}
@Override
protected String getFallback() {//降級方法
return "有貨";
}
}
整體執行流程如下圖所示。
首先,Command會呼叫run方法,如果run方法超時或者丟擲異常,如果啟用了降級處理,則呼叫getFallback方法進行降級。
而降級處理主要進行兩部分處理:HystrixCommandProperties配置和getFallback降級處理方法。首先,我們看下HystrixCommandProperties配置。
withFallbackEnabled:是否啟用降級處理,如果啟用了,則在超時或異常時呼叫getFallback進行降級處理,預設開啟。
withFallbackIsolationSemaphoreMaxConcurrentRequests:fallback方法的訊號量配置,配置getFallback方法併發請求的訊號量,如果請求超過了併發訊號量限制,則不再嘗試呼叫getFallback方法,而是快速失敗,預設訊號量為10。
withExecutionIsolationThreadInterruptOnFutureCancel:當隔離策略為THREAD時,當執行執行緒執行超時時,是否進行中斷處理,即Future#cancel(true)處理,預設為false。
withExecutionIsolationThreadInterruptOnTimeout:當隔離策略為THREAD時,當執行執行緒執行超時時,是否進行中斷處理,預設為true。
withExecutionTimeoutEnabled:是否啟用執行超時機制,預設為true;
withExecutionTimeoutInMilliseconds:執行超時時間,預設為1000毫秒,如果命令是執行緒隔離,且配置了executionIsolationThreadInterruptOnTimeout=true,則執行執行緒將執行中斷處理。如果命令是訊號量隔離,則進行終止操作,因為訊號量隔離與主執行緒是在一個執行緒中執行,其不會中斷執行緒處理,所以要根據實際情況來決定是否採用訊號量隔離,尤其涉及網路訪問的情況。
當開啟了降級處理,run方法超時或者異常時將會呼叫getFallback處理,getFallback需要注意以下幾點。
● 其最大併發數受fallbackIsolationSemaphoreMaxConcurrentRequests控制,因此,如果失敗率非常高,則要重新配置該引數,如果最大併發數超了該配置,則不會再執行getFallback,而是快速失敗,丟擲如“HystrixRuntimeException: GetStockServiceCommand fallback executionrejected”類似的異常。
● 該方法不能進行網路呼叫,應該只是快取的資料,或者靜態資料(如我們的庫存方法返回有貨)。
● 如果必須走網路呼叫,則應該在getFallback方法中呼叫另一個Command實現,通過Command可以有降級和熔斷機制保護應用,而getFallback只有fallbackIsolationSemaphoreMaxConcurrentRequests引數控制最大併發數。
在使用Command的業務程式碼處,可以使用如下方法獲取執行的狀態。
isResponseTimedOut:是否響應超時了。
isFailedExecution:是否執行失敗了,如丟擲了異常。
getFailedExecutionException:獲取失敗後的執行異常,即run方法丟擲的異常。
isResponseFromFallback:是否是getFallback返回的響應。
1 熔斷機制實現
Hystrix提供了熔斷實現,熔斷後會自動降級處理,如下圖所示。
Command首先呼叫HystrixCircuitBreaker#allowRequest判斷是否熔斷了,如果沒有熔斷,則執行Command#run方法正常處理,如果熔斷了,則直接呼叫降級方法Command#getFallback方法降級處理。
接下來,我們先看下HystrixCircuitBreakerImpl#allowRequest方法實現。
public boolean allowRequest() {
//1、如果熔斷開關強制開啟,則熔斷降級處理
if (properties.circuitBreakerForceOpen().get()){
return false;
}
//如果熔斷開關強制閉合,則正常處理
if (properties.circuitBreakerForceClosed().get()){
//還是需要呼叫isOpen方法進行取樣處理
isOpen();
return true;
}
//正常判斷
return !isOpen() || allowSingleTest();
}
//允許在一個時間視窗內進行單次訪問測試
public boolean allowSingleTest() {
//熔斷開關開啟時,最後一次測試時間
long timeCircuitOpenedOrWasLastTested= circuitOpenedOrLastTestedTime.get();
//如果熔斷開關處於開啟狀態,且在一個時間視窗內(circuitBreakerSleepWindowInMilliseconds),則允許一次訪問進行測試
if (circuitOpen.get() &&System.currentTimeMillis() > timeCircuitOpenedOrWasLastTested+properties.circuitBreakerSleepWindowInMilliseconds().get()){
if(circuitOpenedOrLastTestedTime.compareAndSet(timeCircuitOpenedOrWasLastTested,System.currentTimeMillis())) {
return true;
}
}
return false;
}
@Override
public boolean isOpen() {
//如果熔斷開關處於開啟狀態,則熔斷降級處理
if (circuitOpen.get()){
return true;
}
//熔斷開關當前處於閉合狀態,需要根據取樣判斷當前是否需要熔斷
HealthCounts health = metrics.getHealthCounts();
//如果當前取樣的總請求數小於circuitBreakerRequestVolumeThreshold閥值,則不進行熔斷
if (health.getTotalRequests() < properties.circuitBreakerRequestVolumeThreshold().get()) {
return false;
}
//如果當前取樣的錯誤率小於circuitBreakerErrorThresholdPercentage閥值,則不進行熔斷
//errorPercentage = errorCount / totalCount * 100
if (health.getErrorPercentage()< properties. circuitBreakerErrorThresholdPercentage().get()) {
return false;
} else {
//當前失敗率超過了閥值,進行熔斷降級處理
if (circuitOpen.compareAndSet(false,true)) {
circuitOpenedOrLastTestedTime.set(System.currentTimeMillis());
return true;
} else{
return true;
}
}
}
當我們的熔斷開關處於開啟狀態時,此時是不允許任何請求處理的,而是直接降級處理,但是提供了markSuccess方法,當請求處理成功時進行熔斷開關閉合。
public void markSuccess() {
if (circuitOpen.get()){
if(circuitOpen.compareAndSet(true, false)) {
//重置health取樣,不影響其他採用
metrics.resetStream();
}
}
}
通過circuitBreakerSleepWindowInMilliseconds可以控制一個時間視窗內可進行一次請求測試,如果測試成功,則閉合熔斷開關,否則還是開啟狀態,從而實現了快速失敗和快速恢復。
關於熔斷開關需要知道如下幾個概念。
閉合(Closed):如果配置了熔斷開關強制閉合,或者當前請求失敗率沒有超過失敗率閥值,則熔斷開關處於閉合狀態,不啟動熔斷機制,即不進行降級處理。
開啟(Open):如果配置了熔斷開關強制開啟,或者當前失敗率超過失敗率閥值,則熔斷開關開啟,啟動熔斷機制,根據配置呼叫降級處理方法getFallback進行降級處理。
半開啟(Half-Open):當熔斷處於開啟狀態後,不能一直熔斷下去,需要在一個時間視窗後進行重試,這種狀態就是半開啟。Hystrix允許在circuit BreakerSleepWindowInMilliseconds視窗內進行一次重試,重試成功則閉合熔斷開關,否則熔斷開關還是處於開啟狀態。
那什麼樣的請求被認為是錯誤呢,HealthCounts在統計錯誤數量時使用如下方法。
public HealthCounts plus(long[] eventTypeCounts) {
long updatedTotalCount= totalCount;
long updatedErrorCount= errorCount;
long successCount =eventTypeCounts[HystrixEventType.SUCCESS. ordinal()];
long failureCount =eventTypeCounts[HystrixEventType.FAILURE. ordinal()];
long timeoutCount =eventTypeCounts[HystrixEventType.TIMEOUT. ordinal()];
long threadPoolRejectedCount= eventTypeCounts[HystrixEventType. THREAD_POOL_REJECTED.ordinal()];
long semaphoreRejectedCount= eventTypeCounts[HystrixEventType. SEMAPHORE_REJECTED.ordinal()];
updatedTotalCount += (successCount + failureCount + timeoutCount +threadPoolRejectedCount + semaphoreRejectedCount);
updatedErrorCount += (failureCount+ timeoutCount + threadPoolRejectedCount + semaphoreRejectedCount);
return new HealthCounts(updatedTotalCount, updatedErrorCount);
}
即失敗(如異常)、超時、執行緒池拒絕、訊號量拒絕數量總和是失敗總數。
2 配置示例
下面是HystrixCommandProperties的熔斷引數配置。
HystrixCommandProperties.Setter commandProperties =HystrixCommandProperties. Setter()
……
.withCircuitBreakerEnabled(true)//預設為true
.withCircuitBreakerForceClosed(false)//預設為false
.withCircuitBreakerForceOpen(false)//預設為false
.withCircuitBreakerErrorThresholdPercentage(50)//預設為50%
.withCircuitBreakerRequestVolumeThreshold(20) //預設為20
.withCircuitBreakerSleepWindowInMilliseconds(5000)//預設為為5s
具體配置含義如下所示。
withCircuitBreakerEnabled:是否開啟熔斷機制,預設為true。
withCircuitBreakerForceClosed:是否強制關閉熔斷開關,如果強制關閉了熔斷開關,則請求不會被降級,一些特殊場景可以動態配置該開關,預設為false。
withCircuitBreakerForceOpen:是否強制開啟熔斷開關,如果強制開啟可熔斷開關,則請求強制降級呼叫getFallback處理,可以通過動態配置來開啟該開關實現一些特殊需求,預設為false。
withCircuitBreakerErrorThresholdPercentage:如果在一個取樣時間視窗內,失敗率超過該配置,則自動開啟熔斷開關實現降級處理,即快速失敗。預設配置下采樣週期為10s,失敗率為50%。
withCircuitBreakerRequestVolumeThreshold:在熔斷開關閉合情況下,在進行失敗率判斷之前,一個取樣週期內必須進行至少N個請求才能進行取樣統計,目的是有足夠的取樣使得失敗率計算正確,預設為20。
withCircuitBreakerSleepWindowInMilliseconds:熔斷後的重試時間視窗,且在該時間視窗內只允許一次重試。即在熔斷開關開啟後,在該時間視窗允許有一次重試,如果重試成功,則將重置Health取樣統計並閉合熔斷開關實現快速恢復,否則熔斷開關還是開啟狀態,執行快速失敗。
熔斷後將降級呼叫getFallback進行處理(fallbackEnabled=true),通過Command如下方法可以判斷是否熔斷了。
isCircuitBreakerOpen:熔斷開關是否打開了,通過“circuitBreakerForceOpen().get()|| (!circuitBreakerForceClosed().get() && circuitBreaker.isOpen())”判斷。
isResponseShortCircuited:isCircuitBreakerOpen=true,且呼叫getFallback時返回true。
3 取樣統計
Hystrix在記憶體中儲存取樣資料,支援如下兩種取樣。
BucketedCounterStream:計數統計,比如記錄一定時間視窗內的失敗、超時、執行緒池拒絕、訊號量拒絕數量,記錄N組。寫入資料時寫到第N組,統計時使用前N-1組資料,因為第N個剛開始統計時是隨時變化的。然後基於時間滾轉取樣分組即可。
取樣統計滾轉時間視窗為10s,每秒1個分組(桶),即每秒取樣一次,每個分組記錄著當前桶的成功、失敗、超時、執行緒拒絕統計數量。
RollingConcurrencyStream:最大併發數統計,如Command/ThreadPool的最大併發數。
RollingDistributionStream:延時百分比統計,同HystrixRollingNumber類似,差別在於其是百分位數的統計。比如每組記錄P(比如100)個數值,統計時使用前N-1組資料,將分組內資料按從小到大排序,然後累加,處於第p%位置的數值就是p百分位數,通過它可以實現P50、P99、P999,Hystrix用來統計時延的分佈情況。最新版本Hystrix使用HdrHistogram庫來實現統計。
3.1 Command、ThreadPool計數/最大併發取樣統計
HystrixThreadPoolProperties.Setter threadPoolProperties = HystrixThreadPoolProperties.Setter()
……
.withMetricsRollingStatisticalWindowInMilliseconds(1000)
.withMetricsRollingStatisticalWindowBuckets(10);
HystrixCommandProperties.Setter commandProperties =HystrixCommandProperties. Setter()
……
.withMetricsRollingStatisticalWindowInMilliseconds(10000)
.withMetricsRollingStatisticalWindowBuckets(10);
withMetricsRollingStatisticalWindowInMilliseconds:配置取樣統計滾轉時間視窗,預設為10s。
withMetricsRollingStatisticalWindowBuckets:配置採用統計滾轉時間視窗內的桶的總數量,預設為10,比如時間視窗為10000,桶數量為10,則取樣統計間隔為每秒一個桶統計。
3.2 Command健康度取樣統計
HystrixCommandProperties.Setter commandProperties =HystrixCommandProperties. Setter()
……
.withMetricsRollingStatisticalWindowInMilliseconds(10000)
.withMetricsHealthSnapshotIntervalInMilliseconds(500);
withMetricsRollingStatisticalWindowInMilliseconds:同上所示。
withMetricsHealthSnapshotIntervalInMilliseconds:記錄健康採用統計的快照頻率,預設為500ms,即500ms一個取樣統計間隔,那麼桶的數量為10000/500=20個。
該統計在熔斷機制中使用,如果計算熔斷的頻率非常高,則要控制好取樣的頻率,如果太頻繁,那麼將造成CPU計算密集,如10ms一個週期,因為會對前N-1個桶進行統計,計算累加時會耗費CPU。所以選擇Hystrix要注意此處的效能消耗和調優。如果此處是效能瓶頸,則可以廢掉統計,或者按照Hystrix思路實現自己的降級元件。
3.3 Command時延分佈取樣統計
HystrixCommandProperties.Setter commandProperties = HystrixCommandProperties.Setter()
……
.withMetricsRollingPercentileWindowInMilliseconds(60000)
.withMetricsRollingPercentileWindowBuckets(6);
同withMetricsRollingStatisticalWindowInMilliseconds和withMetricsRollingStatisticalWindowBuckets,預設取樣滾轉時間視窗為60s,總共6個桶,即取樣統計間隔為每10秒一個桶統計。
4.統計結果
可以呼叫Command#getMetrics獲取取樣統計,然後通過HystrixCommandMetrics相關方法獲取統計資料。
getExecutionTimePercentile(50);//P50
getExecutionTimePercentile(99);//P99
getExecutionTimePercentile(999);//P999
也可以訂閱HystrixDashboardStream.getInstance()進行統計。Hystrix提供了hystrix-dashboard進行圖形化展示。
接下來我們通過turbine + hystrix-dashboard實現叢集化的統計視覺化。
首先,Hystrix應用會暴露統計介面,然後Turbine會聚合這些統計資料,Hystrix Dashboard會拉取聚合後的統計資訊展示到儀表盤上。
5.Hystrix客戶端新增暴露統計資訊Servlet
@Bean
public ServletRegistrationBean servletRegistrationBean() {
returnnew ServletRegistrationBean(new HystrixMetricsStreamServlet(), "/hystrix.stream");
}
6.部署Turbine
下載Turbine WAR包(本文使用的是Turbine 1.0.0),部署到Tomcat中,然後修改WEB-INF/classes/config.properties配置,啟動Tomcat。
turbine.ConfigPropertyBasedDiscovery.default.instances=127.0.0.1
turbine.instanceUrlSuffix=:9080/hystrix.stream
7.部署Hystrix Dashboard
下載 hystrix-dashboard WAR包(本文使用的是hystrix-dashboard 1.5.6),部署到Tomcat中,然後啟動Tomcat。訪問如http://127.0.0.1:8080/hystrix-dashboard啟動儀表盤。
在如下介面新增要監控的Turbine地址,然後進入儀表盤就可以看到統計資訊。