【你好Hystrix】二:Hystrix指标收集之滑动收集原理源码解析-BucketedRollingCounterStream

前言

整合Archaius可以说是Hystrix最基础的模块也是和我们开发过程中息息相关的模块 毕竟我们80%的时间可能都是在和配置打交道。而今天我们介绍的指标收集 可能我们平时开发中用不到 但是我认为这个是理解Hystrix的敲门砖 。这一块理解了 你有一种恍然大悟的感觉。因为我们说的断路器 判断断合标准其实就是来自这些指标。可以说这是Hystrix的核心的核心。在看这篇文章之前 希望你已对RxJava有所了解。Hystrix1.5之前使用的是环形数组来解决滑动窗口统计问题。可以参考HystrixRollingNumber目前已经废弃了。我们本文讲解的是1.5版本之后基于RxJava的滑动窗口实现!

Hystrix事件流

Hystrix 在一次请求过程中会不停的发送事件,例如命令开始执行时会发送HystrixCommandExecutionStarted事件、命令执行完会发送HystrixCommandCompletion。而我们把这些事件写入不同的流中就变成了不同的事件流,说白了就是发送事件的时候使用Observable作为发射器

事件流定义

public interface HystrixEventStream<E extends HystrixEvent> {
    Observable<E> observe();
}

这是事件流的父接口 只有一个方法 就是获取当前事件流的发射器 Observable

事件流种类

在这里插入图片描述
所有的事件流都实现了HystrixEventStream。我们从这些类的名字大致的都能猜出是什么意思。我就不一一的介绍了。我们以HystrixCommandStartStream为例来看一下命令开始执行事件流的实现。

HystrixCommandStartStream

public class HystrixCommandStartStream implements HystrixEventStream<HystrixCommandExecutionStarted> {
    private final HystrixCommandKey commandKey;
    private final Subject<HystrixCommandExecutionStarted, HystrixCommandExecutionStarted> writeOnlySubject;
    /**
     * 发射器
     */
    private final Observable<HystrixCommandExecutionStarted> readOnlyStream;
    /**
     * 缓存每个commandKey对应同一个HystrixCommandStartStream实例
     */
    private static final ConcurrentMap<String, HystrixCommandStartStream> streams = new ConcurrentHashMap<String, HystrixCommandStartStream>();
    /**
     * 获取HystrixCommandKey 对应的HystrixCommandStartStream实例
     */
    public static HystrixCommandStartStream getInstance(HystrixCommandKey commandKey) {
        HystrixCommandStartStream initialStream = streams.get(commandKey.name());
        if (initialStream != null) {
            return initialStream;
        } else {
            synchronized (HystrixCommandStartStream.class) {
                HystrixCommandStartStream existingStream = streams.get(commandKey.name());
                if (existingStream == null) {
                    HystrixCommandStartStream newStream = new HystrixCommandStartStream(commandKey);
                    streams.putIfAbsent(commandKey.name(), newStream);
                    return newStream;
                } else {
                    return existingStream;
                }
            }
        }
    }
    HystrixCommandStartStream(final HystrixCommandKey commandKey) {
        this.commandKey = commandKey;
        /**
         * 在并发情况下使用SerializedSubject来保证
          */
        this.writeOnlySubject = new SerializedSubject<HystrixCommandExecutionStarted, HystrixCommandExecutionStarted>(PublishSubject.<HystrixCommandExecutionStarted>create());
        /**
         *share方法相当于调用publish().refCount()
         * publish() 会将SerializedSubject转换成connectableObservable 和publishSubject功能一样
         * 唯一的区别是connectableObservable需要调用connect方法才能发射数据
         *
         * refCount()
         * 将connectableObservable转换成一个正常的Observable。和正常的Observable 有一个区别
         * 多个订阅者会共享一份数据  当所有的订阅者取消订阅时 不会再发射数据详情可以参考https://www.jianshu.com/p/575ce5b98389
         */
        this.readOnlyStream = writeOnlySubject.share();
    }
    public static void reset() {
        streams.clear();
    }
    /**
     * 提供写方法将该event写到发射器里面去,这样订阅者就能读啦
     * 该方法的唯一调用处是:HystrixThreadEventStream
     * @param event
     */
    public void write(HystrixCommandExecutionStarted event) {
        writeOnlySubject.onNext(event);
    }
    /**
     * 这里可以获取只读流发射器 获取到这个readOnlyStream就能拿到
     * writeOnlySubject写入的数据 即为HystrixCommandExecutionStarted
     * @return
     */
    @Override
    public Observable<HystrixCommandExecutionStarted> observe() {
        return readOnlyStream;
    }
    @Override
    public String toString() {
        return "HystrixCommandStartStream(" + commandKey.name() + ")";
    }
}

上面代码注释已经很详细了。如果对RxJava的操作符不是那么熟悉可以参考RxJava2 实战知识梳理(12) - 实战讲解 publish & replay & share & refCount & autoConnect
对操作符的了解可以参考

可以看到指标收集这一块基本上就是对RxJava的应用。

  • 关于SerializedSubject
    Subject: 它同时充当了Observer和Observable的角色。因为它是一个Observer,它可以订阅一个或多个Observable;又因为它是一个Observable,它可以转发它收到(Observe)的数据,也可以发射新的数据.
    如果你把 Subject 当作一个 Subscriber 使用,注意不要从多个线程中调用它的onNext方法(包括其它的on系列方法),这可能导致同时(非顺序)调用,这会违反Observable协议,给Subject的结果增加了不确定性。
    要避免此类问题,你可以将 Subject 转换为一个 SerializedSubject
  • 关于share()方法
    相当于调用publish().refCount()
    publish() 会将SerializedSubject转换成connectableObservablepublishSubject功能一样,唯一的区别是connectableObservable需要调用connect方法才能发射数据.
    refCount()connectableObservable转换成一个正常的Observable。和正常的Observable 有一个区别:多个订阅者会共享一份数据 当所有的订阅者取消订阅时 不会再发射数据详情可以参考
  • write(HystrixCommandExecutionStarted event) 该方法就是想流中写入一个事件,供调用者调用。
  • ConcurrentMap<String, HystrixCommandStartStream> 每一个HystrixCommandKey对应一个HystrixCommandStartStream

上面对HystrixCommandStartStream进行介绍,其他的流实现是一毛一样的。那Hystrix定义了这些事件流 在哪里使用呢?作用是什么呢?

分桶计数流BucketedCounterStream

上面我们讲了事件流,这是Hystrix中最基础的数据流它们是数据的来源。而BucketedCounterStream属于更高级别的数据流,是在事件流的基础上进行聚合统计。所以Hystrix的滑动窗口就是通过该流来实现。

实现类

在这里插入图片描述
BucketedCounterStream下面有两个分支:BucketedRollingCounterStreamBucketedCumulativeCounterStream,前者用于滚动统计后者用于累计统计。我们这篇文章主要对 BucketedRollingCounterStream这个分支做一个介绍。

BucketedCounterStream

public abstract class BucketedCounterStream<Event extends HystrixEvent, Bucket, Output> {
    protected final int numBuckets;
    protected final Observable<Bucket> bucketedStream;
    protected final AtomicReference<Subscription> subscription = new AtomicReference<Subscription>(null);
    private final Func1<Observable<Event>, Observable<Bucket>> reduceBucketToSummary;
    private final BehaviorSubject<Output> counterSubject = BehaviorSubject.create(getEmptyOutputValue());
    protected BucketedCounterStream(final HystrixEventStream<Event> inputEventStream, final int numBuckets, final int bucketSizeInMs,
                                    final Func2<Bucket, Event, Bucket> appendRawEventToBucket) {
        this.numBuckets = numBuckets;
        this.reduceBucketToSummary = new Func1<Observable<Event>, Observable<Bucket>>() {
            @Override
            public Observable<Bucket> call(Observable<Event> eventBucket) {
                /**
                 * reduce 操作符 getEmptyBucketSummary()为初始值 appendRawEventToBucket为操作函数
                 * 最终结果会对eventBucket中的值经过appendRawEventToBucket的计算 返回最终的值
                 * eventBucket是一个事件流 转换成一个具体的Observable<Bucket> 对象。
                 * appendRawEventToBucket(转换规则)这个函数由具体的子类提供
                 */
                return eventBucket.reduce(getEmptyBucketSummary(), appendRawEventToBucket);
            }
        };
        //下面是构造一个空数组 作为第一个窗口
        final List<Bucket> emptyEventCountsToStart = new ArrayList<Bucket>();
        for (int i = 0; i < numBuckets; i++) {
            emptyEventCountsToStart.add(getEmptyBucketSummary());
        }
        /**
         * defer操作符 只有当Subscriber 来订阅的时候从才会创建一个新的对象
         * 也就是说 每次订阅都会得到一个刚创建的新的Observable对象。可以确保
         * Observable对象里面的数据是最新的 具体可参考https://mcxiaoke.gitbooks.io/rxdocs/content/operators/Defer.html
         */
        this.bucketedStream = Observable.defer(new Func0<Observable<Bucket>>() {
            @Override
            public Observable<Bucket> call() {
                return inputEventStream
                        //这个observe拿到的是子类的返回的一个Observable
                        .observe()
                        //将其存储在计数器窗口中 以便我们可以按照时间块发送给一下个操作符
                        .window(bucketSizeInMs, TimeUnit.MILLISECONDS)
                        //这里实际上是将 事件流 转换成 存储桶流 即 Observable<Event> ===> Observable<Bucket>
                        .flatMap(reduceBucketToSummary)
                        //start it with empty arrays to make consumer logic as generic as possible (windows are always full)
                        .startWith(emptyEventCountsToStart);
            }
        });
    }
    abstract Bucket getEmptyBucketSummary();
    abstract Output getEmptyOutputValue();
    public abstract Observable<Output> observe();
}
  • reduceBucketToSummary:这个很重要 主要是对原始的时间流转换成 数据桶发射器 也就是一个聚合的动作。
  • numBuckets:桶的数量 这个是子类来设计 最终是可配置的
  • bucketedStream:最终转换的数据桶发射器
  • bucketSizeInMs:桶的大小 也就是收集一个桶要多长时间,例如1s中一个桶 那么这1s接受的事件流都会汇集到一个桶中

这个类最核心的就是构造方法中的一个事件流发射器转桶发射器的一个实现,当然是用了大量的RXJava的运算符。
假如桶大小是1s 最终实现的效果就是 例如源事件发射器发射
第1秒 发射了 1,2,3
第2秒 发射了4,5
第3秒发射6,7,8,9
那么经过该聚合器 将事件流转成 [1,2,3],[4,5],[6,7,8,9]

模拟事件发射器转成桶发射器

上面我们用文字描述了这一过程 但是还是很抽象,下面我们使用代码来帮助理解。我们前面说了这个类的作用就是把Observable<Event>转成Observable<Bucket> 我们模拟的时候Event使用Long来代理 而Bucket使用 ArrayList来代替

//这里完全模拟Hystrix的代码 首先发送一个窗口的空桶
 ArrayList<Long> startWith = new ArrayList<Long>(){{
      for (int i = 0; i < 10; i++) {
        add(0L);
      }
    }};
//下面就是模拟把Long转成 ArrayList的过程startWith(0L)可以忽略 因为interval操作符开始不会
//发送数据 等到100毫秒之后才发送第一个数据 这样会导致我们的window第一个窗口少一个数据 
//所以在这里使用startWith补齐
  Observable<ArrayList<Long>> arrayListObservable
        = Observable
             .defer(() ->Observable.interval(100, TimeUnit.MILLISECONDS).startWith(0L)
             .window(1000, TimeUnit.MILLISECONDS)
             .flatMap(longObservable ->
                          longObservable.reduce(new ArrayList<Long>(), (l1, l2) -> {
                            l1.add(l2);
                            return l1;
                          }))
             .startWith(startWith));
arrayListObservable.subscribe(System.out::println);
TimeUnit.SECONDS.sleep(Integer.MAX_VALUE);

输出结果

[0, 0, 0, 0, 0, 0, 0, 0, 0, 0]
[0, 0, 1, 2, 3, 4, 5, 6, 7, 8]
[9, 10, 11, 12, 13, 14, 15, 16, 17, 18]
[19, 20, 21, 22, 23, 24, 25, 26, 27, 28]
[29, 30, 31, 32, 33, 34, 35, 36, 37, 38]
[39, 40, 41, 42, 43, 44, 45, 46, 47, 48]
[49, 50, 51, 52, 53, 54, 55, 56, 57, 58]
[59, 60, 61, 62, 63, 64, 65, 66, 67, 68]
[69, 70, 71, 72, 73, 74, 75, 76, 77, 78]

上面的代码是完全模拟BucketedCounterStream的操作流程。通过上面的代码把interval发送的一个个的Long对象转换成了 ArrayList。也就是把原始时间转换成了桶。

BucketedRollingCounterStream

BucketedRollingCounterStreamBucketedCounterStream的子类,所以它所处理的数据流是基于Obserable<Bucket>的。从名字我们也可以看出它是基于滑动统计的。所以我们可以认为它是一个滑动窗口的实现。
所有的实现都在构造方法中:

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);
        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);
            }
        };
        //bucketedStream 已在父类的构造方法初始化 这里sourceStream通过bucketedStream转换得到
        this.sourceStream = bucketedStream
                //skip = 1 按照步长为1在数据流中滑动 不断聚集对象
                .window(numBuckets, 1)
                //将Observable<Bucket> 转成 Observable<OutPut>
                .flatMap(reduceWindowToSummary)
                //每次订阅都会调用该方法
                .doOnSubscribe(new Action0() {
                    @Override
                    public void call() {
                        isSourceCurrentlySubscribed.set(true);
                    }
                })
                //每次取消订阅都会调用该方法
                .doOnUnsubscribe(new Action0() {
                    @Override
                    public void call() {
                        isSourceCurrentlySubscribed.set(false);
                    }
                })
                //share操作符 订阅者每次订阅都不会从头开始收数据
                .share()
                //该操作符是为了解决Hot Observable的背压问题  如果消费者消费过慢  将observable发送的事件抛弃掉 不进行积压
                //直到Subscribe再次调用request(n)
                .onBackpressureDrop();
    }

上面的逻辑主要就是将Observable<Bucket>转成Observable<OutPut>的过程。而OutPut是我们需要最终转换的对象,所以这个是一个泛型 需要子类来指定。这里给我们留了很大的一个空间 我们可以继承该类就能实现我们特定的滑动计算的方法。

值得注意的是上面也是一坨Rxjava的操作符 但是我们特别注意的只有两个操作符windowscan.

  • window操作符 可以说是实现滑动算法的核心中的核心。我们在父类也看到它了。这里对其做一个简单的说明。

//父类使用window(long timespan, TimeUnit unit)  参数timespan  表示将多长时间的Observable聚合在一起 
@Test
 public void window1() throws InterruptedException {
   Observable.interval(1000 , TimeUnit.MILLISECONDS)
       .window(2 , TimeUnit.SECONDS)
       .subscribe(windowObser ->{
         System.out.println("============");
         windowObser.subscribe(System.out::println);
       });
   TimeUnit.SECONDS.sleep(Integer.MAX_VALUE);
 }
/*
输出结果 
============
0
===========
1
2
==========
3
4
*/

//子类使用的是window(int count, int skip)  参数count意思为将多少个Observable聚合在一起 参数skip表示跳过多少个
@Test
 public void window2() throws InterruptedException {
   Observable.interval(1000 , TimeUnit.MILLISECONDS)
       .window(2 , 2)
       .subscribe(windowObs ->{
         System.out.println("===========");
         windowObs.subscribe(System.out::println);
       });
   TimeUnit.SECONDS.sleep(Integer.MAX_VALUE);
 }
/*
输出结果 
============
0
1
===========
2
3
==========
4
5
*/

通过上面的例子我们知道window最常用的用法就是 根据时间聚合和根据个数聚合。

  • scan:操作符 这里主要注意它和reduce操作符的区别,scan 会把每一步的计算结果都发送给观察者,而reduce则只会把最终的计算结果发送给观察者
 @Test
   Observable.range(1,10)
       .scan((integer, integer2) -> integer+integer2)
       .subscribe(System.out::println);
 /*
 输出结果:
 1
 3
 6
 10
 ...
 */
   Observable.range(1,10)
       .reduce((integer, integer2) -> integer+integer2)
       .subscribe(System.out::println);
 /*输出结果
 55
*/

模拟桶发射器转换成 目标对象发射器

 ArrayList<Long> startWith = new ArrayList<Long>() {{
      for (int i = 0; i < 5; i++) {
        add(0L);
      }
    }};
    Observable<ArrayList<Long>> arrayListObservable
        = Observable
             .defer(() ->Observable.interval(100, TimeUnit.MILLISECONDS).startWith(0L)
             .window(1000, TimeUnit.MILLISECONDS)
             .flatMap(longObservable ->
                          longObservable.reduce(new ArrayList<Long>(), (l1, l2) -> {
                            l1.add(l2);
                            return l1;
                          }))
             .startWith(startWith));

    //这部分的代码是关键 上面的代码我们在上面的例子中已经展示
    Observable<Long> longObservable = arrayListObservable.window(10, 1)
                    .flatMap(temp -> {
                      Observable<Long> scanObser = temp.scan(0L,
                          (aLong, longs) -> aLong + longs.stream()
                          .collect(Collectors.summingLong(value -> value))).skip(10);
                      return scanObser;
                    })
                    .share()
                    .onBackpressureDrop();
    longObservable.subscribe(System.out::println);

打印结果

3916
4851
5850
6850
7850
....

上面的代码其实我们是对Obserable<Bucket>转换成了Obserable<Output> 只不过我们的Output是一个Long 简单的对10个桶的数据做了累加。这样我们就完成了 由Obserable<Long> -> Obserable<ArrayList> -> Obserable<Long>的转换 当然泛型里面的类型在实际操作中可以是任意类型。

RollingCommandEventCounterStream

滑动的命令事件统计流,前面将的两个类都是抽象类没法直接使用。这个类是在上面两个类的功能上进行的扩展,这个类就比较简单了就是对父类需要的参数做初始化。

 private static final ConcurrentMap<String, RollingCommandEventCounterStream> streams = new ConcurrentHashMap<String, RollingCommandEventCounterStream>();
 private static final int NUM_EVENT_TYPES = HystrixEventType.values().length;
 public static RollingCommandEventCounterStream getInstance(HystrixCommandKey commandKey, HystrixCommandProperties properties) {
     //获取固定窗口大小 默认10000ms
     final int counterMetricWindow = properties.metricsRollingStatisticalWindowInMilliseconds().get();
     //获取桶个数 默认10
     final int numCounterBuckets = properties.metricsRollingStatisticalWindowBuckets().get();
     //计算桶大小 1000 一个桶
     final int counterBucketSizeInMs = counterMetricWindow / numCounterBuckets;
     return getInstance(commandKey, numCounterBuckets, counterBucketSizeInMs);
 }

 public static RollingCommandEventCounterStream getInstance(HystrixCommandKey commandKey, int numBuckets, int bucketSizeInMs) {
     RollingCommandEventCounterStream initialStream = streams.get(commandKey.name());
     if (initialStream != null) {
         return initialStream;
     } else {
         synchronized (RollingCommandEventCounterStream.class) {
             RollingCommandEventCounterStream existingStream = streams.get(commandKey.name());
             if (existingStream == null) {
                 RollingCommandEventCounterStream newStream = new RollingCommandEventCounterStream(commandKey, numBuckets, bucketSizeInMs, HystrixCommandMetrics.appendEventToBucket, HystrixCommandMetrics.bucketAggregator);
                 streams.putIfAbsent(commandKey.name(), newStream);
                 return newStream;
             } else {
                 return existingStream;
             }
         }
     }
 }
 private RollingCommandEventCounterStream(HystrixCommandKey commandKey, int numCounterBuckets, int counterBucketSizeInMs,
                                          Func2<long[], HystrixCommandCompletion, long[]> reduceCommandCompletion,
                                          Func2<long[], long[], long[]> reduceBucket) {
     /**
      * HystrixCommandCompletionStream.getInstance(commandKey) 这里会将一个 命令完成流传过去
      */
     super(HystrixCommandCompletionStream.getInstance(commandKey), numCounterBuckets, counterBucketSizeInMs, reduceCommandCompletion, reduceBucket);
 }

该类的构造方法是私有的 不允许创建,提供了getInstance()方法获取一个事件统计流

  • metricsRollingStatisticalWindowInMilliseconds:固定窗口大小 默认10s
  • metricsRollingStatisticalWindowBuckets:窗口内的桶数量 默认10s 从这两个配置可以看出 1s一个桶 也就是桶的大小是1s(1s内的事件聚合成一个桶)
  • streams:保存了所有CommandKey对应的事件统计流对象
  • 需要注意的是这里的Output是一个long[]Bucket也是一个long[]
  • RollingCommandEventCounterStream的构造方法中发现它获取了一个HystrixCommandCompletionStream作为原始流 给父类处理,所以它处理的是命令完成的事件流。所以只要我们源源不断的往HystrixCommandCompletionStream中写入数据 就能顺利的统计成我们需要的数据

总结

我们看到了 收集的事情都是BucketedCounterStream和他的子类BucketedRollingCounterStream做的 如果需要对一个事件收集统计只需要继承这两个中的一个实现相关的初始化方法即可。HealthCountsStream这个很重要的流 原理是一毛一样的。

在这里插入图片描述

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值