Hystrix数据收集及滑动窗口机制解析

1,412 阅读4分钟

依赖如下

<dependency>
    <groupId>com.netflix.hystrix</groupId>
    <artifactId>hystrix-core</artifactId>
    <version>1.5.18</version>
</dependency>

什么是Hystrix

2018.11发布了最后一个版本,目前处理维护阶段,不再升级版本

  • 用途:

    • 停止级联故障。fallback和优雅的降级,Fail fast和快速恢复
    • 实时监控和配置实时变更
    • 资源隔离,部分不可用不会导致整体系统不可用
  • 场景:商品列表接口中,需要获取红包、价格、标签等数据。这时候可以给这个一个线程池。 如果线程池打满,也不会影响当前服务的非商品列表接口

  • 使用的框架:hystrix主要使用Rxjava,上手可参考:www.jianshu.com/p/5e93c9101…

滑动窗口执行案例

执行步骤:

  1. 假设滑动窗口总为1s,4个桶bucket,那每个bucket的空间为0.25s,限流总数为10(记录为max=10)
  2. 然后如下各个桶的访问记录为
    2.1.. 桶t1(在0~0.25s之间),访问了4次。这时候访问总次数记录为total=4(此时total<=max)
    2.2.. 桶t2(在0.25~0.50s之间),访问了4次。这时候访问总次数记录为total=8(此时total<=max)
    2.3.. 桶t3(在0.50~0.75s之间),访问了1次。这时候访问总次数记录为total=9(此时total<=max) 2.4.. 桶t4(在0.75~1.00s之间),尝试访问了2次。由于max=10,当第total=11即第二次访问时,会拒绝访问,因为当前窗口总数上线已满(此时total=10) 2.5.. 桶t5(在1.00~1.25s之间),尝试访问了3次。由于此时t1已经出了窗口,t1访问的4次会被释放出来, 这时候总访问字数total=10 - 4 + 3 = 9 < max = 10,所以可正常访问,不会被拒绝

滑动窗口的基本流程如下:
滑动窗口.png

滑动窗口执行实现例子

滑动窗口实现例子如下

/**
 * 自定义滑动时间窗口demo - Hystrix也是类似采用这种。
 * - 实现runnable方法:用于控制滑动动作,重置桶的值以及总量值
 *
 * @author lidishan
 */
public class MyDefinedSlideWinDemoLimiter implements RateLimiter, Runnable {
    /** 每秒最多允许5个请求,这是默认值,你可以通过构造方法指定 **/
    private static final int DEFAULT_ALLOWED_VISIT_PER_SECOND = 5;
    /** 最大访问每秒 **/
    private long maxVisitPerSecond;
    /** 默认把1s分为十个桶,这是默认值 **/
    private static final int DEFAULT_BUCKET = 10;
    private int bucket;
    /** 每个桶对应当前的请求数 **/
    private static AtomicInteger[] countPerBucket = null;

    /** 总请求数 **/
    private AtomicInteger count;
    private volatile int index;

    /** 构造器 **/
    public MyDefinedSlideWinDemoLimiter() {
        this(DEFAULT_BUCKET, DEFAULT_ALLOWED_VISIT_PER_SECOND);
    }
    public MyDefinedSlideWinDemoLimiter(int bucket, long maxVisitPerSecond) {
        this.bucket = bucket;
        this.maxVisitPerSecond = maxVisitPerSecond;
        countPerBucket = new AtomicInteger[bucket];
        for (int i = 0; i < bucket; i++) {
            countPerBucket[i] = new AtomicInteger();
        }
        count = new AtomicInteger(0);
    }
    /**
     * 是否超过限制:当前QPS总数是否超过了最大值(默认每秒5个)
     * 注意:这里应该是>=。因为其实如果桶内访问数量已经等于5了,就应该限制住外面的再进来
     */
    @Override
    public boolean isOverLimit() {
        return currentQps() >= maxVisitPerSecond;
    }
    @Override
    public int currentQps() {
        return count.get();
    }
    /**
     * 访问一次,次数+1(只要请求进来了就+1),并且告知是否加载
     * 请注意:放在指定的桶
     */
    @Override
    public boolean visit() {
        countPerBucket[index].incrementAndGet();
        count.incrementAndGet();
        return isOverLimit();
    }
    @Override
    public void run() {
        System.out.println("~~~~~~~~~~~~~~~~~~~~~~~~~~窗口向后滑动一下~~~~~~~~~~~~~~~~~~~~~~~~~~");
        // 桶内的指针向前滑动一下:表示后面的visit请求应该打到下一个桶内
        index = (index + 1) % bucket;
        // 初始化新桶。并且拿出旧值(其实就是把当前这个桶的值释放出来,然后看下这个桶之前是否有访问过,有的话就对count总数减去,然后告诉可以进行访问)
        int val = countPerBucket[index].getAndSet(0);
        // 这个步骤一定不要变了:因为废弃了一个桶,所以总值要减去~
        if (val == 0) {
            // 这个桶等于0,说明这个时刻没有流量进来
            System.out.println("~~~~~~~~~~~~~~~~~~~~~~~~~~窗口没能释放出流量,继续保持限流~~~~~~~~~~~~~~~~~~~~~~~~~~");
        } else {
            count.addAndGet(-val);
            System.out.println("~~~~~~~~~~~~~~~~~~~~~~~~~~窗口释放出了[" + val + "]个访问名额,你可以访问了~~~~~~~~~~~~~~~~~~~~~~~~~~");
        }
    }


    public static void main(String[] args) throws Exception {
        MyDefinedSlideWinDemoLimiter rateLimiter = new MyDefinedSlideWinDemoLimiter();
        // 使用一个线程定时滑动这个窗口:100ms滑动一次(一般保持个桶的跨度保持一致)
        ScheduledExecutorService scheduledExecutorService = Executors.newSingleThreadScheduledExecutor();
        scheduledExecutorService.scheduleAtFixedRate(rateLimiter, 100, 100, TimeUnit.MILLISECONDS);

        // 此处使用单线程访问,你可以改造成多线程版本
        while (true) {
            String currThreadName = Thread.currentThread().getName();
            boolean overLimit = rateLimiter.isOverLimit();
            if (overLimit) {
                System.out.printf("线程[%s]===被限流了===,因为访问次数已经超过阈值[%s]\n%n", currThreadName, rateLimiter.currentQps());
            } else {
                rateLimiter.visit();
                System.out.printf("线程[%s]访问成功,当前访问总数[%s]\n%n", currThreadName, rateLimiter.currentQps());
            }
            Thread.sleep(10);
        }
    }
}
public interface RateLimiter {
    // 是否要限流
    boolean isOverLimit();
    // 当前QPS总数值(也就是窗口期内的访问总量)
    int currentQps();
    // touch一下;增加一次访问量
    boolean visit();
}

Hystrix滑动窗口实现

Hystrix通过滑动窗口来对数据进行统计,一个滑动窗口包含10个桶。每个桶宽度是1秒,负责当前时间段1秒的 成功、失败、超时、拒绝 次数的统计 Hystrix滑动统计.jpg 即每个桶都记录了四个指标:成功量、失败量、超时量、拒绝量,当前滑动时间窗口总数=成功量+失败量+超时量+拒绝量 (所有桶bucket)
其Hystrix滑动时间窗口核心实现为(看看就好,rxjava不用会)

  • 提供了HealthCountsStream提供实时健康检查数据,其中里面有个对象HealthCounts记录滑动窗口期间请求数(总数、失败数、失败百分比)
  • 有滑动时间窗口,肯定也有持续累积窗口BucketedCumulativeCounterStream
public abstract class BucketedRollingCounterStream<Event extends HystrixEvent, Bucket, Output> extends BucketedCounterStream<Event, Bucket, Output> {
    private Observable<Output> sourceStream;
    private final AtomicBoolean isSourceCurrentlySubscribed = new AtomicBoolean(false);
    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 = window -> window.scan(getEmptyOutputValue(), reduceBucket).skip(numBuckets);
        this.sourceStream = bucketedStream      // 数据流,每个对象代表单元窗口产生的桶   stream broken up into buckets
                .window(numBuckets, 1)          // 按照滑动窗口桶的个数进行桶的聚集   emit overlapping windows of buckets
                .flatMap(reduceWindowToSummary) // 将一系列的桶聚集成最后的数据对象    convert a window of bucket-summaries into a single summary
                .doOnSubscribe(() -> isSourceCurrentlySubscribed.set(true))
                .doOnUnsubscribe(() -> isSourceCurrentlySubscribed.set(false))
                .share()                        // 共享。不同的订阅者看到的数据是一致的  multiple subscribers should get same data
                .onBackpressureDrop();          // 被压流量控制,当消费者消费速度过慢时就丢弃数据,不进行积压  if there are slow consumers, data should not buffer
    }
    @Override
    public Observable<Output> observe() {
        return sourceStream;
    }
    /* package-private */ boolean isSourceCurrentlySubscribed() {
        return isSourceCurrentlySubscribed.get();
    }
}
public static class HealthCounts {
    private final long totalCount;// 总数
    private final long errorCount;// 错误总数
    private final int errorPercentage;// 错误百分比
}

下面再展示一个Hystrix降级的流程图:

img.png