1. 程式人生 > >降級特技之使用Hystrix實現降級和熔斷—《億級流量網站架構核心技術》

降級特技之使用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返回的響應。

  熔斷機制實現

  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);

  }

  即失敗(如異常)、超時、執行緒池拒絕、訊號量拒絕數量總和是失敗總數。

  配置示例

  下面是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。

  取樣統計

  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地址,然後進入儀表盤就可以看到統計資訊。