SpringCloud Alibaba系列——14Sentinel简介及基本应用

导读:本篇文章讲解 SpringCloud Alibaba系列——14Sentinel简介及基本应用,希望对大家有帮助,欢迎收藏,转发!站点地址:www.bmabk.com

学习目标

  1. Sentinel是什么?它的作用

  2. 你了解哪些限流算法

  3. Sentinel的限流规则有哪些

  4. Sentinel的限流策略

  5. Sentinel的限流模

  6. 熔断与限流的区别

  7. Sentinel 降级熔断策略有哪些

第1章 限流

1.1 概述与作用

随着微服务的流行,服务和服务之间的稳定性变得越来越重要。缓存、降级和限流是保护微服务系统运行稳定性的三大利器。

缓存:提升系统访问速度和增大系统能处理的容量 ​ 降级:当服务出问题或者影响到核心流程的性能则需要暂时屏蔽掉 ​ 限流:解决服务雪崩,级联服务发生阻塞时,及时熔断,防止请求堆积消耗占用系统的线程、IO等资源,造成其他级联服务所在服务器的崩溃

这里我们说一下限流,限流的目的应当是通过对并发访问/请求进行限速或者一个时间窗口内的的请求进行限速来保护系统,一旦达到限制速率就可以拒绝服务、等待、降级。

1.2 限流算法

限流算法常用的几种实现方式有如下四种:计数器、滑动窗口、漏桶和令牌桶

1.2.1 计数器

固定窗口

1、思想:计数器算法是使用计数器在周期内累加访问次数,当达到设定的限流值时,触发限流策略。下一个周期开始时,进行清零,重新计数。此算法在单机还是分布式环境下实现都非常简单,使用redis的incr原子自增性和线程安全即可轻松实现。SpringCloud Alibaba系列——14Sentinel简介及基本应用

2、问题:这个算法通常用于QPS限流和统计总访问量,对于秒级以上的时间周期来说,会存在一个非常严重的问题,那就是临界问题,如下图:SpringCloud Alibaba系列——14Sentinel简介及基本应用

假设1min内服务器的负载能力为100,因此一个周期的访问量限制在100,然而在第一个周期的最后1秒和下一个周期的开始1秒时间段内,分别涌入100的访问量,虽然没有超过每个周期的限制量,但是整体上2秒内已达到200的访问量,已远远超过服务器的负载能力,由此可见,计数器算法方式限流对于周期比较长的限流,存在很大的弊端。  

3、代码实现

定义异常

public class BlockException extends Exception {
}

定义接口

public interface RateLimit {
    boolean canPass() throws BlockException;
}

 定义实现类

/**
 * CounterRateLimit
 * 计算器限流
 *
 */
public class CounterRateLimit implements RateLimit , Runnable {
    /**
     * 阈值
     */
    private Integer limitCount;
    /**
     * 当前通过请求数
     */
    private AtomicInteger passCount;
    /**
     * 统计时间间隔
     */
    private long period;
    private TimeUnit timeUnit;
    private ScheduledExecutorService scheduledExecutorService;

    public CounterRateLimit(Integer limitCount) {
        this(limitCount, 1000, TimeUnit.MILLISECONDS);
    }

    public CounterRateLimit(Integer limitCount, long period, TimeUnit timeUnit) {
        this.limitCount = limitCount;
        this.period = period;
        this.timeUnit = timeUnit;
        passCount = new AtomicInteger(1);
        this.startResetTask();
    }

    @Override
    public boolean canPass() throws BlockException {
        if (passCount.incrementAndGet() > limitCount) {
            throw new BlockException();
        }
        return true;
    }

    private void startResetTask() {
        scheduledExecutorService = Executors.newSingleThreadScheduledExecutor();
        scheduledExecutorService.scheduleAtFixedRate(this, 0, period, timeUnit);
    }

    @Override
    public void run() {
        passCount.set(1);
    }

    public static void main(String[] args) throws BlockException, InterruptedException {
        CounterRateLimit counterRateLimit = new CounterRateLimit(10, 2000, TimeUnit.MILLISECONDS);
        for (int i = 1; i < 10000000; i++) {
            Thread.sleep(200);
            if (counterRateLimit.canPass()) {
                System.out.println("我是第" + i + "个请求");
            }
        }
    }
}

1.2.2 滑动窗口

 Sentinel底层实现方式

1、思想:固定窗口存在临界值问题,要解决这种临界值问题,显然只用一个窗口是解决不了问题的。假设我们仍然设定1分钟内允许通过的请求是100个,但是在这里我们需要把1分钟的时间分成多格,假设分成5格(格数越多,流量过渡越平滑),每格窗口的时间大小是12秒,每过12秒,就将窗口向前移动一格。为了便于理解,可以看下图  

SpringCloud Alibaba系列——14Sentinel简介及基本应用

图中将窗口划为5份,每个小窗口中的数字表示在这个窗口中请求数,所以通过观察上图,可知在当前时间快(12秒)允许通过的请求数应该是10而不是100(只要超过10就会被限流),因为我们最终统计请求数时是需要把当前窗口的值进行累加,进而得到当前请求数来判断是不是需要进行限流。

此算法可以很好的解决固定窗口算法的临界问题。

2、代码实现

/**
 * SlidingWindowRateLimit
 * 滑动窗口限流
 */
@Slf4j
public class SlidingWindowRateLimit implements RateLimit, Runnable {

    /**
     * 阈值
     */
    private Integer limitCount;

    /**
     * 当前通过的请求数
     */
    private AtomicInteger passCount;

    /**
     * 窗口数
     */
    private Integer windowSize;

    /**
     * 每个窗口时间间隔大小
     */
    private long windowPeriod;
    private TimeUnit timeUnit;


    private Window[] windows;
    private volatile Integer windowIndex = 0;

    private Lock lock = new ReentrantLock();
    public SlidingWindowRateLimit(Integer limitCount) {
        // 默认统计qps, 窗口大小5
        this(limitCount, 5, 200, TimeUnit.MILLISECONDS);
    }

    /**
     * 统计总时间 = windowSize * windowPeriod
     */
    public SlidingWindowRateLimit(Integer limitCount, Integer windowSize, Integer windowPeriod, TimeUnit timeUnit) {
        this.limitCount = limitCount;
        this.windowSize = windowSize;
        this.windowPeriod = windowPeriod;
        this.timeUnit = timeUnit;
        this.passCount = new AtomicInteger(0);
        this.initWindows(windowSize);
        this.startResetTask();
    }

    @Override
    public boolean canPass() throws BlockException {
        lock.lock();
        if (passCount.get() > limitCount) {
            throw new BlockException();
        }
        windows[windowIndex].passCount.incrementAndGet();
        passCount.incrementAndGet();
        lock.unlock();
        return true;
    }

    private void initWindows(Integer windowSize) {
        windows = new Window[windowSize];
        for (int i = 0; i < windowSize; i++) {
            windows[i] = new Window();
        }
    }

    private ScheduledExecutorService scheduledExecutorService;
    private void startResetTask() {
        scheduledExecutorService = Executors.newSingleThreadScheduledExecutor();
        scheduledExecutorService.scheduleAtFixedRate(this, windowPeriod, windowPeriod, timeUnit);
    }

    @Override
    public void run() {
        // 获取当前窗口索引
        Integer curIndex = (windowIndex + 1) % windowSize;
//        log.info("curIndex = {}", curIndex);
        // 重置当前窗口索引通过数量,并获取上一次通过数量
        Integer count = windows[curIndex].passCount.getAndSet(0);
        windowIndex = curIndex;
        // 总通过数量 减去 当前窗口上次通过数量
        passCount.addAndGet(-count);
//        log.info("curOldCount = {}, passCount = {}, windows = {}", count, passCount, windows);
    }

    @Data
    class Window {
        private AtomicInteger passCount;
        public Window() {
            this.passCount = new AtomicInteger(0);
        }
    }

    public static void main(String[] args) throws BlockException, InterruptedException {
        SlidingWindowRateLimit slidingWindowRateLimit = new SlidingWindowRateLimit(100);
        for(int i=1;i<10000000;i++){
//            Thread.sleep(10);
            if(slidingWindowRateLimit.canPass()){
                System.out.println("我是第"+i+"个请求");
            }
        }

    }
}

1.2.3 漏桶算法

1、思想:漏桶算法是首先想象有一个木桶,桶的容量是固定的。当有请求到来时先放到木桶中,处理请求的worker以固定的速度从木桶中取出请求进行相应。如果木桶已经满了,直接返回请求频率超限的错误码或者页面

SpringCloud Alibaba系列——14Sentinel简介及基本应用

 2、适用场景:漏桶算法是流量最均匀的限流实现方式,一般用于流量“整形”。例如保护数据库的限流,先把对数据库的访问加入到木桶中,worker再以db能够承受的qps从木桶中取出请求,去访问数据库。

3、问题:木桶流入请求的速率是不固定的,但是流出的速率是恒定的。这样的话能保护系统资源不被打满,但是面对突发流量时会有大量请求失败,不适合电商抢购和微博出现热点事件等场景的限流。

4、代码实现

/**
 * LeakyBucketRateLimit
 * 漏桶算法
 */
@Slf4j
public class LeakyBucketRateLimit implements RateLimit, Runnable {

    /**
     * 出口限制qps
     */
    private Integer limitSecond;
    /**
     * 漏桶队列
     */
    private BlockingQueue<Thread> leakyBucket;

    private ScheduledExecutorService scheduledExecutorService;

    public LeakyBucketRateLimit(Integer bucketSize, Integer limitSecond) {
        this.limitSecond = limitSecond;
        this.leakyBucket = new LinkedBlockingDeque<>(bucketSize);

        scheduledExecutorService = Executors.newSingleThreadScheduledExecutor();
        long interval = (1000 * 1000 * 1000) / limitSecond;//周期秒。TimeUnit.NANOSECONDS(毫微秒,1秒=1000*1000*1000毫微秒
        scheduledExecutorService.scheduleAtFixedRate(this, 0, interval, TimeUnit.NANOSECONDS);
    }

    @Override
    public boolean canPass() throws BlockException {
        if (leakyBucket.remainingCapacity() == 0) {
            throw new BlockException();
        }
        leakyBucket.offer(Thread.currentThread());
        LockSupport.park();
        return true;
    }

    @Override
    public void run() {
        Thread thread = leakyBucket.poll();
        if (Objects.nonNull(thread)) {
            LockSupport.unpark(thread);
        }
    }
    public static void main(String[] args) throws BlockException, InterruptedException {
        LeakyBucketRateLimit leakyBucketRateLimit = new LeakyBucketRateLimit(100,10);
        for(int i=1;i<10000000;i++){
            if(leakyBucketRateLimit.canPass()){
                System.out.println("我是第"+i+"个请求");
            }
        }
    }
}

1.2.4 令牌桶算法

 1、思想:令牌桶是反向的”漏桶”,它是以恒定的速度往木桶里加入令牌,木桶满了则不再加入令牌。服务收到请求时尝试从木桶中取出一个令牌,如果能够得到令牌则继续执行后续的业务逻辑。如果没有得到令牌,直接返回访问频率超限的错误码或页面等,不继续执行后续的业务逻辑。

SpringCloud Alibaba系列——14Sentinel简介及基本应用

 2、适用场景:适合电商抢购或者微博出现热点事件这种场景,因为在限流的同时可以应对一定的突发流量。如果采用漏桶那样的均匀速度处理请求的算法,在发生热点时间的时候,会造成大量的用户无法访问,对用户体验的损害比较大。

3、代码实现

/**
 * TokenBucketRateLimit
 * 令牌桶
 */
@Slf4j
public class TokenBucketRateLimit implements RateLimit, Runnable {

    /**
     * token 生成 速率 (每秒)
     */
    private Integer tokenLimitSecond;

    /**
     * 令牌桶队列
     */
    private BlockingQueue<String /* token */> tokenBucket;

    private static final String TOKEN = "__token__";

    private ScheduledExecutorService scheduledExecutorService;

    public TokenBucketRateLimit(Integer bucketSize, Integer tokenLimitSecond) {
        this.tokenLimitSecond = tokenLimitSecond;
        this.tokenBucket = new LinkedBlockingDeque<>(bucketSize);

        scheduledExecutorService = Executors.newSingleThreadScheduledExecutor();
        long interval = (1000 * 1000 * 1000) / tokenLimitSecond;
        scheduledExecutorService.scheduleAtFixedRate(this, 0, interval, TimeUnit.NANOSECONDS);
    }

    @Override
    public boolean canPass() throws BlockException {
        String token = tokenBucket.poll();
        if (StringUtils.isEmpty(token)) {
            throw new BlockException();
        }
        return true;
    }
    @Override
    public void run() {
        if (tokenBucket.remainingCapacity() == 0) {
            return;
        }
        tokenBucket.offer(TOKEN);
    }
}

4、guava的实现  

  • 引包
    <dependency>
        <groupId>com.google.guava</groupId>
        <artifactId>guava</artifactId>
        <version>22.0</version>
    </dependency>
  • 代码举例
    public class RateLimiterDemo {
        private static final SimpleDateFormat FORMATTER = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
        //线程数
        private static final int THREAD_COUNT = 25;
    
        public static void main(String[] args) {
            RateLimiterDemo rateLimiterDemo = new RateLimiterDemo();
            rateLimiterDemo.testRateLimiter1();
        }
    
        public void testRateLimiter1() {
            //令牌桶算法
            RateLimiter rateLimiter=RateLimiter.create(1); //TPS=1
            Thread[] ts = new Thread[THREAD_COUNT];
            for (int i = 0; i < THREAD_COUNT; i++) {
                ts[i] = new Thread(new RateLimiterThread(rateLimiter), "RateLimiterThread-" + i);
            }
            for (int i = 0; i < THREAD_COUNT; i++) {
                ts[i].start();
            }
            for(;;) {
            }
        }
    
        public class RateLimiterThread implements Runnable {
            private RateLimiter rateLimiter;
            public RateLimiterThread(RateLimiter rateLimiter) {
                this.rateLimiter = rateLimiter;
            }
            @Override
            public void run() {
                rateLimiter.acquire(1);
                System.out.println(Thread.currentThread().getName() + "获取到了令牌,时间 = " + FORMATTER.format(new Date()));
            }
        }
    }

1.2.5 算法比较

算法 确定参数 空间复杂度 时间复杂度 限制突发流量 平滑限流 分布式环境下实现难度
计数器 计数周期T、周期内最大访问数N 低O(1)(记录周期内访问次数及周期开始时间) 低O(1)
滑动窗口 计数周期T、周期内最大访问数N 高O(N)(记录每个小周期中的访问数量) 中O(N) 相对实现。滑动窗口的格子划分的越多,那么滑动窗口的滚动就越平滑
漏桶 漏桶流出速度r、漏桶容量N 低O(1)(记录当前漏桶中容量) 高O(N)
令牌桶 令牌产生速度r、令牌桶容量N 低O(1)(记录当前令牌桶中令牌数) 高O(N)

第2章 Sentinel

官网:home

2.1 简介

Sentinel 是阿里中间件团队开源的,面向分布式服务架构的轻量级高可用流量控制组件,主要以流量为切入点,从流量控制、熔断降级、系统负载保护等多个维度来帮助用户保护服务的稳定性。

SpringCloud Alibaba系列——14Sentinel简介及基本应用

2.1.1 基本概念

  • 资源 (需要被保护的东西)

资源是 Sentinel 的关键概念。它可以是 Java 应用程序中的任何内容,例如,由应用程序提供的服务,或由应用程序调用的其它应用提供的服务,甚至可以是一段代码。在接下来的文档中,我们都会用资源来描述代码块。

只要通过 Sentinel API 定义的代码,就是资源,能够被 Sentinel 保护起来。大部分情况下,可以使用方法签名,URL,甚至服务名称作为资源名来标示资源。

资源的定义有两种,可以通过@SentinelResource(value=”doTest”)设置,如果没有设置的话,就默认取请求后面的一截

  • 规则(限流的规则/熔断规则)

围绕资源的实时状态设定的规则,可以包括流量控制规则、熔断降级规则以及系统保护规则。所有规则可以动态实时调整。

2.1.2 功能和设计理念

2.1.2.1 流量控制

流量控制在网络传输中是一个常用的概念,它用于调整网络包的发送数据。然而,从系统稳定性角度考虑,在处理请求的速度上,也有非常多的讲究。任意时间到来的请求往往是随机不可控的,而系统的处理能力是有限的。我们需要根据系统的处理能力对流量进行控制。Sentinel 作为一个调配器,可以根据需要把随机的请求调整成合适的形状,如下图所示:

SpringCloud Alibaba系列——14Sentinel简介及基本应用

流量控制有以下几个角度:

  • 资源的调用关系,例如资源的调用链路,资源和资源之间的关系;

  • 运行指标,例如 QPS、线程池、系统负载等;

  • 控制的效果,例如直接限流、冷启动、排队等。

不管是什么方式,流控的核心原理是,监控应用流量的QPS或者并发线程数这些指标,去判断指标的阈值来实现对流量的控制,防止瞬时流高峰流量导致系统被压垮,从而保证系统的高可用性。

在Sentinel中,限流的直接表现形式是,在执行Entry nodeA = SphU.entry(resourceName)的时候抛出FlowException异常。FlowExceptionBlockException的子类,您可以捕捉BlockException来自定义被限流之后的处理逻辑。

同一个资源可以创建多条限流规则。 FlowSlot 会对该资源的所有限流规则依次遍历,直到有规则触发限流或者所有规则遍历完毕。

一条限流规则主要由下面几个因素组成,我们可以组合这些元素来实现不同的限流效果:

  • resource :资源名,即限流规则的作用对象

  • count : 限流阈值

  • grade : 限流阈值类型(QPS 或并发线程数)

  • limitApp : 流控针对的调用来源,若为 default 则不区分调用来源

    • default:表示不区分调用者,来自任何调用者的请求都将进行限流统计。如果这个资源名的调用总和超过了这条规则定义的阈值,则触发限流。

    • {some_origin_name}:表示针对特定的调用者,只有来自这个调用者的请求才会进行流量控制。例如 NodeA 配置了一条针对调用者caller1的规则,那么当且仅当来自 caller1 对 NodeA 的请求才会触发流量控制。

    • other:表示针对除 {some_origin_name} 以外的其余调用方的流量进行流量控制。例如,资源NodeA配置了一条针对调用者 caller1 的限流规则,同时又配置了一条调用者为 other 的规则,那么任意来自非 caller1 对 NodeA 的调用,都不能超过 other 这条规则定义的阈值。

    • 同一个资源名可以配置多条规则,规则的生效顺序为:{some_origin_name} > other > default

  • strategy : 调用关系限流策略

  • controlBehavior : 流量控制效果(直接拒绝、Warm Up、匀速排队)

通过这个地址,可以查看实时的统计信息: http://localhost:8719/cnode?id=doTest

  • thread: 代表当前处理该资源的并发数;

  • pass: 代表一秒内到来到的请求;

  • blocked: 代表一秒内被流量控制的请求数量;

  • success: 代表一秒内成功处理完的请求;

  • total: 代表到一秒内到来的请求以及被阻止的请求总和;

  • RT: 代表一秒内该资源的平均响应时间;

  • 1m-pass: 则是一分钟内到来的请求;

  • 1m-block: 则是一分钟内被阻止的请求;

  • 1m-all: 则是一分钟内到来的请求和被阻止的请求的总和;

  • exception: 则是一秒内业务本身异常的总和。

  1. 并发线程数

    并发数控制用于保护业务线程池不被慢调用耗尽。例如,当应用所依赖的下游应用由于某种原因导致服务不稳定、响应延迟增加,对于调用者来说,意味着吞吐量下降和更多的线程数占用,极端情况下甚至导致线程池耗尽。为应对太多线程占用的情况,业内有使用隔离的方案,比如通过不同业务逻辑使用不同线程池来隔离业务自身之间的资源争抢(线程池隔离)。这种隔离方案虽然隔离性比较好,但是代价就是线程数目太多,线程上下文切换的 overhead 比较大,特别是对低延时的调用有比较大的影响。Sentinel 并发控制不负责创建和管理线程池,而是简单统计当前请求上下文的线程数目(正在执行的调用数目),如果超出阈值,新的请求会被立即拒绝,效果类似于信号量隔离。并发数控制通常在调用端进行配置。

  2. QPS流量

    当 QPS 超过某个阈值的时候,则采取措施进行流量控制。流量控制的效果包括以下几种:直接拒绝、Warm Up、匀速排队。对应 FlowRule 中的 controlBehavior 字段。

  3. 基于调用关系

    调用关系包含调用方和被调用方,一个方法有可能会调用其他方法,形成一个调用链,所谓的调用关系限流,就是根据不同的调用纬度来触发流量控制。

    • 根据调用方限流

    • 根据调用链路入口限流

    • 具有关系的资源流量控制

根据调用方限流

很多场景下,根据调用方来限流也是非常重要的。比如有两个服务 A 和 B 都向 Service Provider 发起调用请求,我们希望只对来自服务 B 的请求进行限流,则可以设置限流规则的 limitApp 为服务 B 的名称。Sentinel Dubbo Adapter 会自动解析 Dubbo 消费者(调用方)的 application name 作为调用方名称(origin),在进行资源保护的时候都会带上调用方名称。若限流规则未配置调用方(default),则该限流规则对所有调用方生效。若限流规则配置了调用方则限流规则将仅对指定调用方生效。

所谓调用方限流,就是根据请求来源进行流量控制,我们可以设置limitApp属性来配置来源信息,它有三个选项:

  • default :表示不区分调用者,来自任何调用者的请求都将进行限流统计。如果这个资源名的调用总和超过了这条规则定义的阈值,则触发限流。

  • {some_origin_name} :表示针对特定的调用者,只有来自这个调用者的请求才会进行流量控制。例如 NodeA 配置了一条针对调用者 caller1 的规则,那么当且仅当来自 caller1 对NodeA 的请求才会触发流量控制。

  • other :表示针对除 {some_origin_name} 以外的其余调用方的流量进行流量控制。例如,资源 NodeA 配置了一条针对调用者 caller1 的限流规则,同时又配置了一条调用者为 other 的规则,那么任意来自非 caller1 对 NodeA 的调用,都不能超过 other 这条规则定义的阈值。

对于同一个资源,可以配置多条规则,规则的生效顺序为: {some_origin_name} > other >default

根据调用链路入口限流

一个被限流的保护方法,可能来自于不同的调用链路,比如针对资源NodeA,入口 Entrance1 和Entrance2的请求都调用到了资源 NodeA ,Sentinel 允许只根据某个入口的统计信息对资源限流。比如我们可以设置 strategy 为 RuleConstant.STRATEGY_CHAIN ,同时设置 refResource 为Entrance1 来表示只有从入口 Entrance1 的调用才会记录到 NodeA 的限流统计当中,而不关心经Entrance2 到来的调用

SpringCloud Alibaba系列——14Sentinel简介及基本应用

具有关系的资源流量控制

当两个资源之间具有资源争抢或者依赖关系的时候,这两个资源便具有了关联。比如对数据库同一个字段的读操作和写操作存在争抢,读的速度过高会影响写得速度,写的速度过高会影响读的速度。如果放任读写操作争抢资源,则争抢本身带来的开销会降低整体的吞吐量。可使用关联限流来避免具有关联关系的资源之间过度的争抢,举例来说, read_db 和 write_db 这两个资源分别代表数据库读写,我们可以给 read_db 设置限流规则来达到写优先的目的:设置 strategy 为RuleConstant.STRATEGY_RELATE 同时设置 refResource 为 write_db 。这样当写库操作过于频繁时,读数据的请求会被限流

2.1.2.2 控制策略

  1. 直接拒绝

    直接拒绝( RuleConstant.CONTROL_BEHAVIOR_DEFAULT )方式是默认的流量控制方式,当QPS超过任意规则的阈值后,新的请求就会被立即拒绝,拒绝方式为抛出 FlowException 。这种方式适用于对系统处理能力确切已知的情况下,比如通过压测确定了系统的准确水位时。

  2. Warm Up

    Warm Up( RuleConstant.CONTROL_BEHAVIOR_WARM_UP )方式,即预热/冷启动方式。当系统长期处于低水位的情况下,当流量突然增加时,直接把系统拉升到高水位可能瞬间把系统压垮。通过”冷启动”,让通过的流量缓慢增加,在一定时间内逐渐增加到阈值上限,给冷系统一个预热的时间,避免冷系统被压垮

    如下图所示,当前系统所能够处理的最大并发数是480,首先在最下面的标记位置,系统一直处于空闲状态,接着请求量突然直线升高,这个时候系统并不是直接将QPS拉到最大值,而是在一定时间内逐步增加阈值,而中间这段时间就是一个系统逐步预热。SpringCloud Alibaba系列——14Sentinel简介及基本应用

  3. 匀速排队

    匀速排队( RuleConstant.CONTROL_BEHAVIOR_RATE_LIMITER )方式会严格控制请求通过的间隔时间,也即是让请求以均匀的速度通过,对应的是漏桶算法。SpringCloud Alibaba系列——14Sentinel简介及基本应用

     这种方式主要用于处理间隔性突发的流量,例如消息队列。想象一下这样的场景,在某一秒有大量的请求到来,而接下来的几秒则处于空闲状态,我们希望系统能够在接下来的空闲期间逐渐处理这些请求,而不是在第一秒直接拒绝多余的请求。

2.1.2.3 熔断降级

  1. 什么是熔断降级

    除了流量控制以外,降低调用链路中的不稳定资源也是 Sentinel 的使命之一。由于调用关系的复杂性,如果调用链路中的某个资源出现了不稳定,最终会导致请求发生堆积。这个问题和 Hystrix 里面描述的问题是一样的。SpringCloud Alibaba系列——14Sentinel简介及基本应用

     Sentinel 和 Hystrix 的原则是一致的: 当调用链路中某个资源出现不稳定,例如,表现为 timeout,异常比例升高的时候,则对这个资源的调用进行限制,并让请求快速失败,避免影响到其它的资源,最终产生雪崩的效果。

  2. 熔断降级设计理念

    在限制的手段上,Sentinel 和 Hystrix 采取了完全不一样的方法。

    Hystrix 通过线程池的方式,来对依赖(在我们的概念中对应资源)进行了隔离。这样做的好处是资源和资源之间做到了最彻底的隔离。缺点是除了增加了线程切换的成本,还需要预先给各个资源做线程池大小的分配。

    Sentinel 对这个问题采取了两种手段:

    • 通过并发线程数进行限制

    和资源池隔离的方法不同,Sentinel 通过限制资源并发线程的数量,来减少不稳定资源对其它资源的影响。这样不但没有线程切换的损耗,也不需要您预先分配线程池的大小。当某个资源出现不稳定的情况下,例如响应时间变长,对资源的直接影响就是会造成线程数的逐步堆积。当线程数在特定资源上堆积到一定的数量之后,对该资源的新请求就会被拒绝。堆积的线程完成任务后才开始继续接收请求。

    • 通过响应时间对资源进行降级

    除了对并发线程数进行控制以外,Sentinel 还可以通过响应时间来快速降级不稳定的资源。当依赖的资源出现响应时间过长后,所有对该资源的访问都会被直接拒绝,直到过了指定的时间窗口之后才重新恢复。

2.1.3 如何工作的

Sentinel 的主要工作机制如下:

  1. 对主流框架提供适配或者显示的 API,来定义需要保护的资源,并提供设施对资源进行实时统计和调用链路分析。(API的使用或者集成Springboot使用)

  2. 根据预设的规则,结合对资源的实时统计信息,对流量进行控制。同时,Sentinel 提供开放的接口,方便您定义及改变规则。(使用之前先定规则)

  3. Sentinel 提供实时的监控系统,方便您快速了解目前系统的状态。(控制台)

2.2 控制台

启动方式:

下载jar包,通过命令方式

Releases · alibaba/Sentinel · GitHub下载对应版本的dashboard jar包,进入到该jar所在的目录,然后通过java命令运行该jar包即可:

java -Dserver.port=7777 -Dcsp.sentinel.dashboard.server=localhost:7777 -Dproject.name=sentinel-dashboard-1.8.0 -jar sentinel-dashboard-1.8.0.jar

访问地址:localhost:7777

默认账号和密码都是:sentinel

2.3 应用方式

官网介绍了基本使用的方式有五种,其实归根结底就两种:1.api的使用;2.主流框架的封装。我们这节课就讲api的使用和集成springboot,在使用过程中包含了动态规则配置

版本对应关系:版本说明 · alibaba/spring-cloud-alibaba Wiki · GitHub

API使用只需要引sentinel-core包就可以(版本选择1.8.1)

集成springboot时版本选择:这里选择springboot的版本为2.3.2.RELEASE,所以Spring Cloud Alibaba Version版本选择为2.2.6.RELEASE,sentinel的版本选择为1.8.1

2.3.1 api使用

2.3.1.1 本地配置

1、创建springboot工程,依赖spring web

2、配置pom

<dependency>
    <groupId>com.alibaba.csp</groupId>
    <artifactId>sentinel-core</artifactId>
    <version>1.8.1</version>
</dependency>

3、初始化流控规则  

@SpringBootApplication
public class SpringbootSentinelApplication {

    //在spring容器启动的时候初始化流控规则
    public static void main(String[] args) {
        initFlowRules();
        SpringApplication.run(SpringbootSentinelApplication.class, args);
    }

    public static void initFlowRules(){
        List<FlowRule> rules = new ArrayList<FlowRule>();
        //定义一条规则
        FlowRule rule1 = new FlowRule();
        //规则是针对哪个资源,这个资源名可以任意定义,不定义的时候默认是url的值
        rule1.setResource("rule01");
        // QPS控制在2以内
        rule1.setCount(2);
        // QPS限流
        rule1.setGrade(RuleConstant.FLOW_GRADE_QPS);
        rule1.setLimitApp("default");
        rules.add(rule1);
        FlowRuleManager.loadRules(rules);
    }
}

4、定义请求进来的controller  

@RestController
public class UserController {
    @Autowired
    private UserServiceImpl01 userServiceImpl01;

    @GetMapping("/getUser")
    public String userName(@RequestParam("name") String name){
        return userServiceImpl01.getUserName(name);
    }
}

5、定义真正要限流的接口方法(一般在service层)

api的使用也有多种方式:if、try catch的方式

  • if的方式
  • @Service
    public class UserServiceImpl01 {
        public String getUserName(String name){
            if(SphO.entry("rule01")){
                try {
                    return "你好吗?亲爱的"+name;
                } finally {
                    SphO.exit();
                }
            }else{
                return "别问了,烦不烦啊";
            }
        }
    }

    try catch方式

  • @Service
    public class UserServiceImpl01 {
        public String getUserName(String name){
            Entry entry = null;
            try {
                entry = SphU.entry("rule01");
                return "你好吗?亲爱的"+name;
            }catch (BlockException e1){
                // 接口被限流的时候, 会进入到这里
                return "烦不烦啊,降级";
            }finally {
                // SphU.entry(xxx) 需要与 entry.exit() 成对出现,否则会导致调用链记录异常
                if (entry != null) {
                    entry.exit();
                }
            }
        }
    }

    6、日志查看

    日志 ~/logs/csp/${appName}-metrics.log.xxx 里看到下面的输出:

  • 时间戳|格式化的时间戳|资源名|通过的请求|被拒绝的请求|成功数量|异常数量|rt平均响应时间
    1600608724000|2020-09-20 21:32:04|helloWorld|5|6078|5|0|5|0|0|0
    1600608725000|2020-09-20 21:32:05|helloWorld|5|32105|5|0|0|0|0|0
    1600608726000|2020-09-20 21:32:06|helloWorld|5|41084|5|0|0|0|0|0
    1600608727000|2020-09-20 21:32:07|helloWorld|5|72211|5|0|0|0|0|0
    1600608728000|2020-09-20 21:32:08|helloWorld|5|60828|5|0|0|0|0|0
    1600608729000|2020-09-20 21:32:09|helloWorld|5|41696|5|0|0|0|0|0

    SpringCloud Alibaba系列——14Sentinel简介及基本应用

    SpringCloud Alibaba系列——14Sentinel简介及基本应用

2.3.1.2 动态配置

1、增加下面的依赖

<dependency>
    <groupId>com.alibaba.csp</groupId>
    <artifactId>sentinel-datasource-nacos</artifactId>
    <version>1.8.1</version>
</dependency>
<dependency>
    <groupId>com.alibaba.csp</groupId>
    <artifactId>sentinel-datasource-extension</artifactId>
    <version>1.8.1</version>
</dependency>
<dependency>
    <groupId>com.alibaba</groupId>
    <artifactId>fastjson</artifactId>
    <version>1.2.76</version>
</dependency>

 2、初始化流控规则,这里不在通过本地配置,而是通过配置中心去完成

所以在启动spring容器的时候要先连接配置中心,通过spi机制去实现

在resources目录下创建services目录,然后再services目录下创建com.alibaba.csp.sentinel.init.InitFunc文件

com.example.springbootsentinel.spi.DataSourceInitFunc
public class DataSourceInitFunc implements InitFunc {
    private final String nacosAddress="192.168.8.74:8848";
    private final String groupId="SENTINEL_GROUP";
    private final String dataId="--flow-rules";

    private final String appName="springboot-sentinel";
    @Override
    public void init() throws Exception {
        ReadableDataSource<String, List<FlowRule>> flowRuleDs=
                new NacosDataSource<List<FlowRule>>(nacosAddress,groupId,appName+dataId,
                        source-> JSON.parseObject(source,new TypeReference<List<FlowRule>>(){}));
        FlowRuleManager.register2Property(flowRuleDs.getProperty());

    }
}

 3、启动类

@SpringBootApplication
public class SpringbootSentinelApplication {
    public static void main(String[] args) {
        SpringApplication.run(SpringbootSentinelApplication.class, args);
    }
}

4、nacos的配置  

SpringCloud Alibaba系列——14Sentinel简介及基本应用

 5、测试类跟上面本地配置一样

2.3.2 集成springCloud

2.3.2.1 本地配置

1、配置pom

<dependencies>
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-web</artifactId>
    </dependency>

    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-test</artifactId>
        <scope>test</scope>
    </dependency>
    <dependency>
        <groupId>com.alibaba.csp</groupId>
        <artifactId>sentinel-datasource-nacos</artifactId>
        <version>1.8.1</version>
    </dependency>
    <dependency>
        <groupId>com.alibaba.csp</groupId>
        <artifactId>sentinel-datasource-extension</artifactId>
        <version>1.8.1</version>
    </dependency>
    <dependency>
        <groupId>com.alibaba.cloud</groupId>
        <artifactId>spring-cloud-starter-alibaba-sentinel</artifactId>
        <version>2.2.6.RELEASE</version>
    </dependency>
    <dependency>
        <groupId>com.alibaba.csp</groupId>
        <artifactId>sentinel-transport-simple-http</artifactId>
        <version>1.8.1</version>
    </dependency>
</dependencies>

2、初始化流控配置  

@SpringBootApplication
public class SpringbootSentinelApplication {

    //在spring容器启动的时候初始化流控规则
    public static void main(String[] args) {
        initFlowRules();
        SpringApplication.run(SpringbootSentinelApplication.class, args);
    }

    public static void initFlowRules(){
        List<FlowRule> rules = new ArrayList<FlowRule>();
        //定义一条规则
        FlowRule rule1 = new FlowRule();
        //规则是针对哪个资源,这个资源名可以任意定义,不定义的时候默认是url的值
        rule1.setResource("rule01");
        // QPS控制在2以内
        rule1.setCount(2);
        // QPS限流
        rule1.setGrade(RuleConstant.FLOW_GRADE_QPS);
        rule1.setLimitApp("default");
        rules.add(rule1);
        FlowRuleManager.loadRules(rules);
    }
}

3、实现接口  

@Service
public class UserServiceImpl01 {
    @SentinelResource(value = "rule01",blockHandler = "blockHandlerDemo",fallback = "fallBackDemo")
    public String getUserName(String name){
        // 模拟接口运行时抛出代码异常
        if ("文文".equals(name)) {
            throw new RuntimeException();
        }
        return "你好吗?亲爱的"+name;
    }

    /**
     * 订单查询接口抛出限流或降级时的处理逻辑
     *
     * 注意: 方法参数、返回值要与原函数保持一致
     * @return
     */
    public String blockHandlerDemo(String name, BlockException e) {
        e.printStackTrace();
        return "被限流了, " + name;
    }

    /**
     * 订单查询接口运行时抛出的异常提供fallback处理
     *
     * 注意: 方法参数、返回值要与原函数保持一致
     * @return
     */
    public String fallBackDemo(String name, Throwable e) {
        return "被熔断了: " + name;
    }
}

2.3.2.2 动态配置

 1、启动类

@SpringBootApplication
public class SpringbootSentinelApplication {

    public static void main(String[] args) {
        SpringApplication.run(SpringbootSentinelApplication.class, args);
    }
}

2、可以通过spi机制也可以通过配置文件实现,这里通过配置文件  

# 应用名称
spring.application.name=springboot-sentinel

# 应用服务 WEB 访问端口
server.port=8080
# spring 静态资源扫描路径
spring.resources.static_locations=classpath:/static/

# Sentinel 控制台地址
spring.cloud.sentinel.transport.dashboard=127.0.0.1:7777
# 取消Sentinel控制台懒加载
# 默认情况下 Sentinel 会在客户端首次调用的时候进行初始化,开始向控制台发送心跳包
# 配置 sentinel.eager=true 时,取消Sentinel控制台懒加载功能
spring.cloud.sentinel.eager=true

spring.cloud.sentinel.datasource.ds1.nacos.serverAddr=192.168.8.74:8848
spring.cloud.sentinel.datasource.ds1.nacos.dataId=springboot-sentinel--flow-rules
spring.cloud.sentinel.datasource.ds1.nacos.groupId=SENTINEL_GROUP
# FlowRule , json
spring.cloud.sentinel.datasource.ds1.nacos.dataType=json
spring.cloud.sentinel.datasource.ds1.nacos.ruleType=flow
spring.cloud.sentinel.datasource.ds1.nacos.username=nacos
spring.cloud.sentinel.datasource.ds1.nacos.password=nacos

2.4 集群限流

代码见sentinel-token-server和springcloud-sentinel-cluster-client

在前面的所有案例中,我们只是基于Sentinel的基本使用和单机限流的使用,假如有这样一个场景,我们现在把provider部署了10个集群,希望调用这个服务的api的总的qps是100,意味着每一台机器的qps是10,理想情况下总的qps就是100.但是实际上由于负载均衡策略的流量分发并不是非常均匀的,就会导致总的qps不足100时,就被限了。在这个场景中,仅仅依靠单机来实现总体流量的控制是有问题的。所以最好是能实现集群限流。

集群流控中共有两种身份:

  • Token Client:集群流控客户端,用于向所属 Token Server 通信请求 token。集群限流服务端会返回给客户端结果,决定是否限流。

  • Token Server:即集群流控服务端,处理来自 Token Client 的请求,根据配置的集群规则判断是否应该发放 token(是否允许通过)。

SpringCloud Alibaba系列——14Sentinel简介及基本应用

 下面的演示案例,主要会集成到Dubbo服务层,实现Dubbo服务集群的分布式限流

2.4.1 Token-server

1、创建sentinel-token-server的Maven项目

2、添加依赖

<?xml version="1.0" encoding="UTF-8"?>

<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
  xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
  <modelVersion>4.0.0</modelVersion>

  <groupId>com.example.sentinel</groupId>
  <artifactId>sentinel-token-server</artifactId>
  <version>1.0-SNAPSHOT</version>

  <name>sentinel-token-server</name>
  <!-- FIXME change it to the project's website -->
  <url>http://www.example.com</url>

  <properties>
    <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
    <maven.compiler.source>1.8</maven.compiler.source>
    <maven.compiler.target>1.8</maven.compiler.target>
  </properties>

  <dependencies>
    <dependency>
      <groupId>junit</groupId>
      <artifactId>junit</artifactId>
      <version>4.11</version>
      <scope>test</scope>
    </dependency>
    <dependency>
      <groupId>com.alibaba.csp</groupId>
      <artifactId>sentinel-cluster-server-default</artifactId>
      <version>1.8.1</version>
    </dependency>
    <dependency>
      <groupId>com.alibaba.csp</groupId>
      <artifactId>sentinel-datasource-nacos</artifactId>
      <version>1.8.1</version>
    </dependency>
    <dependency>
      <groupId>com.alibaba.csp</groupId>
      <artifactId>sentinel-transport-simple-http</artifactId>
      <version>1.8.1</version>
    </dependency>
    <dependency>
      <groupId>org.slf4j</groupId>
      <artifactId>slf4j-log4j12</artifactId>
      <version>1.7.30</version>
    </dependency>
  </dependencies>
</project>

3、在resource目录下创建log4j.properties文件并配置

log4j.rootLogger=debug, stdout
log4j.appender.stdout=org.apache.log4j.ConsoleAppender
log4j.appender.stdout.layout=org.apache.log4j.PatternLayout
log4j.appender.stdout.layout.ConversionPattern=%d %p [%c] - %m%n

 4、添加InitFunc实现类

public class FlowRuleInitFunc implements InitFunc {
    private final String nacosAddress="192.168.8.74:8848";
    private final String groupId="DEFAULT_GROUP";
    private String dataId="-flow-rules"; //dataId的后缀,它会和namespace组成一个完整的dataID,namespace表示token-client的名字,后续会用到

    @Override
    public void init() throws Exception {
        ClusterFlowRuleManager.setPropertySupplier(namespace->{
            ReadableDataSource<String, List<FlowRule>> flowRuleDs=
                    new NacosDataSource<>(nacosAddress, groupId, namespace + dataId,
                            source -> JSON.parseObject(source, new TypeReference<List<FlowRule>>() {
                            }));
            return flowRuleDs.getProperty();
        });
    }
}

5、SPI实现,在META-INF/services目录下创建com.alibaba.csp.sentinel.init.InitFunc文件  

com.example.sentinel.FlowRuleInitFunc

6、创建启动类  

public class ClusterServer {

    public static void main(String[] args) throws Exception {
        ClusterTokenServer tokenServer=new SentinelDefaultTokenServer();
        //手动载入namespace和serverTransportConfig的配置到ClusterServerConfigManager
        //集群限流服务端通信相关配置
        ClusterServerConfigManager.loadGlobalTransportConfig(
                new ServerTransportConfig().setIdleSeconds(600).setPort(9999));
        //加载namespace集合列表() , namespace也可以放在配置中心
        ClusterServerConfigManager.loadServerNamespaceSet(Collections.singleton("cluster-demo"));
        tokenServer.start();
        //Token-client会上报自己的project.name到token-server。Token-server会根据namespace来统计连接数
    }
}

7、nacos上面增加配置

  • Dataid:cluster-demo-flow-rules

  • Group:DEFAULT_GROUP

  • 格式为JSON

    [{
    	"resource":"clusterdemo01",
    	"grade":1,
    	"count":5,
    	"clusterMode":true,
    	"clusterConfig":{
    		"flowId":100001,
    		"thresholdType":1,
    		"fallbackToLocalWhenFail":true
    	}
    }]

 8、启动:

-Dcsp.sentinel.dashboard.server=localhost:7777 -Dproject.name=sentinel-token-server -Dcsp.sentinel.log.use.pid=true

服务启动之后,在$user.home$/logs/csp/ 可以找到sentinel-record.log.pid*.date文件,如果看到日志文件中获取到了远程服务的信息,说明token-server启动成功了  

2.4.2 cluster-client

1、创建springcloud-sentinel-cluster-client的springboot项目

2、引包

<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
    <modelVersion>4.0.0</modelVersion>
    <parent>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-parent</artifactId>
        <version>2.3.2.RELEASE</version>
        <relativePath/> <!-- lookup parent from repository -->
    </parent>
    <groupId>com.example</groupId>
    <artifactId>springcloud-sentinel-cluster-client</artifactId>
    <version>0.0.1-SNAPSHOT</version>
    <name>springcloud-sentinel-cluster-client</name>
    <description>Demo project for Spring Boot</description>
    <properties>
        <java.version>1.8</java.version>
    </properties>
    <dependencies>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-web</artifactId>
        </dependency>

        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-test</artifactId>
            <scope>test</scope>
        </dependency>
        <dependency>
            <groupId>com.alibaba.cloud</groupId>
            <artifactId>spring-cloud-starter-alibaba-sentinel</artifactId>
            <version>2.2.6.RELEASE</version>
        </dependency>
        <dependency>
            <groupId>com.alibaba.csp</groupId>
            <artifactId>sentinel-datasource-nacos</artifactId>
            <version>1.8.1</version>
        </dependency>
    </dependencies>
</project>

 3、通过SPI实现InitFunc接口,实现类

public class FlowRuleInitFunc implements InitFunc {

    private static final String CLUSTER_SERVER_HOST = "localhost";
    private static final int CLUSTER_SERVER_PORT = 9999; //Token-Server的端口和ip
    private static final int REQUEST_TIME_OUT = 200000;

    private static final String REMOTE_ADDRESS = "192.168.8.74:8848"; //nacos服务器地址
    private static final String GROUP_ID = "DEFAULT_GROUP";
    private static final String FLOW_POSTFIX = "-flow-rules";
    private static final String APP_NAME = "cluster-demo"; //这个名字需要在Token-Server的namespace中配置

    @Override
    public void init() throws Exception {

        ClusterStateManager.applyState(ClusterStateManager.CLUSTER_CLIENT);

        loadClusterClientConfig();
        registerClusterFlowRuleProperty();
    }

    //通过硬编码的方式,配置连接到token-server服务的地址,{这种在实际使用过程中不建议,后续可以基于动态配置源改造}
    private static void loadClusterClientConfig() {
        ClusterClientAssignConfig assignConfig = new ClusterClientAssignConfig();
        assignConfig.setServerHost(CLUSTER_SERVER_HOST);
        assignConfig.setServerPort(CLUSTER_SERVER_PORT);
        ClusterClientConfigManager.applyNewAssignConfig(assignConfig);
        ClusterClientConfig clientConfig = new ClusterClientConfig();
        clientConfig.setRequestTimeout(REQUEST_TIME_OUT); //token-client请求token-server获取令牌超时的时间
        ClusterClientConfigManager.applyNewConfig(clientConfig);
    }

    /**
     * 注册动态规则Property
     * 当client与Server连接中断,退化为本地限流时需要用到的该规则
     * 该配置为必选项,客户端会从nacos上加载限流规则,请求tokenserver时,会带上要check的规则id
     */
    private static void registerClusterFlowRuleProperty() {
        ReadableDataSource<String, List<FlowRule>> ds =
                new NacosDataSource<List<FlowRule>>(REMOTE_ADDRESS, GROUP_ID, APP_NAME + FLOW_POSTFIX,
                        source -> JSON.parseObject(source, new TypeReference<List<FlowRule>>() {
                        }));
        FlowRuleManager.register2Property(ds.getProperty());
    }
}

4、controller和service复制之前的项目中的类

5、集成Dashboard

spring.application.name=cluster-client
server.port=8080
# 连接Dashboard地址
spring.cloud.sentinel.transport.dashboard=localhost:7777
# 默认是当触发了限流后,才会连接到dashboard,设置为true启动后就连接
spring.cloud.sentinel.eager=true 
spring.cloud.sentinel.log.switch-pid=true

6、然后启动两个同样的Token-client,端口分别为8080和8081。

7、然后通过Jmeter同时向两个端口发起测试,观察结果

第3章 Sentinel和Hystrix

3.1 总体说明

先来看一下 Hystrix 的官方介绍:

Hystrix is a library that helps you control the interactions between these distributed services by adding latency tolerance and fault tolerance logic. Hystrix does this by isolating points of access between the services, stopping cascading failures across them, and providing fallback options, all of which improve your system’s overall resiliency.

可以看到 Hystrix 的关注点在于以隔离和熔断为主的容错机制,超时或被熔断的调用将会快速失败,并可以提供 fallback 机制。

而 Sentinel 的侧重点在于:

  • 多样化的流量控制

  • 熔断降级

  • 系统负载保护

  • 实时监控和控制台

可以看到两者解决的问题还是有比较大的不同的,下面我们来具体对比一下。

3.2 共同特性

1、资源模型和执行模型上的对比

Hystrix 的资源模型设计上采用了命令模式,将对外部资源的调用和 fallback 逻辑封装成一个命令对象(HystrixCommand/ HystrixObservableCommand),其底层的执行是基于 RxJava 实现的。每个 Command 创建时都要指定 commandKey 和 groupKey(用于区分资源)以及对应的隔离策略(线程池隔离 or 信号量隔离)。线程池隔离模式下需要配置线程池对应的参数(线程池名称、容量、排队超时等),然后 Command 就会在指定的线程池按照指定的容错策略执行;信号量隔离模式下需要配置最大并发数,执行 Command 时 Hystrix 就会限制其并发调用。

Sentinel 的设计则更为简单。相比 Hystrix Command 强依赖隔离规则,Sentinel 的资源定义与规则配置的耦合度更低。Hystrix 的 Command 强依赖于隔离规则配置的原因是隔离规则会直接影响 Command 的执行。在执行的时候 Hystrix 会解析 Command 的隔离规则来创建 RxJava Scheduler 并在其上调度执行,若是线程池模式则 Scheduler 底层的线程池为配置的线程池,若是信号量模式则简单包装成当前线程执行的 Scheduler。

而Sentinel则不一样,开发的时候只需要考虑这个方法/代码是否需要保护,置于用什么来保护,可以任何时候动态实时的区修改。

从 0.1.1 版本开始,Sentinel 还支持基于注解的资源定义方式,可以通过注解参数指定异常处理函数和 fallback 函数。Sentinel 提供多样化的规则配置方式。除了直接通过 loadRules API 将规则注册到内存态之外,用户还可以注册各种外部数据源来提供动态的规则。用户可以根据系统当前的实时情况去动态地变更规则配置,数据源会将变更推送至 Sentinel 并即时生效。

2、隔离设计上的对比

隔离是 Hystrix 的核心功能之一。Hystrix 提供两种隔离策略:线程池隔离(Bulkhead Pattern)和信号量隔离,其中最推荐也是最常用的是线程池隔离。Hystrix 的线程池隔离针对不同的资源分别创建不同的线程池,不同服务调用都发生在不同的线程池中,在线程池排队、超时等阻塞情况时可以快速失败,并可以提供 fallback 机制。线程池隔离的好处是隔离度比较高,可以针对某个资源的线程池去进行处理而不影响其它资源,但是代价就是线程上下文切换的 overhead 比较大,特别是对低延时的调用有比较大的影响。

但是,实际情况下,线程池隔离并没有带来非常多的好处。最直接的影响,就是会让机器资源碎片化。考虑这样一个常见的场景,在 Tomcat 之类的 Servlet 容器使用 Hystrix,本身 Tomcat 自身的线程数目就非常多了(可能到几十或一百多),如果加上 Hystrix 为各个资源创建的线程池,总共线程数目会非常多(几百个线程),这样上下文切换会有非常大的损耗。另外,线程池模式比较彻底的隔离性使得 Hystrix 可以针对不同资源线程池的排队、超时情况分别进行处理,但这其实是超时熔断和流量控制要解决的问题,如果组件具备了超时熔断和流量控制的能力,线程池隔离就显得没有那么必要了。

Hystrix 的信号量隔离限制对某个资源调用的并发数。这样的隔离非常轻量级,仅限制对某个资源调用的并发数,而不是显式地去创建线程池,所以 overhead 比较小,但是效果不错。但缺点是无法对慢调用自动进行降级,只能等待客户端自己超时,因此仍然可能会出现级联阻塞的情况。

Sentinel 可以通过并发线程数模式的流量控制来提供信号量隔离的功能。并且结合基于响应时间的熔断降级模式,可以在不稳定资源的平均响应时间比较高的时候自动降级,防止过多的慢调用占满并发数,影响整个系统。

3、熔断降级的对比

Sentinel 和 Hystrix 的熔断降级功能本质上都是基于熔断器模式(Circuit Breaker Pattern)。Sentinel 与 Hystrix 都支持基于失败比率(异常比率)的熔断降级,在调用达到一定量级并且失败比率达到设定的阈值时自动进行熔断,此时所有对该资源的调用都会被 block,直到过了指定的时间窗口后才启发性地恢复。上面提到过,Sentinel 还支持基于平均响应时间的熔断降级,可以在服务响应时间持续飙高的时候自动熔断,拒绝掉更多的请求,直到一段时间后才恢复。这样可以防止调用非常慢造成级联阻塞的情况。

4、实时指标统计实现的对比

Hystrix 和 Sentinel 的实时指标数据统计实现都是基于滑动窗口的。Hystrix 1.5 之前的版本是通过环形数组实现的滑动窗口,通过锁配合 CAS 的操作对每个桶的统计信息进行更新。Hystrix 1.5 开始对实时指标统计的实现进行了重构,将指标统计数据结构抽象成了响应式流(reactive stream)的形式,方便消费者去利用指标信息。同时底层改造成了基于 RxJava 的事件驱动模式,在服务调用成功/失败/超时的时候发布相应的事件,通过一系列的变换和聚合最终得到实时的指标统计数据流,可以被熔断器或 Dashboard 消费。

Sentinel 目前抽象出了 Metric 指标统计接口,底层可以有不同的实现,目前默认的实现是基于 LeapArray 的滑动窗口,后续根据需要可能会引入 reactive stream 等实现。

3.3 Sentinel 特性

除了之前提到的两者的共同特性之外,Sentinel 还提供以下的特色功能:

1、轻量级和高性能

Sentinel 作为一个功能完备的高可用流量管控组件,其核心 sentinel-core 没有任何多余依赖,打包后只有不到 200 KB,非常轻量级。开发者可以放心地引入 sentinel-core 而不需担心依赖问题。同时,Sentinel 提供了多种扩展点,用户可以很方便地根据需求去进行扩展,并且无缝地切合到 Sentinel 中。

引入 Sentinel 带来的性能损耗非常小。只有在业务单机量级超过 25W QPS 的时候才会有一些显著的影响(5% – 10% 左右),单机 QPS 不太大的时候损耗几乎可以忽略不计。

2、流量控制

Sentinel 可以针对不同的调用关系,以不同的运行指标(如 QPS、并发调用数、系统负载等)为基准,对资源调用进行流量控制,将随机的请求调整成合适的形状。

Sentinel 支持多样化的流量整形策略,在 QPS 过高的时候可以自动将流量调整成合适的形状。常用的有:

  • 直接拒绝模式:即超出的请求直接拒绝。

  • 慢启动预热模式:当流量激增的时候,控制流量通过的速率,让通过的流量缓慢增加,在一定时间内逐渐增加到阈值上限,给冷系统一个预热的时间,避免冷系统被压垮。

  • 匀速器模式:利用 Leaky Bucket 算法实现的匀速模式,严格控制了请求通过的时间间隔,同时堆积的请求将会排队,超过超时时长的请求直接被拒绝。Sentinel 还支持基于调用关系的限流,包括基于调用方限流、基于调用链入口限流、关联流量限流等,依托于 Sentinel 强大的调用链路统计信息,可以提供精准的不同维度的限流。SpringCloud Alibaba系列——14Sentinel简介及基本应用

 目前 Sentinel 对异步调用链路的支持还不是很好,后续版本会着重改善支持异步调用。

3、系统负载保护

Sentinel 对系统的维度提供保护,负载保护算法借鉴了 TCP BBR 的思想。当系统负载较高的时候,如果仍持续让请求进入,可能会导致系统崩溃,无法响应。在集群环境下,网络负载均衡会把本应这台机器承载的流量转发到其它的机器上去。如果这个时候其它的机器也处在一个边缘状态的时候,这个增加的流量就会导致这台机器也崩溃,最后导致整个集群不可用。针对这个情况,Sentinel 提供了对应的保护机制,让系统的入口流量和系统的负载达到一个平衡,保证系统在能力范围之内处理最多的请求。

4、实时监控和控制面板

Sentinel 提供 HTTP API 用于获取实时的监控信息,如调用链路统计信息、簇点信息、规则信息等。如果用户正在使用 Spring Boot/Spring Cloud 并使用了Sentinel Spring Cloud Starter,还可以方便地通过其暴露的 Actuator Endpoint 来获取运行时的一些信息,如动态规则等。未来 Sentinel 还会支持标准化的指标监控 API,可以方便地整合各种监控系统和可视化系统,如 Prometheus、Grafana 等。

Sentinel控制台(Dashboard)提供了机器发现、配置规则、查看实时监控、查看调用链路信息等功能,使得用户可以非常方便地去查看监控和进行配置。

5、生态

Sentinel 目前已经针对 Servlet、Dubbo、Spring Boot/Spring Cloud、gRPC 等进行了适配,用户只需引入相应依赖并进行简单配置即可非常方便地享受 Sentinel 的高可用流量防护能力。未来 Sentinel 还会对更多常用框架进行适配,并且会为 Service Mesh 提供集群流量防护的能力。

3.4 总结

Sentinel Hystrix
隔离策略 信号量隔离 线程池隔离/信号量隔离
熔断降级策略 基于响应时间或失败比率 基于失败比率
实时指标实现 滑动窗口 滑动窗口(基于R小Java)
规则配置 支持多种数据源 支持多种数据源
扩展性 多个扩展点 插件的形式
基于注解的支持 支持 支持
限流 基于QPS、支持基于调用关系的限流 不支持
流量整形 支持慢启动、匀速器模式 不支持
系统负载保护 支持 不支持
控制台 开箱即用、可配置规则、查看秒级监控、机器发现等 不完善
常见框架的适配 Servlet、Spring Cloud、Dubbo、GRPC等 Servlet、Spring Cloud Netflix

下文预告

  1. Sentinel的工作原理

版权声明:本文内容由互联网用户自发贡献,该文观点仅代表作者本人。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如发现本站有涉嫌侵权/违法违规的内容, 请发送邮件至 举报,一经查实,本站将立刻删除。

文章由极客之音整理,本文链接:https://www.bmabk.com/index.php/post/76693.html

(0)
小半的头像小半

相关推荐

极客之音——专业性很强的中文编程技术网站,欢迎收藏到浏览器,订阅我们!