[享學Netflix] 二十三、Netflix Hystrix桶計數器:BucketedCounterStream

入一行,先別惦記着掙錢,而是要先讓自己值錢

–> 返回專欄總目錄 <–
代碼下載地址:https://github.com/f641385712/netflix-learning

前言

上篇文章介紹了Hystrix指標數據收集的數據源HystrixEvent和數據流HystrixEventStream,通過示例瞭解了Hystrix數據收集的基本模型,但實際上並不精確。

我們已經知道Hystrix它是通過滑動窗口的數據結構/算法來統計調用的指標數據的,但若直接使用HystrixEventStream作爲管道傳播數據的話,是點對點的,並無時間區間、時間窗口等概念。因此本文將以以這爲目的,深入瞭解與時間窗口相關的數據傳輸、收集處理的核心API:BucketedCounterStream


正文

Hystrix1.5版本(2016.12)開始,它全面擁抱RxJava把這塊代碼重寫設計爲了基於數據流Stream的形式,通過消費數據流的形式利用滑動窗口,並對數據流進行變換後進行後續的操作,可以讓開發者更加靈活地去使用。

滑動窗口本質就是不斷變換的數據流,滑動窗口中每個桶的數據都來自於源源不斷的事件,因此滑動窗口非常適合用觀察者模式和響應式編程思想的 RxJava 實現。

說明數據流Stream的實現強依賴與RxJava思想,推薦若對此還不太熟悉,請翻閱前幾篇文章or其它文章先了解RxJava的思想以及使用。

使用 RxJava可以通過它的一系列操作符來實現滑動窗口,從而可以依賴 RxJava 的線程模型來保證數據寫入和聚合的線程安全,將這一系列的機制交給 RxJava來得以保證。所有的操作都是在 RxJava 的後臺線程上進行的,這也大大降低了對業務線程的延遲性的影響。


Hystrix裏的滑動窗口

Hystrix通過滑動窗口來對數據進行“平滑”統計,默認情況下,一個滑動窗口包含10個桶(Bucket),每個桶時間寬度是1秒,負責1秒的數據統計。滑動窗口包含的總時間以及其中的桶數量都是可以配置的,來張官方的截圖認識下滑動窗口:

在這裏插入圖片描述
上圖的每個小矩形代表一個桶,可以看到,每個桶都記錄着1秒內的四個指標數據:成功量、失敗量、超時量和拒絕量,這裏的拒絕量指的就是上面流程圖中【信號量/線程池資源檢查】中被拒絕的流量。10個桶合起來是一個完整的滑動窗口,所以計算一個滑動窗口的總數據需要將10個桶的數據加起來。


BucketedCounterStream

滑動窗口所有的數據流實現均位於com.netflix.hystrix.metric.consumer這個包下,這裏先挑最頂層的類BucketedCounterStream進行說明。

BucketedCounterStream它是抽象類,提供了基本的桶計數器(BucketedCounter)實現:按配置的時間間隔將所有事件聚合成桶

該抽象類定義最爲基本的概念:桶、窗口

// Event:需要匯聚到桶裏面的原始事件類型(HystrixEvent是原始的,HystrixRollingNumberEvent是直接的)
	// Hystrix 中的調用事件,如命令開始執行、命令執行完成等
// Bucket:每個桶中包含的數據類型
// Output:最終輸出類型:發送給流訂閱者的數據類型(通常與Bucket相同,但不必相同)
public abstract class BucketedCounterStream<Event extends HystrixEvent, Bucket, Output> {

    protected final int numBuckets;
    protected final Observable<Bucket> bucketedStream;
    // 訂閱信息:允許訂閱or取消訂閱
    protected final AtomicReference<Subscription> subscription = new AtomicReference<Subscription>(null);

	// 它是一個函數。用於把Observable<Event>轉爲Observable<Bucket>
	private final Func1<Observable<Event>, Observable<Bucket>> reduceBucketToSummary;
	// 它是個Subject:既能發射數據,也能監聽數據
	// 用於計數
	private final BehaviorSubject<Output> counterSubject = BehaviorSubject.create(getEmptyOutputValue());


	// inputEventStream:事件流,input輸入。比如command執行開始、結束時都會有輸入
	// numBuckets:用戶不配置的話,默認它是10
	// bucketSizeInMs:窗口毫秒值。若不配置回事1秒
	// appendRawEventToBucket:它是一個函數 R call(T1 t1, T2 t2) 輸入Bucket, Event返回Bucket類型
    protected BucketedCounterStream(final HystrixEventStream<Event> inputEventStream, final int numBuckets, final int bucketSizeInMs,
                                    final Func2<Bucket, Event, Bucket> appendRawEventToBucket) {

	    this.numBuckets = numBuckets;

		// getEmptyBucketSummary是否抽象方法:獲取空桶
	    this.reduceBucketToSummary = eventBucket -> eventBucket.reduce(getEmptyBucketSummary(), appendRawEventToBucket);
	    final List<Bucket> emptyEventCountsToStart = new ArrayList<>();
	    for (int i = 0; i < numBuckets; i++) {
	        emptyEventCountsToStart.add(getEmptyBucketSummary());
	    }
	    this.bucketedStream = Observable.defer(() -> {
	        return inputEventStream
	                .observe()
	                // 利用RxJava進行窗口滑動
	                // bucketSizeInMs默認值是1000,表示1s表示一個窗口
	                .window(bucketSizeInMs, TimeUnit.MILLISECONDS) 
	                .flatMap(reduceBucketToSummary) 
	                .startWith(emptyEventCountsToStart);
	    });
	
	}

}

用戶在使用 Hystrix 的時候一般都要配兩個值(當然,大多數情況下默認值即可):timeInMillisecondsnumBuckets,前者代表滑動窗口的長度(時間間隔),後者代表滑動窗口中桶的個數,那麼每個桶對應的窗口長度就是 bucketSizeInMs = timeInMilliseconds / numBuckets(記爲一個單元窗口週期)。BucketedCounterStream每隔一個單元窗口週期(bucketSizeInMs)就把這段時間內的所有調用事件聚合到一個桶內(使用的便是reduceBucketToSummary函數完成)。


共享的事件流HystrixEventStream

BucketedCounterStream 核心代碼在構造函數裏,裏面最核心的邏輯就是如何將一個一個的事件按一段時間(RxJava的window方法)聚合成一個桶(flatMap方法)。我們可以看到 bucketedStream 是經事件源 inputEventStream 變換而成的,事件源的類型爲 HystrixEventStream<Event>,關於此事件流你可參考上篇文章,電梯直達:[享學Netflix] 二十二、Netflix Hystrix事件源與事件流:HystrixEvent和HystrixEventStream

此處說明一點:發送事件/數據的順序性、write()數據時的線程安全性均由RxJava以及Hystrix使用ThreadLocal提供保證的,使用者放心使用即可


事件聚合 -> 桶(Event -> Bucket)

事件流通過HystrixEventStream源源不斷的傳遞過來,某一時段甚至某一時刻進來的事件會有N個,但是這個時候需要把它聚合成Bucket桶,以方便後續的統計(因爲桶纔是窗口的最小單位),這部分核心邏輯在這:

this.reduceBucketToSummary = eventBucket -> eventBucket.reduce(getEmptyBucketSummary(), appendRawEventToBucket);

this.bucketedStream = Observable.defer(() -> { // defer 的意思是 lazy 創建
    return inputEventStream
            .observe()
            .window(bucketSizeInMs, TimeUnit.MILLISECONDS) // 按單元窗口長度來將某個時間段內的調用事件聚集起來
            .flatMap(reduceBucketToSummary)                // 將每個單元窗口內聚集起來的事件集合聚合成桶
            .startWith(emptyEventCountsToStart);           // 爲了保證窗口的完整性,開始的時候先產生一串空的桶
});

這裏最爲核心是 window 操作符:它可以按單元窗口長度來將某個時間段內的調用事件聚集起來。
此時數據流裏每個對象都是一個集合:Observable<Event>,所以需要將其聚集成桶類型以將其扁平化。Hystrix 通過 RxJava 的 reduce 操作符進行“歸納”操作,將一串事件歸納成一個桶:

this.reduceBucketToSummary = eventBucket -> eventBucket.reduce(getEmptyBucketSummary(), appendRawEventToBucket);

這個reduce函數的初始值爲:getEmptyBucketSummary()也就是空桶,它是抽象方法由子類實現。appendRawEventToBucket負責具體的reduce聚合邏輯,這是由構造函數傳進來的函數:Bucket + Event -> Bucket,表示:對於每個 Event,都將其聚合到 Bucket 中,並返回聚合後的 Bucket。

說明:不同的實現對歸約appendRawEventToBucket函數的實現是不同的,比如熔斷器依賴的HealthCountsStream它就是以long[]作爲每個桶的。

Tips:window(timespan, unit)操作符屬於計算型操作符,默認會在 Schedulers.computation() 調度器下執行(CPU 密集型,關於Schedulers前文有過詳細解釋),其底層本質是線程數爲 CPU 核數的線程池。RxJava 會確保其線程安全。


其它方法

BucketedCounterStream:

	// 抽象方法:訪問權限是Default哦~~~
    abstract Bucket getEmptyBucketSummary(); // 空桶
    abstract Output getEmptyOutputValue(); // 空的輸出值。作爲BehaviorSubject的默認值

	// 注意:這個泛型是output,並不是輸入哦。返回的是處理後的輸出流,所以一般是桶
	// 它是public的
    public abstract Observable<Output> observe();

	// 取消subscription的訂閱(它的設值方法見下)
    public void unsubscribe() {
        Subscription s = subscription.get();
        if (s != null) {
            s.unsubscribe();
            subscription.compareAndSet(s, null);
        }
    }
    // 若subscription還爲null(還未開始),那就讓counterSubject去監聽着
    // observe().subscribe(counterSubject);
    public void startCachingStreamValuesIfUnstarted() { ... }

	// 這是一個同步調用。以檢索最後一個計算的桶,而不需要等待任何發射
	// 該方法會在很多地方被調用
    public Output getLatest() {
        startCachingStreamValuesIfUnstarted();
        if (counterSubject.hasValue()) {
            return counterSubject.getValue();
        } else {
            return getEmptyOutputValue();
        }
    }

總結

BucketedCounterStream提供的能力可描述爲:桶計數器,它負責把一段時間窗口內的事件歸約到一個桶裏,並且對外提供Stream的訪問方式,讓外部可以訂閱、處理。

如果說HystrixEventStream是點對點的建立了通道,那麼BucketedCounterStream就是定期的去通道了收集數據,統計裝到桶裏,以便後續使用。至於桶是什麼結構?裝了哪些數據,以及具體的歸約、計算邏輯均在子類實現,下面文章將繼續分享這方面的內容,敬請關注。

分隔線

聲明

原創不易,碼字不易,多謝你的點贊、收藏、關注。把本文分享到你的朋友圈是被允許的,但拒絕抄襲。你也可【左邊掃碼/或加wx:fsx641385712】邀請你加入我的 Java高工、架構師 系列羣大家庭學習和交流。
往期精選

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章