SpringCloud原始碼學習之Hystrix熔斷器

genter發表於2018-12-13

歡迎訪問陳同學部落格原文
文中原始碼基於 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 + 嘗試機制,自動恢復 “供電”


相關文章