今天一定要搞清楚限流、熔断和降级

3,013 阅读9分钟

20220226110835_bbaa7.jpg

限流、熔断和降级都是系统容错的重要设计模式

限流

是对系统的被请求频率以及内部的部分功能的执行频率加以限制,防止因突发的流量激增,导致整个系统不可用。限流主要是防御保护手段,从流量源头开始控制流量规避问题。

熔断

是在流量过大时(或下游服务出现问题时),可以自动断开与下游服务的交互,并可以通过自我诊断下游系统的错误是否已经修正,或上游流量是否减少至正常水平,来恢复自我恢复。熔断更像是自动化补救手段,可能发生在服务无法支撑大量请求或服务发生其他故障时,对请求进行限制处理,同时还可尝试性的进行恢复。

服务降级

主要是针对非核心业务功能,而核心业务如果流程超过预估的峰值,就需要进行限流。降级一般考虑的是分布式系统的整体性,从源头上切断流量的来源。降级更像是预估手段,在预计流量峰值前提下,提前通过配置功能降低服务体验,或暂停次要功能,保证系统主要流程功能平稳响应。

限流和熔断也可以看作是一种服务降级的手段。以下对每个概念进行详细讨论。

什么是限流?

限流是流量限速(Rate Limit)的简称,是指只允许指定的事件进入系统,超过的部分将被拒绝服务、排队或等待、降级等处理。对于server服务而言,限流为了保证一部分的请求流量可以得到正常的响应,总好过全部的请求都不能得到响应,甚至导致系统雪崩。

限流常见的算法:

1.计数器算法(固定窗口)

在一段时间间隔内(时间窗)处理请求的最大数量固定,超过部分不做处理。

计数器算法Java简单实现

// 计速器 限速
@Slf4j
public class CounterLimiter {

    // 起始时间
    private static long lastTime = System.currentTimeMillis();
    // 时间区间的时间间隔 ms
    private static long interval = 1000;
    // 每秒限制数量
    private static long capacity = 2;
    //累加器
    private static AtomicLong counter = new AtomicLong();

    //返回正数,表示没有被限流
    //返回-1 ,表示被限流
    // 计数判断, 是否超出限制
    private static long tryPass(long requestId, int turn) {
        long nowTime = System.currentTimeMillis();
        //当前时间,在时间区间之内
        if (nowTime < lastTime + interval) {
            //增加计数
            long count = counter.incrementAndGet();

            if (count <= capacity) {
                return count;
            } else {
                return -1;  // 容量耗尽
            }
        } else {
            //在时间区间之外
            synchronized (CounterLimiter.class) {
                log.info("新时间区到了,requestId{}, turn {}..", requestId, turn);
                // 再一次判断,防止重复初始化
                // dc 双重校验
                if (nowTime > lastTime + interval) {
                    //reset counter
                    counter.set(1);
                    lastTime = nowTime;
                }else {
                    //补充上遗漏的场景, 计数器算法看似简单,实际上陷阱重重
                    //增加计数
                    long count = counter.incrementAndGet();

                    if (count <= capacity) {
                        return count;
                    } else {
                        return -1;  // 容量耗尽
                    }
                }
            }
            return 1;
        }
    }

    //线程池,用于多线程模拟测试
    private ExecutorService pool = Executors.newFixedThreadPool(10);

    @Test
    public void testLimit() {

        // 被限制的次数
        AtomicInteger limited = new AtomicInteger(0);
        // 线程数
        final int threads = 2;
        // 每条线程的执行轮数
        final int turns = 20;
        // 同步器
        CountDownLatch countDownLatch = new CountDownLatch(threads);
        long start = System.currentTimeMillis();
        for (int i = 0; i < threads; i++) {
            pool.submit(() ->
            {
                try {

                    for (int j = 0; j < turns; j++) {

                        long taskId = Thread.currentThread().getId();
                        long index = tryPass(taskId, j);
                        System.out.println("index = " + index);

                        if (index <= 0) {
                            // 被限制的次数累积
                            limited.getAndIncrement();

                        }
                        Thread.sleep(200);
                    }

                } catch (Exception e) {
                    e.printStackTrace();
                }
                //等待所有线程结束
                countDownLatch.countDown();

            });
        }
        try {
            countDownLatch.await();
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        float time = (System.currentTimeMillis() - start) / 1000F;
        //输出统计结果

        log.info("限制的次数为:" + limited.get() +
                ",通过的次数为:" + (threads * turns - limited.get()));
        log.info("限制的比例为:" + (float) limited.get() / (float) (threads * turns));
        log.info("运行的时长为:" + time);
    }

}

计数器算法的缺点

在两个间隔之间,如果有密集的请求。则会导致单位时间内的实际请求超过阈值。

例如,限制每分钟100个请求,在第一个1分钟时间窗里临界点的时候(比如 0:59时刻),瞬间来了 100 个请求,这时能够正常处理请求,然后在1:01时刻的时候,第二个1分钟时间窗里又来了100个请求,这时也能够正常处理这些请求。但在 3s 内,一共处理 200 个请求,但是时间跨度小于1分钟,可能会造成后端过载。

计数器限流.jpg

所以在使用计数器限流策略时,用户通过在时间窗口的连接处突发大量请求,可以瞬间超过速率限制,从而瞬间压垮应用,这就是计数器限流算法的临界问题。

如何处理临界问题,可以通过滑动窗口、漏铜、令牌桶三种算法解决。

2.滑动窗口算法

基本概念:

滑动窗口和时间桶

  1. 滑动窗口 可以这么来理解滑动窗口:一位乘客坐在正在行驶的列车的靠窗座位上,列车行驶的公路两侧种着一排挺拔的白杨树,随着列车的前进,路边的白杨树迅速从窗口滑过,我们用每棵树来代表一个请求,用列车的行驶代表时间的流逝,那么,列车上的这个窗口就是一个典型的滑动窗口,这个乘客能通过窗口看到的白杨树的数量,就是滑动窗口要统计的数据。
  2. 时间桶 时间桶是统计滑动窗口数据时的最小单位。同样类比列车窗口,在列车速度非常快时,如果每掠过一棵树就统计一次窗口内树的数据,显然开销非常大,如果乘客将窗口分成N份,前进行时列车每掠过窗口的N分之一就统计一次数据,开销就大大地减小了。简单来说,时间桶也就是滑动窗口的N分之一。

滑动窗口算法 弥补了计数器算法的不足。

滑动窗口算法把间隔时间划分成更小的粒度,当更小粒度的时间间隔过去后,把过去的间隔请求数减掉,再补充一个空的时间间隔。

如下图所示,把1分钟划分为10个更小的时间间隔,每6s为一个间隔。每个间隔是1个时间桶

滑动窗口.jpg

1 一个时间窗口为1分钟,滑动窗口分成10个时间桶,每个时间桶6秒。

2 每过6秒,滑动窗口向右移动1个时间桶。

3 每个时间桶都有独立的计数器。

4 如果时间窗口内所有时间桶的计数器之和超过了限流阀值,则触发限流操作。

如下图所示,滑动窗口算法比计数器算法控制得更精细。

滑动窗口2.jpg

滑动窗口算法伪代码

/**
     * 单位时间划分的小周期(单位时间是1分钟,10s一个小格子窗口,一共6个格子)
     */
    private int SUB_CYCLE = 10;

    /**
     * 每分钟限流请求数
     */
    private int thresholdPerMin = 100;

    /**
     * 计数器, k-为当前窗口的开始时间值秒,value为当前窗口的计数
     */
    private final TreeMap<Long, Integer> counters = new TreeMap<>();

   /**
     * 滑动窗口时间算法实现
     */
    boolean slidingWindowsTryAcquire() {
        long currentWindowTime = LocalDateTime.now().toEpochSecond(ZoneOffset.UTC) / SUB_CYCLE * SUB_CYCLE; //获取当前时间在哪个小周期窗口
        int currentWindowNum = countCurrentWindow(currentWindowTime); //当前窗口总请求数

        //超过阀值限流
        if (currentWindowNum >= thresholdPerMin) {
            return false;
        }

        //计数器+1
        counters.get(currentWindowTime)++;
        return true;
    }

   /**
    * 统计当前窗口的请求数
    */
    private int countCurrentWindow(long currentWindowTime) {
        //计算窗口开始位置
        long startTime = currentWindowTime - SUB_CYCLE* (60s/SUB_CYCLE-1);
        int count = 0;

        //遍历存储的计数器
        Iterator<Map.Entry<Long, Integer>> iterator = counters.entrySet().iterator();
        while (iterator.hasNext()) {
            Map.Entry<Long, Integer> entry = iterator.next();
            // 删除无效过期的子窗口计数器
            if (entry.getKey() < startTime) {
                iterator.remove();
            } else {
                //累加当前窗口的所有计数器之和
                count =count + entry.getValue();
            }
        }
        return count;
    }

缺点

滑动窗口设置得越精细,限流的效果越好,但滑动窗口的时间间隔(小格子)多了,存储的空间也会增加。

3.漏铜桶算法

请求直接进入到漏桶里,漏桶以一定的速度对请求进行放行,当请求数量递增,漏桶内的总请求量大于桶容量就会直接溢出,请求被拒绝。

漏桶.jpg

漏桶限流规则 1)请求以任意速率流入漏桶。 2)漏桶的容量是固定的,放行速率也是固定的。 3)漏桶容量是不变的,如果处理速度太慢,桶内请求数量会超出桶的容量,则后面流入的请求会溢出,表示请求被拒绝。

public class LeakBucketLimiter {
    // 计算的起始时间
    private static long lastOutTime = System.currentTimeMillis();
    // 时间区间的时间间隔 ms
    private static long interval = 1000;
    // 流出速率 每秒 2 次
    private static int leakRate = 2;
    // 桶的容量
    private static int capacity = 20;
    //剩余的容量
    private static AtomicInteger waterInBucket = new AtomicInteger(0);
    //返回值说明:
    // false 没有被限制到
    // true 被限流
    public static synchronized boolean isLimit(long taskId, int turn) {
        // 如果是空桶,就当前时间作为漏出的时间
        if (waterInBucket.get() == 0) {
            lastOutTime = System.currentTimeMillis();
            waterInBucket.addAndGet(1);
            return false;
        }
        //场景三:当前请求和上次请求,在同一个时间区间
        long nowTime = System.currentTimeMillis();
        //当前时间,在时间区间之内
        //漏水以时间区间为计算维度,同一个区间,没有必要重复去计算漏水
        if (nowTime < lastOutTime + interval) {
            // 尝试加水,并且水还未满 ,放行
            if ((waterInBucket.get()) < capacity) {
                waterInBucket.addAndGet(1);
                return false;
            } else {
                // 水满,拒绝加水, 限流
                return true;
            }
        }
        //场景二: 桶里边有水
        //当前时间,在时间区间之外
        // 计算漏水,以时间的区间为维度的
        int waterLeaked = ((int) ((System.currentTimeMillis() - lastOutTime) / 1000)) * leakRate;
        // 计算剩余水量
        int waterLeft = waterInBucket.get() - waterLeaked;
        //校正数据
        waterLeft = Math.max(0, waterLeft);
        waterInBucket.set(waterLeft);
        // 重新更新leakTimeStamp
        lastOutTime = System.currentTimeMillis();
        // 尝试添加请求,并且容量还未满 ,放行
        if ((waterInBucket.get()) < capacity) {
            waterInBucket.addAndGet(1);
            return false;
        } else {
            // 漏桶满了,限流
            return true;
        }

    }

    //线程池,用于多线程模拟测试
    private ExecutorService pool = Executors.newFixedThreadPool(10);

    @Test
    public void testLimit() {

        // 被限制的次数
        AtomicInteger limited = new AtomicInteger(0);
        // 线程数
        final int threads = 2;
        // 每条线程的执行轮数
        final int turns = 20;
        // 线程同步器
        CountDownLatch countDownLatch = new CountDownLatch(threads);
        long start = System.currentTimeMillis();
        for (int i = 0; i < threads; i++) {
            pool.submit(() ->
            {
                try {
                    for (int j = 0; j < turns; j++) {
                        long taskId = Thread.currentThread().getId();
                        boolean intercepted = isLimit(taskId, j);
                        if (intercepted) {
                            // 被限制的次数累积
                            limited.getAndIncrement();
                        }
                        Thread.sleep(200);
                    }
                } catch (Exception e) {
                    e.printStackTrace();
                }
                //等待所有线程结束
                countDownLatch.countDown();
            });
        }
        try {
            countDownLatch.await();
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        float time = (System.currentTimeMillis() - start) / 1000F;
        //输出统计结果
        log.info("限制的次数为:" + limited.get() +
                ",通过的次数为:" + (threads * turns - limited.get()));
        log.info("限制的比例为:" + (float) limited.get() / (float) (threads * turns));
        log.info("运行的时长为:" + time);
    }
}

缺点

漏桶的出水速度是固定的,也就是请求放行速度是固定的,故漏桶不能有效应对突发流量, 但是能起到平滑突发流量(整流)的作用。

4.令牌桶算法

令牌桶算法以一个设定的速率产生令牌并放入令牌桶,每次用户请求都得申请令牌,如果令牌不足,则拒绝请求。

令牌桶.jpg

令牌的数量与时间和发放速率强相关,流逝的时间越长,往桶里加入令牌的越多,如果令牌发放速度比申请速度快,则令牌会放入令牌桶,直到占满整个令牌桶,令牌桶满了,多的令牌就直接丢弃。

令牌桶限流大致的规则如下

1)进水口按照某个速度向桶中放入令牌。

2)令牌桶的容量是固定的,但是放行的速度不是固定的,只要桶中还有剩余令牌,一旦请求 过来就能申请成功,然后放行。

3)如果令牌的发放速度慢于请求的到来速度,则桶内就无牌可领,请求就会被拒绝。

令牌桶算法的Java简单实现

public class TokenBucketLimiter {
    // 上一次令牌发放时间
    public long lastTime = 0;
    // 桶的容量
    public int capacity = 2;
    // 令牌生成速度 /s
    public int rate = 2;
    // 当前令牌数量
    public AtomicInteger tokens = new AtomicInteger(0);

    //返回值说明:
    // false 没有被限制到
    // true 被限流
    public synchronized boolean isLimited(long taskId, int applyCount) {
        long now = System.currentTimeMillis();
        //时间间隔,单位为 ms
        long gap = now - lastTime;

        //场景三:当前请求和上次请求,在同一个时间区间
        //当前时间,在时间区间之内
        //以时间区间为计算维度,同一个区间,没有必要重复去计算令牌数量
        if (lastTime != 0 && gap < 1000/*interval*/) {
            if (tokens.get() < applyCount) {
                // 若拿不到令牌,则拒绝
                // log.info("被限流了.." + taskId + ", applyCount: " + applyCount);
                return true;
            } else {
                // 还有令牌,领取令牌
                tokens.getAndAdd(-applyCount);

                // log.info("剩余令牌.." + tokens);
                return false;
            }
        }
        System.out.println("时区之外 gap = " + gap);

        if (lastTime == 0) {
            gap = 1000 /*interval*/;
        }
        //计算时间段内的令牌数
        int reverse_permits = (int) (gap * rate / 1000);
        int all_permits = tokens.get() + reverse_permits;
        // 当前令牌数
        tokens.set(Math.min(capacity, all_permits));
        log.info("tokens {} capacity {} gap {} ", tokens, capacity, gap);
        lastTime = now;

        if (tokens.get() < applyCount) {
            // 若拿不到令牌,则拒绝
            // log.info("被限流了.." + taskId + ", applyCount: " + applyCount);
            return true;
        } else {
            // 还有令牌,领取令牌
            tokens.getAndAdd(-applyCount);

            // log.info("剩余令牌.." + tokens);
            return false;
        }

    }

    //线程池,用于多线程模拟测试
    private ExecutorService pool = Executors.newFixedThreadPool(10);

    @Test
    public void testLimit() {

        // 被限制的次数
        AtomicInteger limited = new AtomicInteger(0);
        // 线程数
        final int threads = 2;
        // 每条线程的执行轮数
        final int turns = 20;

        // 同步器
        CountDownLatch countDownLatch = new CountDownLatch(threads);
        long start = System.currentTimeMillis();
        for (int i = 0; i < threads; i++) {
            pool.submit(() ->
            {
                try {

                    for (int j = 0; j < turns; j++) {

                        long taskId = Thread.currentThread().getId();
                        boolean intercepted = isLimited(taskId, 1);
                        if (intercepted) {
                            // 被限制的次数累积
                            limited.getAndIncrement();
                        }

                        Thread.sleep(200);
                    }

                } catch (Exception e) {
                    e.printStackTrace();
                }
                //等待所有线程结束
                countDownLatch.countDown();

            });
        }
        try {
            countDownLatch.await();
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        float time = (System.currentTimeMillis() - start) / 1000F;
        //输出统计结果

        log.info("限制的次数为:" + limited.get() +
                ",通过的次数为:" + (threads * turns - limited.get()));
        log.info("限制的比例为:" + (float) limited.get() / (float) (threads * turns));
        log.info("运行的时长为:" + time);
    }

}

什么是熔断

1. 熔断

1.1 熔断来源

词典解释:保险丝烧断 保险丝也被称为熔断器。

在90年代很多电闸都有保险丝,现在都是空气开关,当电压出现短路问题时,保险丝过热,因为是铅丝熔点低于铜铁,所以短路造成线路温度升高,导致铅丝融化,线路断开。此刻电路主动断开,我们的电器就会受到到保护。否则,可能造成火灾等严重后果。

保险丝就是一个自我保护装置,保护整个电路。此概念被引入多个行业,我们要了解的是分布式系统中的熔断。

1.2 分布式系统中的熔断

在分布式系统中,不同的服务互相依赖,某些服务需要依赖下游服务,下游服务不管是内部系统还是第三方服务,如果出现问题,我们还是盲目地持续地去请求,即使失败了多次,还是不断的去请求,去等待,就可能造成系统更严重的问题。

1、等待增加了整个链路的请求时间。

2、下游系统有问题,不断的请求导致下游系统有持续的访问压力难以恢复。

3、等待时间过长造成上游服务线程阻塞,CPU被拉满等问题。

4、可能导致服务雪崩。

1.3 熔断的作用

熔断模式可以防止应用程序不断地尝试请求下游可能超时和失败的服务,可以不必等待下游服务的修复而达到应用程序可执行的状态。

1.4为什么要使用熔断器

熔断器模式最重要的是能让应用程序自我诊断下游系统的错误是否已经修正,如果没有,不放量去请求,如果请求成功了,慢慢的增加请求,再次尝试调用。

熔断有三种状态:

1.Closed:关闭状态

所有请求都正常访问。

2.Open:打开状态 所有请求都会被降级

在关闭状态下,熔断器会对请求情况计数,当一定时间内失败请求百分比达到阈值,则触发熔断,断路器会完全打开。一般默认失败比例的阈值是50%,请求次数最少不低于20次。

3.Half Open:半开状态 允许部分请求通过

open状态不是永久的,在熔断器开启状态打开后会进入休眠时间(一般默认是5S)。随后断路器会自动进入半开状态。此时会释放部分请求通过,若这些请求都是健康的,则会完全关闭断路器,否则继续保持打开,再次进行休眠计时。

熔断器.jpg

熔断器开源组件

1.Hystrix

Hystrix(豪猪--->身上很多刺--->保护自己),宣⾔“defend your app”,是由Netflflix开源的⼀个延迟和容错库,⽤于隔离访问远程系统、服务或者第三⽅库,防⽌级联失败,从而提升系统的可⽤性与容错性。Hystrix主要通过以下几点实现延迟和容错。

包裹请求:使⽤HystrixCommand包裹对依赖的调⽤逻辑。 ⾃动投递微服务⽅法(@HystrixCommand 添加Hystrix控制) ——调⽤简历微服务跳闸机制:当某服务的错误率超过⼀定的阈值时,Hystrix可以跳闸,停⽌请求该服务⼀段时间。

资源隔离:Hystrix为每个依赖都维护了⼀个⼩型的线程池(舱壁模式)(或者信号量)。如果该线程池已满, 发往该依赖的请求就被⽴即拒绝,⽽不是排队等待,从⽽加速失败判定。

监控:Hystrix可以近乎实时地监控运⾏指标和配置的变化,例如成功、失败、超时、以及被拒绝 的请求等。

回退机制:当请求失败、超时、被拒绝,或当断路器打开时,执⾏回退逻辑。回退逻辑由开发⼈员 ⾃⾏提供,例如返回⼀个缺省值。

⾃我修复:断路器打开⼀段时间后,会⾃动进⼊“半开”状态。

2.Resilience4j

Resilience4j是一款轻量级,易于使用的容错库,其灵感来自于Netflix Hystrix,但是专为Java 8和函数式编程而设计。轻量级,因为库只使用了Vavr,它没有任何其他外部依赖下。相比之下,Netflix HystrixArchaius具有编译依赖性,Archaius具有更多的外部库依赖性,例如GuavaApache Commons Configuration

Resilience4j与Hystrix主要区别:

1.Hystrix调用必须被封装到HystrixCommand里,Resilience4j以装饰器的方式提供对函数式接口、lambda表达式等的嵌套装饰,因此你可以用简洁的方式组合多种高可用机制。

2.Hystrix采用滑动窗口方式统计频次,Resilience4j采用环形缓冲区

3.半开启状态下,Hystrix进使用一次执行判断是否进行状态转换,Resilience4j则可配置执行次数与阈值,通过配置参数执行判断是否进行状态转换,这种方式提高了熔断机制的稳定性。

4.Hystrix采用基于线程池和信号量的隔离,而resilience4j只提供基于信号量的隔离

3.Sentinel

随着微服务的流行,服务和服务之间的稳定性变得越来越重要。Sentinel 以流量为切入点,从流量控制、流量路由、熔断降级、系统自适应过载保护、热点流量防护等多个维度保护服务的稳定性。

Sentinel 具有以下特征:

  • 丰富的应用场景:Sentinel 承接了阿里巴巴近 10 年的双十一大促流量的核心场景,例如秒杀(即突发流量控制在系统容量可以承受的范围)、消息削峰填谷、集群流量控制、实时熔断下游不可用应用等。
  • 完备的实时监控:Sentinel 同时提供实时的监控功能。您可以在控制台中看到接入应用的单台机器秒级数据,甚至 500 台以下规模的集群的汇总运行情况。
  • 广泛的开源生态:Sentinel 提供开箱即用的与其它开源框架/库的整合模块,例如与 Spring Cloud、Apache Dubbo、gRPC、Quarkus 的整合。您只需要引入相应的依赖并进行简单的配置即可快速地接入 Sentinel。同时 Sentinel 提供 Java/Go/C++ 等多语言的原生实现。
  • 完善的 SPI 扩展机制:Sentinel 提供简单易用、完善的 SPI 扩展接口。您可以通过实现扩展接口来快速地定制逻辑。例如定制规则管理、适配动态数据源等。

Hystrix健康统计滑动窗口的执行流程

1)HystrixCommand命令器的执行结果(失败、成功)会以事件的形式通过RxJava事件流弹射 出去,形成执行完成事件流。 2)桶计数流以事件流作为来源,将事件流中的事件按照固定时间长度(桶时间间隔)划分成 滚动窗口,并对时间桶滚动窗口内的事件按照类型进行累积,完成之后将桶数据弹射出去,形成桶 计数流。 3)桶滑动统计流以桶计数流作为来源,按照步长为1、长度为设定的桶数(配置的滑动窗口 桶数)的规则划分滑动窗口,并对滑动窗口内的所有的桶数据按照各事件类型进行汇总,汇总成最终 的窗口健康数据并弹射出去,形成最终的桶滑动统计流,作为Hystrix熔断器进行状态转换的数据支撑。

模拟实现

public class WindowDemo
{

    /**
     * window 创建操作符 创建滑动窗口
     * 演示 window 创建操作符   创建滑动窗口
     */
    @Test
    public void simpleWindowObserverDemo()
    {

        List<Integer> srcList = Arrays.asList(10, 11, 20, 21, 30, 31);

        Observable.from(srcList)
                .window(3)
                .flatMap(o -> o.toList())
                .subscribe(list -> log.info(list.toString()));
    }

    /**
     * window 创建操作符 创建滑动窗口
     * 演示 window 创建操作符   创建滑动窗口
     */
    @Test
    public void windowObserverDemo()
    {

        List<Integer> srcList = Arrays.asList(10, 11, 20, 21, 30, 31);

        Observable.from(srcList)
                .window(3, 1)
                .flatMap(o -> o.toList())
                .subscribe(list -> log.info(list.toString()));
    }

    /**
     * window 创建操作符 创建滑动窗口
     * 演示 window 滑动窗口和归并
     */
    @Test
    public void windowMergeDemo()
    {

        List<Integer> srcList = Arrays.asList(10, 11, 20, 21, 30, 31);
        Observable.merge(
                Observable.from(srcList)
                        .window(3, 1))
                .subscribe(integer -> log.info(integer.toString()));
    }

    /**
     * window 创建操作符 创建时间窗口
     * 演示  window 创建操作符   创建时间窗口
     */
    @Test
    public void timeWindowObserverDemo() throws InterruptedException
    {

        Observable eventStream = Observable
                .interval(100, TimeUnit.MILLISECONDS);
        eventStream.window(300, TimeUnit.MILLISECONDS)
                .flatMap(o -> ((Observable<Integer>) o).toList())
                .subscribe(list -> log.info(list.toString()));

        Thread.sleep(Integer.MAX_VALUE);
    }

    /**
     * 演示  hystrix 的  健康统计 metric
     */
    @Test
    public void hystrixTimewindowDemo() throws InterruptedException
    {
        //创建Random类对象
        Random random = new Random();

        //模拟 Hystrix event 事件流,每 100ms 发送一个 0或1随机值, 随机值为 0 代表失败,机值为 1 代表成功
        Observable eventStream = Observable
                .interval(100, TimeUnit.MILLISECONDS)
                .map(i -> random.nextInt(2));

        /**
         *完成桶内0值计数的聚合函数
         */
        Func1 reduceBucketToSummary = new Func1<Observable<Integer>, Observable<Long>>()
        {
            @Override
            public Observable<Long> call(Observable<Integer> eventBucket)
            {
                Observable<List<Integer>> olist = eventBucket.toList();
                Observable<Long> countValue = olist.map(list ->
                {
                    long count = list.stream().filter(i -> i == 0).count();
                    log.info("{} '0 count:{}", list.toString(), count);
                    return count;

                });
                return countValue;
            }
        };

        /**
         *桶计数流
         */
        Observable<Long> bucketedCounterStream = eventStream
                .window(300, TimeUnit.MILLISECONDS)
                .flatMap(reduceBucketToSummary);  // 将时间桶进行聚合,统计为事件值为0 的个数

        /**
         * 滑动窗口聚合函数
         */
        Func1 reduceWindowToSummary = new Func1<Observable<Long>, Observable<Long>>()
        {
            @Override
            public Observable<Long> call(Observable<Long> eventBucket)
            {
                return eventBucket.reduce(new Func2<Long, Long, Long>()
                {
                    @Override
                    public Long call(Long bucket1, Long bucket2)
                    {
                        /**
                         * 对窗口内的桶,进行的累加
                         */
                        return bucket1 + bucket2;
                    }
                });

            }
        };
        /**
         * 桶滑动统计流
         */
        Observable bucketedRollingCounterStream = bucketedCounterStream
                .window(3, 1)
                .flatMap(reduceWindowToSummary);// 将滑动窗口进行聚合
        bucketedRollingCounterStream.subscribe(sum -> log.info("滑动窗口的和:{}", sum));
        Thread.sleep(Integer.MAX_VALUE);
    }
}

什么是降级?

为什么要降级?

降级最主要解决的是资源不足和访问量增加的矛盾,在有限的资源情况下,可以应对高并发大量请求。那么在有限的资源情况下,想要达到以上效果就需要对一些服务功能进行一些限制,放弃一些功能,保证整个系统能够平稳运行。

降级的方式有哪些?

1、将强一致性变成最终一致性,不需要强一致性的功能,可以通过消息队列进行削峰填谷,变为最终一致性达到应用程序想要的效果。

2、停止访问一些次要功能,释放出更多资源。比如双十一不让退货等。

3、简化功能流程,把一些复杂的流程简化。提高访问效率。

自动降级的条件

  • 调用失败次数达到阈值
  • 请求响应超时时间达到阈值
  • 请求下游服务发生故障通过状态码
  • 流量达到阈值触发服务降级

怎样实现降级?

降级开源组件:sentinel和Hystrix

手动降级:可采用系统配置开关来控制

总结一下,限流、熔断和降级经常组合出现,是在服务不同条件下进行的服务保护机制,同时限流可能触发降级,熔断又是服务降级的一种方式,所以相辅相成,互相关联。还需要多思考。