SpringCloud原始碼學習之Hystrix熔斷器
歡迎訪問陳同學部落格原文
文中原始碼基於 Spring Cloud Finchley.SR1、Spring Boot 2.0.6.RELEASE.
本文學習了Hystrix熔斷器的原理、配置和原始碼,包含滑動視窗、狀態變化等。
簡介
circuit-breaker: circuit表示電路,大家譯為熔斷器非常精準。
回想起小時候,家裡保險絲突然被燒斷,需 手工更換一根新的保險絲;後來,保險絲被取代,電流過大時會跳閘,閘拉上去後立馬恢復供電;等到上大學時,只要開啟功率高一點的電吹風,砰的一聲就斷電,但過10分鐘就自動來電。在電流過大時,通過熔斷機制以保護電路和家電。
Hystrix 屬於上面的第三種,一種自動恢復的智慧熔斷器,區別在於它保護的是系統,且判斷 “電流過大” 的方式是:不斷收集請求指標資訊(sucess、failure、timeout、rejection),當達到設定熔斷條件時(預設是請求失敗率達到50%)進行熔斷。
在 Spring Cloud 原始碼學習之 Hystrix Metrics 收集 一文中,學習了 Metrics 收集,這是上文的圖。
Hystrix Command 執行過程中,各種情況都以事件形式發出,再封裝成特定的資料結構,最後匯入到事件流中(HystrixEventStream)。事件流提供了 observe() 方法,搖身一變,事件流把自己變成了一個資料來源(各小溪匯入成河,消費者從河裡取水),其他消費者可以從這裡獲取資料,而 circuit-breaker 就是消費者之一。
原理
在統計中,會使用一定數量的樣本,並將樣本進行分組,最後進行統計分析。
Hystrix 有點類似,例如:以秒為單位來統計請求的處理情況(成功請求數量、失敗請求數、超時請求數、被拒絕的請求數),然後每次取最近10秒的資料來進行計算,如果失敗率超過50%,就進行熔斷,不再處理任何請求。
這是Hystrix官網的一張圖:
它演示了 Hystrix 滑動視窗 策略,假定以秒為單位來統計請求處理情況,上面每個格子代表1秒,格子中的資料就是1秒內各處理結果的請求數量,格子稱為 Bucket(譯為桶)。
若每次的決策都以10個Bucket的資料為依據,計算10個Bucket的請求處理情況,當失敗率超過50%時就熔斷。10個Bucket就是10秒,這個10秒就是一個 滑動視窗(Rolling window)。
為什麼叫滑動視窗?因為在沒有熔斷時,每當收集好一個新的Bucket後,就會丟棄掉最舊的一個Bucket。上圖中的深色的(23 5 2 0)就是被丟棄的桶,這和拿著放大鏡從左到右看書有點類似,視野永遠是放大鏡那一部分。
下面是官方完整的流程圖,策略是:不斷收集資料,達到條件就熔斷;熔斷後拒絕所有請求一段時間(sleepWindow);然後放一個請求過去,如果請求成功,則關閉熔斷器,否則繼續開啟熔斷器。
相關配置
預設配置都在HystrixCommandProperties類中。
先看兩個metrics收集的配置。
- metrics.rollingStats.timeInMilliseconds
表示滑動視窗的時間(the duration of the statistical rolling window),預設10000(10s),也是熔斷器計算的基本單位。
- metrics.rollingStats.numBuckets
滑動視窗的Bucket數量(the number of buckets the rolling statistical window is divided into),預設10. 通過timeInMilliseconds和numBuckets可以計算出每個Bucket的時長。
metrics.rollingStats.timeInMilliseconds % metrics.rollingStats.numBuckets 必須等於 0,否則將拋異常。
再看看熔斷器的配置。
- circuitBreaker.requestVolumeThreshold
滑動視窗觸發熔斷的最小請求數。如果值是20,但滑動視窗的時間內請求數只有19,那即使19個請求全部失敗,也不會熔斷,必須達到這個值才行,否則樣本太少,沒有意義。
- circuitBreaker.sleepWindowInMilliseconds
這個和熔斷器自動恢復有關,為了檢測後端服務是否恢復,可以放一個請求過去試探一下。sleepWindow指的發生熔斷後,必須隔sleepWindow這麼長的時間,才能放請求過去試探下服務是否恢復。預設是5s
- circuitBreaker.errorThresholdPercentage
錯誤率閾值,表示達到熔斷的條件。比如預設的50%,當一個滑動視窗內,失敗率達到50%時就會觸發熔斷。
原始碼學習
HystrixCircuitBreaker的建立
circuitBreaker是AbstractCommand的成員變數,AbstractCommand是HystrixCommand和HystrixObservableCommand的父類,因此每個command都有個circuitBreaker屬性。
abstract class AbstractCommand<R> implements HystrixInvokableInfo<R>, HystrixObservable<R> {
protected final HystrixCircuitBreaker circuitBreaker;
}
在AbstractCommand構造器中初始化circuitBreaker。
private static HystrixCircuitBreaker initCircuitBreaker(boolean enabled, HystrixCircuitBreaker fromConstructor, HystrixCommandKey commandKey...) {
// 如果啟用了熔斷器
if (enabled) {
// 若commandKey沒有對應的CircuitBreaker,則建立
if (fromConstructor == null) {
return HystrixCircuitBreaker.Factory.getInstance(commandKey, groupKey, properties, metrics);
} else {
// 如果有則返回現有的
return fromConstructor;
}
} else {
return new NoOpCircuitBreaker();
}
}
再看看 HystrixCircuitBreaker.Factory.getInstance(commandKey, groupKey, properties, metrics) 如何建立circuit-breakder?
circuitBreaker以commandKey為維度,每個commandKey都會有對應的circuitBreaker。
public static HystrixCircuitBreaker getInstance(HystrixCommandKey key, HystrixCommandGroupKey group, HystrixCommandProperties properties, HystrixCommandMetrics metrics) {
// 如果有則返回現有的, key.name()即command的name作為檢索條件
HystrixCircuitBreaker previouslyCached = circuitBreakersByCommand.get(key.name());
if (previouslyCached != null) {
return previouslyCached;
}
// 如果沒有則建立並cache
HystrixCircuitBreaker cbForCommand = circuitBreakersByCommand.putIfAbsent(key.name(), new HystrixCircuitBreakerImpl(key, group, properties, metrics));
if (cbForCommand == null) {
return circuitBreakersByCommand.get(key.name());
} else {
return cbForCommand;
}
}
如何訂閱HystrixEventStream
本文最前面說到,HystrixEventStream提供了結構化的資料,提供了一個Observable物件,Hystrix只需要訂閱它即可。
這HystrixCircuitBreaker介面實現類的構造器:
protected HystrixCircuitBreakerImpl(HystrixCommandKey key, HystrixCommandGroupKey commandGroup, final HystrixCommandProperties properties, HystrixCommandMetrics metrics) {
this.properties = properties;
// 這是Command中的metrics物件,metrics物件也是commandKey維度的
this.metrics = metrics;
// !!!重點:訂閱事件流
Subscription s = subscribeToStream();
activeSubscription.set(s);
}
// 訂閱事件流, 前面打的比方: 小溪匯成的河, 各事件以結構化資料匯入了Stream中
private Subscription subscribeToStream() {
// HealthCountsStream是重點,下面會分析
return metrics.getHealthCountsStream()
.observe()
// 利用資料統計的結果HealthCounts, 實現熔斷器
.subscribe(new Subscriber<HealthCounts>() {
@Override
public void onCompleted() {}
@Override
public void onError(Throwable e) {}
@Override
public void onNext(HealthCounts hc) {
// 檢查是否達到最小請求數,預設20個; 未達到的話即使請求全部失敗也不會熔斷
if (hc.getTotalRequests() < properties.circuitBreakerRequestVolumeThreshold().get()) {
// 啥也不做
} else {
// 錯誤百分比未達到設定的閥值
if (hc.getErrorPercentage() < properties.circuitBreakerErrorThresholdPercentage().get()) {
} else {
// 錯誤率過高, 進行熔斷
if (status.compareAndSet(Status.CLOSED, Status.OPEN)) {
circuitOpened.set(System.currentTimeMillis());
}
}
}
}
});
}
HealthCounts 屬性如下,表示一個滑動視窗內的統計資料。
public static class HealthCounts {
// rolling window 中請求總數量
private final long totalCount;
// 錯誤請求數(failure + success + timeout + threadPoolRejected + semaphoreRejected)
private final long errorCount;
// 錯誤率
private final int errorPercentage;
}
滑動視窗的實現
跟進下 metrics.getHealthCountsStream().observe(),那熔斷器是如何進行資料統計的?
首先看下HystrixCommandMetrics的構造器,會初始化healthCountsStream這個健康統計資料流。
private HealthCountsStream healthCountsStream;
HystrixCommandMetrics(final HystrixCommandKey key, ...) {
// 又是以key為維度
healthCountsStream = HealthCountsStream.getInstance(key, properties);
}
對於HealthCountsStream的部分註釋如下,翻譯過來不夠準確,簡單中英對照下:
Maintains a stream of rolling health counts for a given Command.
它是一份流資料, 承載了指定Command的健康統計資料
There is a rolling window abstraction on this stream.
基於這個stream, 抽象出了滑動視窗
The HealthCounts object is calculated over a window of t1 milliseconds. This window has b buckets.
HealthCounts的資料是根據一個t1毫秒的滑動視窗計算得來,這個視窗有b個Buckets
Therefore, a new HealthCounts object is produced every t2 (=t1/b) milliseconds
因此, 每 t2 (=t1/b)毫秒就會產生一個HealthCounts作為統計結果
HealthCountsStream.getInstance如下:
public static HealthCountsStream getInstance(HystrixCommandKey commandKey, HystrixCommandProperties properties) {
// 每個Bucket的時間長度
final int healthCountBucketSizeInMs = properties.metricsHealthSnapshotIntervalInMilliseconds().get();
// 滑動視窗的時間/每個Bucket的時間長度=滑動視窗內Bucket的數量
final int numHealthCountBuckets = properties.metricsRollingStatisticalWindowInMilliseconds().get() / healthCountBucketSizeInMs;
return getInstance(commandKey, numHealthCountBuckets, healthCountBucketSizeInMs);
}
// 以Key為維度,每個Key有自己唯一的一個HealthCountsStream
public static HealthCountsStream getInstance(HystrixCommandKey commandKey, int numBuckets, int bucketSizeInMs) {
HealthCountsStream initialStream = streams.get(commandKey.name());
if (initialStream != null) {
return initialStream;
} else {
final HealthCountsStream healthStream;
synchronized (HealthCountsStream.class) {
HealthCountsStream existingStream = streams.get(commandKey.name());
if (existingStream == null) {
// appendEventToBucket是一個Func2,負責將Hystrix各個事件轉換成一個Bucket
HealthCountsStream newStream = new HealthCountsStream(commandKey, numBuckets, bucketSizeInMs,
HystrixCommandMetrics.appendEventToBucket);
streams.putIfAbsent(commandKey.name(), newStream);
healthStream = newStream;
} else {
healthStream = existingStream;
}
}
healthStream.startCachingStreamValuesIfUnstarted();
return healthStream;
}
}
HealthCountsStream有兩個父類,HealthCountsStream extends BucketedRollingCounterStream extends BucketedCounterStream,利用父類將stream的基礎資料彙總成Bucket,再彙總成rolling window,最後得到統計結果HealthClounts.
下面按順序看一下它和父類的呼叫情況:
首先是 BucketedCounterStream:
public abstract class BucketedCounterStream<Event extends HystrixEvent, Bucket, Output> {
...
protected BucketedCounterStream(final HystrixEventStream<Event> inputEventStream, final int numBuckets, final int bucketSizeInMs,
final Func2<Bucket, Event, Bucket> appendRawEventToBucket) {
this.numBuckets = numBuckets;
// 將Hystrix事件彙總成Bucket的處理者, 是一個Func1
this.reduceBucketToSummary = new Func1<Observable<Event>, Observable<Bucket>>() {
// 傳入Event型別的資料來源,彙總成Bucket型別的資料
@Override
public Observable<Bucket> call(Observable<Event> eventBucket) {
...
}
};
...
this.bucketedStream = Observable.defer(new Func0<Observable<Bucket>>() {
@Override
public Observable<Bucket> call() {
// inputEventStream 就是一直提到的HystrixEventStream, 通過observe()來獲取資料來源
return inputEventStream
.observe()
// 利用視窗函式,收集一個Bucket時間內的資料
.window(bucketSizeInMs, TimeUnit.MILLISECONDS)
// 將資料彙總成一個Bucket
.flatMap(reduceBucketToSummary)
.startWith(emptyEventCountsToStart);
}
});
}
}
通過BucketedCounterStream,將資料彙總成了以Bucket為單位的stream. 然後,BucketedRollingCounterStream基於Bucket的stream,繼續實現滑動視窗邏輯。
protected BucketedRollingCounterStream(HystrixEventStream<Event> stream, final int numBuckets, int bucketSizeInMs,
final Func2<Bucket, Event, Bucket> appendRawEventToBucket,
final Func2<Output, Bucket, Output> reduceBucket) {
super(stream, numBuckets, bucketSizeInMs, appendRawEventToBucket);
// Bucket彙總處理者
Func1<Observable<Bucket>, Observable<Output>> reduceWindowToSummary = new Func1<Observable<Bucket>, Observable<Output>>() {
@Override
public Observable<Output> call(Observable<Bucket> window) {
return window.scan(getEmptyOutputValue(), reduceBucket).skip(numBuckets);
}
};
// 基於父類BucketedCounterStream已經彙總的bucketedStream
this.sourceStream = bucketedStream
// 將N個Bucket進行彙總
.window(numBuckets, 1)
// 彙總成一個視窗
.flatMap(reduceWindowToSummary)
...
.share()
.onBackpressureDrop();
}
現在再回到熔斷器的邏輯:
private Subscription subscribeToStream() {
return metrics.getHealthCountsStream()
.observe()
.subscribe(new Subscriber<HealthCounts>() {
...
}
metrics.getHealthCountsStream()拿到的是一個已經彙總成以 “rollingWindow” 為單位的統計資料,observe() 實際拿到的是BucketedRollingCounterStream的sourceStream。如下:
public abstract class BucketedRollingCounterStream<...> {
private Observable<Output> sourceStream;
protected BucketedRollingCounterStream(...)
// sourceStream 已經是rollingWindow級別的統計資料
this.sourceStream = bucketedStream
.window(numBuckets, 1)
.flatMap(reduceWindowToSummary)...
}
@Override
public Observable<Output> observe() {
return sourceStream;
}
}
熔斷器就是利用最終的統計結果HealthCounts來判斷是否進行熔斷。
熔斷器狀態變化
熔斷器有三種狀態,如下:
enum Status {
CLOSED, OPEN, HALF_OPEN;
}
在Command的執行過程中,會呼叫HystrixCircuitBreaker的方法來更新狀態。下面是幾個重要的方法:
命令執行時,判斷熔斷器是否開啟
Spring Cloud 原始碼學習之 Hystrix 工作原理 中有介紹 Hystrix 如何實現其防護機制。
// 是否允許執行
public boolean attemptExecution() {
// 熔斷器配置了強制開啟, 不允許執行命令
if (properties.circuitBreakerForceOpen().get()) {
return false;
}
// 熔斷器配置了強制關閉, 允許執行
if (properties.circuitBreakerForceClosed().get()) {
return true;
}
// AtomicLong circuitOpened, -1是表示熔斷器未開啟
if (circuitOpened.get() == -1) {
return true;
} else {
// 熔斷後,會拒絕所有命令一段時間(預設5s), 稱為sleepWindow
if (isAfterSleepWindow()) {
// 過了sleepWindow後,將熔斷器設定為"HALF_OPEN",允許第一個請求過去
if (status.compareAndSet(Status.OPEN, Status.HALF_OPEN)) {
return true;
} else {
return false;
}
} else {
return false;
}
}
}
當Command成功執行結束時,會呼叫HystrixCircuitBreaker.markSuccess()來標記執行成功.
public void markSuccess() {
// 如果是HALF_OPEN狀態,則關閉熔斷器
if (status.compareAndSet(Status.HALF_OPEN, Status.CLOSED)) {
// 重新開始統計metrics,拋棄所有原先的metrics資訊
metrics.resetStream();
Subscription previousSubscription = activeSubscription.get();
if (previousSubscription != null) {
previousSubscription.unsubscribe();
}
Subscription newSubscription = subscribeToStream();
activeSubscription.set(newSubscription);
// circuitOpened設定為-1表示關閉熔斷器
circuitOpened.set(-1L);
}
}
當Command執行失敗時, 如果熔斷器屬於HALF_OPEN狀態,也就是熔斷器剛過sleepWindow時間,嘗試放一個請求過去,結果又失敗了,於是馬上開啟熔斷器,繼續拒絕sleepWindow的時間。
public void markNonSuccess() {
if (status.compareAndSet(Status.HALF_OPEN, Status.OPEN)) {
circuitOpened.set(System.currentTimeMillis());
}
}
這是呼叫markNonSuccess()的地方,handleFallback是所有失敗情況的處理者.
final Func1<Throwable, Observable<R>> handleFallback = new Func1<Throwable, Observable<R>>() {
@Override
public Observable<R> call(Throwable t) {
circuitBreaker.markNonSuccess();
Exception e = getExceptionFromThrowable(t);
executionResult = executionResult.setExecutionException(e);
// 執行緒池拒絕
if (e instanceof RejectedExecutionException) {
return handleThreadPoolRejectionViaFallback(e);
// 超時
} else if (t instanceof HystrixTimeoutException) {
return handleTimeoutViaFallback();
// Bad Request
} else if (t instanceof HystrixBadRequestException) {
return handleBadRequestByEmittingError(e);
} else {
if (e instanceof HystrixBadRequestException) {
eventNotifier.markEvent(HystrixEventType.BAD_REQUEST, commandKey);
return Observable.error(e);
}
return handleFailureViaFallback(e);
}
}
};
小結
斷斷續續寫了好幾天才寫完,寫作不易。Circuit-Breaker的設計、實現都很有意思:
- 滴水成河,收集每個命令的執行情況,彙總後通過滑動視窗,不斷動態計算最新統計資料,基於統計資料來開啟熔斷器
- 巧妙的利用RxJava的window()函式來彙總資料,先彙總為Bucket, N Bucket組成Rolling Window
- 使用sleepWindow + 嘗試機制,自動恢復 “供電”
相關文章
- springcloud之Hystrix熔斷器SpringGCCloud
- SpringCloud學習筆記:熔斷器Hystrix(5)SpringGCCloud筆記
- springcloud(四):熔斷器HystrixSpringGCCloud
- springcloud之hystrix熔斷器-Finchley.SR2版SpringGCCloud
- 跟我學SpringCloud | 第四篇:熔斷器HystrixSpringGCCloud
- 《SpringCloud專題17》-Hystrix熔斷器案例SpringGCCloud
- 熔斷器 Hystrix 原始碼解析 —— 執行命令方式原始碼
- (24)SpringCloud-Hystrix(熔斷器)介紹及使用SpringGCCloud
- springcloud(五):熔斷監控Hystrix DashboardSpringGCCloud
- 微服務SpringCloud之熔斷監控Hystrix Dashboard和Turbine微服務SpringGCCloud
- 微服務SpringCloud之熔斷器微服務SpringGCCloud
- SpringCloud-Hystrix 服務降級、熔斷SpringGCCloud
- Hystrix--熔斷
- 微服務熔斷限流Hystrix之Dashboard微服務
- 跟我學SpringCloud | 第五篇:熔斷監控Hystrix Dashboard和TurbineSpringGCCloud
- SpringCloud(三)Hystrix斷路器SpringGCCloud
- SpringCloud Netflix (五) : Hystrix 服務熔斷和服務降級SpringGCCloud
- SpringCloud之斷路器聚合監控(Hystrix Turbine)SpringGCCloud
- 微服務11:熔斷、降級的Hystrix實現(附原始碼)微服務原始碼
- Spring cloud(4)-熔斷(Hystrix)SpringCloud
- Spring Cloud實戰系列(四) - 熔斷器HystrixSpringCloud
- Spring Cloud 原始碼學習之 Hystrix 入門SpringCloud原始碼
- SpringCloud之HystrixSpringGCCloud
- 微服務熔斷限流Hystrix之流聚合微服務
- java B2B2C 原始碼多租戶電子商城系統-熔斷器HystrixJava原始碼
- Java springcloud B2B2C o2o多使用者商城 springcloud架構(四):熔斷器HystrixJavaSpringGCCloud架構
- 從kratos分析breaker熔斷器原始碼實現原始碼
- 使用springcloud gateway搭建閘道器(分流,限流,熔斷)SpringGCCloudGateway
- springcloud(六):熔斷監控TurbineSpringGCCloud
- 分散式服務防雪崩熔斷器,Hystrix理論+實戰。分散式
- springcloud學習筆記(四)Spring Cloud HystrixSpringGCCloud筆記
- 微服務元件之限流器與熔斷器微服務元件
- springCloud入門學習--Hystrix狀態監控SpringGCCloud
- SpringCloud 2020.0.4 系列之Hystrix看板SpringGCCloud
- 五. SpringCloud服務降級與熔斷SpringGCCloud
- Springcloud原始碼學習筆記1—— Zuul閘道器原理SpringGCCloud原始碼筆記Zuul
- java spring cloud 版b2b2c社交電商-熔斷器HystrixJavaSpringCloud
- springCloud學習3(Netflix Hystrix彈性客戶端)SpringGCCloud客戶端