Sentinel微服务限流框架

8/23/2021 微服务

# Sentinel 介绍🚨

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

# Sentinel 具有以下特征:🔎

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

# Sentinel 的主要特性:🎇

sentinel-1

# Sentinel 分为两个部分:🧐

  • 核心库(Java 客户端) 不依赖任何框架/库,能够运行于所有 Java 运行时环境,同时对 Dubbo / Spring Cloud 等框架也有较好的支持。
  • 控制台(Dashboard) 基于 Spring Boot 开发,打包后可以直接运行,不需要额外的 Tomcat 等应用容器。

官方Github (opens new window)

官方文档地址 (opens new window)

# 项目实例🛻

# 0、前期准备🍳

本笔记sentinel-dashboard版本为:V1.8.0

Nacos版本为:1.4.1

# 指定端口号启动
java -Dserver.port=8718 -Dcsp.sentinel.dashboard.server=localhost:8718 -Dproject.name=sentinel-dashboard -jar sentinel-dashboard-1.8.0.jar
1
2

# 1、引入依赖

<dependencies>
        <!--SpringCloud ailibaba nacos -->
        <dependency>
            <groupId>com.alibaba.cloud</groupId>
            <artifactId>spring-cloud-starter-alibaba-nacos-discovery</artifactId>
        </dependency>
        <!--SpringCloud ailibaba sentinel-datasource-nacos 后续做持久化用到-->
        <dependency>
            <groupId>com.alibaba.csp</groupId>
            <artifactId>sentinel-datasource-nacos</artifactId>
        </dependency>
        <!--SpringCloud ailibaba sentinel -->
        <dependency>
            <groupId>com.alibaba.cloud</groupId>
            <artifactId>spring-cloud-starter-alibaba-sentinel</artifactId>
        </dependency>
        <!--openfeign-->
        <dependency>
            <groupId>org.springframework.cloud</groupId>
            <artifactId>spring-cloud-starter-openfeign</artifactId>
        </dependency>
        <!-- SpringBoot整合Web组件+actuator -->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-web</artifactId>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-actuator</artifactId>
        </dependency>
    </dependencies>
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

# 2、yaml配置

server:
  port: 8401

spring:
  application:
    name: cloudalibaba-sentinel-service
  cloud:
    nacos:
      discovery:
        server-addr: localhost:8848 #Nacos服务注册中心地址
    sentinel:
      transport:
        dashboard: localhost:8080 #配置Sentinel dashboard地址
        port: 8719
      datasource:
        ds1:
          nacos:
            server-addr: localhost:8848
            dataId: cloudalibaba-sentinel-service
            groupId: DEFAULT_GROUP
            data-type: json
            rule-type: flow

management:
  endpoints:
    web:
      exposure:
        include: '*'

#feign:
  #sentinel:
    #enabled: true # 激活Sentinel对Feign的支持
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

# 3、主启动类

/**
 * @Description: 主启动
 * @author: LiJunYi
 */
@EnableDiscoveryClient
@SpringBootApplication
public class MainApp8401
{
    public static void main(String[] args) {
        SpringApplication.run(MainApp8401.class, args);
    }
}
1
2
3
4
5
6
7
8
9
10
11
12

# 4、controller限流测试接口

/**
 * 流量限制控制器
 * @author LiJunYi
 */
@RestController
@Slf4j
public class FlowLimitController
{
    @GetMapping("/testA")
    public String testA()
    {
        return "------testA";
    }

    @GetMapping("/testB")
    public String testB()
    {
        log.info(Thread.currentThread().getName()+"\t"+"...testB");
        return "------testB";
    }


    @GetMapping("/testD")
    public String testD()
    {
       /* try { TimeUnit.SECONDS.sleep(1); } catch (InterruptedException e) { e.printStackTrace(); }
        log.info("testD 测试RT");*/

        log.info("testD 异常比例");
        // int age = 10/0;
        return "------testD";
    }

    @GetMapping("/testE")
    public String testE()
    {
        log.info("testE 测试异常数");
        int age = 10/0;
        return "------testE 测试异常数";
    }

    @GetMapping("/testHotKey")
    @SentinelResource(value = "testHotKey",blockHandler = "dealTestHotKey")
    public String testHotKey(@RequestParam(value = "p1",required = false) String p1,
                             @RequestParam(value = "p2",required = false) String p2)
    {
        //int age = 10/0;
        return "------testHotKey";
    }

    /**
     * 兜底方法
     *
     * @param p1        p1
     * @param p2        p2
     * @param exception 异常
     * @return {@link String}
     */
    public String dealTestHotKey(String p1, String p2, BlockException exception)
    {
        //sentinel系统默认的提示:Blocked by Sentinel (flow limiting)
        return "------dealTestHotKey,o(╥﹏╥)o";
    }

}

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

# 5、Sentinel控制台

启动程序去调用 testA与testB接口,然后在Sentinel控制台中我们便可以看见下图情况:

sentinel-2

sentinel-3

# 流控规则🌋

sentinel-4

解释

1、资源名:唯一名称,默认请求路径

2、针对来源: Sentinel可以针对调用者进行限流,填写微服务名,默认default(不区分来源)

3、阈值类型/单机阈值:

	QPS(每秒钟的请求数量)︰当调用该api的QPS达到阈值的时候,进行限流
	
	线程数:当调用该api的线程数达到阈值的时候,进行限流

4、是否集群:不需要集群

5、流控模式:

	直接: api达到限流条件时,直接限流

	关联:当关联的资源达到阈值时,就限流自己

	链路:只记录指定链路上的流量(指定资源从入口资源进来的流量,如果达到阈值,就进行限流)【api级别的针对来源】

6、流控效果:

	快速失败:直接失败,抛异常

	Warm Up:根据codeFactor (冷加载因子,默认3)的值,从阈值/codeFactor,经过预热时长,才达到设置的QPS阈值

	排队等待:匀速排队,让请求以匀速的速度通过,阈值类型必须设置为QPS,否则无效
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

# 流控模式(直接 关联 链路)😎

# 直接 -> 快速失败(系统默认)

sentinel-5

sentinel-6

# 关联:当关联的资源达到阈值,就限流自己

sentinel-7

# 链路:多个请求调用同一个微服务

# 流控效果🙌

# 快速失败

sentinel-8

# Warm up 预热 冷启动

官方说明 (opens new window)

  • 公式表示为:阈值除以coldFactor(默认值为3),经过预热时长后才会达到阈值
  • 默认coldFactor为3,即请求QPS 从 threshold / 3开始,经预热时长逐渐升至设定的QPS阈值

# 源码解析

public class WarmUpController implements TrafficShapingController {

    protected double count;
    private int coldFactor;
    protected int warningToken = 0;
    private int maxToken;
    protected double slope;

    protected AtomicLong storedTokens = new AtomicLong(0);
    protected AtomicLong lastFilledTime = new AtomicLong(0);

    public WarmUpController(double count, int warmUpPeriodInSec, int coldFactor) {
        construct(count, warmUpPeriodInSec, coldFactor);
    }

    public WarmUpController(double count, int warmUpPeriodInSec) {
        construct(count, warmUpPeriodInSec, 3);// <--->这里
    }

    private void construct(double count, int warmUpPeriodInSec, int coldFactor) {

        if (coldFactor <= 1) {
            throw new IllegalArgumentException("Cold factor should be larger than 1");
        }

        this.count = count;

        this.coldFactor = coldFactor;

        // thresholdPermits = 0.5 * warmupPeriod / stableInterval.
        // warningToken = 100;
        warningToken = (int)(warmUpPeriodInSec * count) / (coldFactor - 1);
        // / maxPermits = thresholdPermits + 2 * warmupPeriod /
        // (stableInterval + coldInterval)
        // maxToken = 200
        maxToken = warningToken + (int)(2 * warmUpPeriodInSec * count / (1.0 + coldFactor));

        // slope
        // slope = (coldIntervalMicros - stableIntervalMicros) / (maxPermits
        // - thresholdPermits);
        slope = (coldFactor - 1.0) / count / (maxToken - warningToken);

    }

    @Override
    public boolean canPass(Node node, int acquireCount) {
        return canPass(node, acquireCount, false);
    }

    @Override
    public boolean canPass(Node node, int acquireCount, boolean prioritized) {
        long passQps = (long) node.passQps();

        long previousQps = (long) node.previousPassQps();
        syncToken(previousQps);

        // 开始计算它的斜率
        // 如果进入了警戒线,开始调整他的qps
        long restToken = storedTokens.get();
        if (restToken >= warningToken) {
            long aboveToken = restToken - warningToken;
            // 消耗的速度要比warning快,但是要比慢
            // current interval = restToken*slope+1/count
            double warningQps = Math.nextUp(1.0 / (aboveToken * slope + 1.0 / count));
            if (passQps + acquireCount <= warningQps) {
                return true;
            }
        } else {
            if (passQps + acquireCount <= count) {
                return true;
            }
        }

        return false;
    }

    protected void syncToken(long passQps) {
        long currentTime = TimeUtil.currentTimeMillis();
        currentTime = currentTime - currentTime % 1000;
        long oldLastFillTime = lastFilledTime.get();
        if (currentTime <= oldLastFillTime) {
            return;
        }

        long oldValue = storedTokens.get();
        long newValue = coolDownTokens(currentTime, passQps);

        if (storedTokens.compareAndSet(oldValue, newValue)) {
            long currentValue = storedTokens.addAndGet(0 - passQps);
            if (currentValue < 0) {
                storedTokens.set(0L);
            }
            lastFilledTime.set(currentTime);
        }

    }

    private long coolDownTokens(long currentTime, long passQps) {
        long oldValue = storedTokens.get();
        long newValue = oldValue;

        // 添加令牌的判断前提条件:
        // 当令牌的消耗程度远远低于警戒线的时候
        if (oldValue < warningToken) {
            newValue = (long)(oldValue + (currentTime - lastFilledTime.get()) * count / 1000);
        } else if (oldValue > warningToken) {
            if (passQps < (int)count / coldFactor) {
                newValue = (long)(oldValue + (currentTime - lastFilledTime.get()) * count / 1000);
            }
        }
        return Math.min(newValue, maxToken);
    }

}

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
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115

# 排队等待

应用场景:秒杀系统

匀速排队,让请求以均匀的速度通过,阀值类型必须设成QPS,否则无效。

设置含义:/testA每秒1次请求,超过的话就排队等待,等待的超时时间为20000毫秒

sentinel-9

sentinel-10

# 降级规则

官方熔断降级说明 (opens new window)

# Sentinel 提供以下几种熔断策略🔎

RT(慢调用比例)

平均响应时间超出阈值且在时间窗口内通过的请求>=5,两个条件同时满足后触发降级窗口期过后关闭断路器

RT最大4900(更大的需要通过-Dcsp.sentinel.statistic.max.rt=XXXX才能生效)

异常比列(秒级)

​ QPS >= 5且异常比例(秒级统计)超过阈值时,触发降级;时间窗口结束后,关闭降级

异常数(分钟级)

​ 异常数(分钟统计)超过阈值时,触发降级;时间窗口结束后,关闭降级

sentinel-11

# 进一步说明🎗️

Sentinel熔断降级会在调用链路中某个资源出现不稳定状态时(例如调用超时或异常比例升高),对这个资源的调用进行限制,让请求快速失败,避免影响到其它的资源而导致级联错误。

当资源被降级后,在接下来的降级时间窗口之内,对该资源的调用都自动熔断(默认行为是抛出 DegradeException)

# Sentinel的断路器是没有半开状态的!!!

半开的状态系统自动去检测是否请求有异常,没有异常就关闭断路器恢复使用,有异常型继续打开断路器不可用。具体可以参考Hystrix。

# RT(慢调用比例)

sentinel-12

# 异常比例->异常数

异常数( DEGRADE_GRADE_EXCEPTION_COUNT ):

当资源近1分钟的异常数目超过阈值之后会进行熔断。注意由于统计时间窗口是分钟级别的,若timewindow 小于60s,则结束熔断状态后仍可能再进入熔断状态。

注意:时间窗口一定要大于等于60秒。

sentinel-13

代码示例

@GetMapping("/testE")
public String testE()
{
    log.info("testE 测试异常数");
    int age = 10/0;
    return "------testE 测试异常数";
}
1
2
3
4
5
6
7

sentinel配置

sentinel-14

访问,异常数超过5次就会提示

sentinel-15

# 热点规则⛑️

热点参数限流规则 (opens new window)

兜底方法

分为系统默认和客户自定义

之前的示例中,限流出问题后,都是用sentinel系统默认的提示: Blocked by Sentinel (flow limiting);

sentinel提供了 @sehtinelResource 注解实现兜底降级方法

/**
 * 流量限制控制器
 * @author LiJunYi
 */
@RestController
@Slf4j
public class FlowLimitController
{
    @GetMapping("/testHotKey")
    @SentinelResource(value = "testHotKey",blockHandler = "dealTestHotKey")
    public String testHotKey(@RequestParam(value = "p1",required = false) String p1,
                             @RequestParam(value = "p2",required = false) String p2)
    {
        //int age = 10/0;
        return "------testHotKey";
    }

    /**
     * 兜底方法
     *
     * @param p1        p1
     * @param p2        p2
     * @param exception 异常
     * @return {@link String}
     */
    public String dealTestHotKey(String p1, String p2, BlockException exception)
    {
        //sentinel系统默认的提示:Blocked by Sentinel (flow limiting)
        return "------dealTestHotKey,o(╥﹏╥)o";
    }

}

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

sentinel配置

sentinel-16

测试效果

sentinel-17

如果不配置 blockHandler ,页面则会直接显示error。

# 参数例外项🎇

上述案例演示了第一个参数p1,当QPS超过1秒1次点击后马上被限流

一般情况:

​ 超过1秒钟一个后,达到阈值1后马上被限流

特别情况:

​ 我们期望p1参数当它是某个特殊值时,它的限流值和平时不一样

​ 假如当p1的值等于5时,它的阈值可以达到200

sentinel配置

sentinel-18

!!! 前提条件:热点参数的注意点,参数必须是基本类型或者String

# 其他情况👓

sentinel-19

@sentinelResource

处理的是sentinel控制台配置的违规情况,有blockHandler方法配置的兜底处理;

RuntimeException

int age = 10/0,这个是java运行时报出的运行时异常RunTimeException,@SentinelResource不管

总结

@sentinelResource主管配置出错,运行出错该走异常走异常

# 系统规则🎱

系统自适应限流 (opens new window)