Sentinel实现熔断与限流

Mr.LR2022年7月12日
大约 14 分钟

Sentinel实现熔断与限流

类似于之前提到的Hystrix 官网资料:

除了流量控制以外,对调用链路中不稳定的资源进行熔断降级也是保障高可用的重要措施之一。一个服务常常会调用别的模块,可能是另外的一个远程服务、数据库,或者第三方 API 等。例如,支付的时候,可能需要远程调用银联提供的 API;查询某个商品的价格,可能需要进行数据库查询。然而,这个被依赖服务的稳定性是不能保证的。如果依赖的服务出现了不稳定的情况,请求的响应时间变长,那么调用服务的方法的响应时间也会变长,线程会产生堆积,最终可能耗尽业务自身的线程池,服务本身也变得不可用。

image-20220710173309424

现代微服务架构都是分布式的,由非常多的服务组成。不同服务之间相互调用,组成复杂的调用链路。以上的问题在链路调用中会产生放大的效果。复杂链路上的某一环不稳定,就可能会层层级联,最终导致整个链路都不可用。因此我们需要对不稳定的弱依赖服务调用进行熔断降级,暂时切断不稳定调用,避免局部不稳定因素导致整体的雪崩。熔断降级作为保护自身的手段,通常在客户端(调用端)进行配置。

Sentinel安装&演示

安装

官网下载

执行命令

java -jar sentinel-dashboard-1.8.4.jar 

访问:用户密码默认都是Sentinel

image-20220707221009559

演示工程

启动nacos

新建cloudalibaba-sentinel-service8401

pom

<?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">
    <parent>
        <artifactId>tudou_cloud</artifactId>
        <groupId>org.example</groupId>
        <version>1.0-SNAPSHOT</version>
    </parent>
    <modelVersion>4.0.0</modelVersion>

    <artifactId>cloudalibaba-sentinel-service8401</artifactId>

    <properties>
        <maven.compiler.source>8</maven.compiler.source>
        <maven.compiler.target>8</maven.compiler.target>
    </properties>
    <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>
        <!--日常通用jar包配置-->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-devtools</artifactId>
            <scope>runtime</scope>
            <optional>true</optional>
        </dependency>
        <dependency>
            <groupId>cn.hutool</groupId>
            <artifactId>hutool-all</artifactId>
            <version>4.6.3</version>
        </dependency>
        <dependency>
            <groupId>org.projectlombok</groupId>
            <artifactId>lombok</artifactId>
            <optional>true</optional>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-test</artifactId>
            <scope>test</scope>
        </dependency>

    </dependencies>

</project>

yml

server:
  port: 8401

spring:
  application:
    name: cloudalibaba-sentinel-service
  cloud:
    nacos:
      discovery:
        #Nacos服务注册中心地址
        server-addr: 192.168.83.133:8848
    sentinel:
      transport:
        #配置Sentinel dashboard地址
        dashboard: 192.168.83.133:8080
        #默认8719端口,假如被占用会自动从8719开始依次+1扫描,直至找到未被占用的端口
        port: 8719

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

主启动

/**
 * @Author LR
 * @Date 2022/7/6 22:50
 */
@EnableDiscoveryClient
@SpringBootApplication
public class MainApp8401 {
    public static void main(String[] args) {
        SpringApplication.run(MainApp8401.class, args);
    }
}

业务类

/**
 * @Author LR
 * @Date 2022/7/6 22:51
 */
@RestController
public class FlowLimitController {
    @GetMapping("/testA")
    public String testA() {
        return "------testA";
    }

    @GetMapping("/testB")
    public String testB() {
        return "------testB";
    }
}

测试

访问:http://localhost:8401/testAopen in new window

image-20220707222158723

流控规则

基本介绍

image-20220707223112735

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

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

  • 阈值类型单机阈值:

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

    • 线程数:当调用该pi的线程数达到阈值的时候,进行限流

  • 是否集群:不需要集群

  • 流控模式:

    • 直接:api达到限流涤件时,直接限流

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

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

  • 流控效果:

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

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

    • 排队等待:匀速排队,让请求以匀速的速度通过,阈值类型必须设置为QPS,否则无效

流控模式

直接

image-20220707224240411

表示1秒钟内查询1次就是OK,若超过次数1,就直接-快速失败,报默认错误

image-20220707224311596

关联

image-20220707224945667

当关联资源达到阈值就限制自己,即并发访问testB,限制访问testA。 这里用apifox模拟并发访问testB,然后再访问testA则会限制访问testA,等testB访问结束,访问testA恢复正常。

image-20220707231601626

image-20220707231622077

链路

流控效果

快速失败

直接失败,抛出异常 :Blocked by Sentinel (flow limiting)

预热

公式:阈值除以coldFactor(默认值为3),经过预热时长后才会达到阈值 即系统初始化阈值为12/3 = 4,然后过5秒后,慢慢恢复升高到12

image-20220707232948281

多次访问testA,刚开始受限制,后续慢慢可以访问

应用场景:秒杀系统在开启的瞬间,会有很多流量上来,很有可能把系统打死,预热方式就是把为了保护系统,可慢慢的把流量放进来,慢慢的把阀值增长到设置的阀值

排队等候(匀速排队)

匀速排队,阈值必须设置为QPS

匀速排队(RuleConstant.CONTROL_BEHAVIOR_RATE_LIMITER)方式会严格控制请求通过的间隔时间,也即是让请求以均匀的速度通过,对应的是漏桶算法。

该方式的作用如下图所示:

image-20220707233514511

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

注意:匀速排队模式暂时不支持 QPS > 1000 的场景。

@SentinelResource

之前的案例,限流出问题后,都是用sentinel系统默认的提示:Blocked by Sentinel (flow limiting),能否类似hystrix,某个方法出问题了,就找对应的降级方法?

因此引出@SentinelResource

按资源名称限流

修改服务cloudalibaba-sentinel-service8401

pom引入依赖

<dependency><!-- 引入自己定义的api通用包,可以使用Payment支付Entity -->
    <groupId>org.example</groupId>
    <artifactId>cloud-api-commons</artifactId>
    <version>${project.version}</version>
</dependency>

业务类

@RestController
public class RateLimitController {
    @GetMapping("/byResource")
    @SentinelResource(value = "byResource", blockHandler = "handleException")
    public CommonResult byResource() {
        return new CommonResult(200, "按资源名称限流测试OK", new Payment(2020L, "serial001"));
    }

    public CommonResult handleException(BlockException exception) {
        return new CommonResult(444, exception.getClass().getCanonicalName() + "\t 服务不可用");
    }
}

新增配置

image-20220710182538318

测试

快速访问:http://localhost:8401/byResourceopen in new window

image-20220710182749435

全局统一处理方法

上一个案例中,需要对访问资源byResource,定义出错时的方法,但是如果业务功能较多,每个访问请求都定义不太现实

创建CustomerBlockHandler类用于自定义限流处理逻辑

/**
 * @Author LR
 * @Date 2022/7/10 18:40
 */
public class CustomerBlockHandler {
    public static CommonResult handleExceptionMy(BlockException exception) {
        return new CommonResult(2020, "自定义的限流处理信息......CustomerBlockHandler");
    }
}

RateLimitController

@GetMapping("/byResourceAll")
@SentinelResource(value = "byResourceAll",
        blockHandlerClass = CustomerBlockHandler.class,//全局处理的类
        blockHandler = "handleExceptionMy")//自定义的限流处理信息
public CommonResult byResourceAll() {
    return new CommonResult(200, "按资源名称限流测试OK", new Payment(2020L, "serial001"));
}

配置sentinel

image-20220710184701023

测试:

访问:http://localhost:8401/byResourceAllopen in new window

image-20220710184718831

热点key限流

概述

何为热点?热点即经常访问的数据。很多时候我们希望统计某个热点数据中访问频次最高的 Top K 数据,并对其访问进行限制。比如:

  • 商品 ID 为参数,统计一段时间内最常购买的商品 ID 并进行限制
  • 用户 ID 为参数,针对一段时间内频繁访问的用户 ID 进行限制

热点参数限流会统计传入参数中的热点参数,并根据配置的限流阈值与模式,对包含热点参数的资源调用进行限流。热点参数限流可以看做是一种特殊的流量控制,仅对包含热点参数的资源调用生效。

image-20220710185435964

使用案例

业务代码

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

public String dealHandler_testHotKey(String p1, String p2, BlockException exception) {
    return "-----dealHandler_testHotKey";
}

配置sentinel

image-20220710185948374

测试:

参数例外配置项

可以当参数=某个值时,改变限流阈值

image-20220710191130967

当参数为5时,限流阈值为100

熔断降级(feign)

官网资料:

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

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

案例

新建服务端:cloudalibaba-provider-payment9004

pom

<?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">
    <parent>
        <artifactId>tudou_cloud</artifactId>
        <groupId>org.example</groupId>
        <version>1.0-SNAPSHOT</version>
    </parent>
    <modelVersion>4.0.0</modelVersion>

    <artifactId>cloudalibaba-provider-payment9004</artifactId>

    <properties>
        <maven.compiler.source>8</maven.compiler.source>
        <maven.compiler.target>8</maven.compiler.target>
    </properties>

    <dependencies>
        <!--SpringCloud ailibaba nacos -->
        <dependency>
            <groupId>com.alibaba.cloud</groupId>
            <artifactId>spring-cloud-starter-alibaba-nacos-discovery</artifactId>
        </dependency>
        <dependency><!-- 引入自己定义的api通用包,可以使用Payment支付Entity -->
            <groupId>org.example</groupId>
            <artifactId>cloud-api-commons</artifactId>
            <version>${project.version}</version>
        </dependency>
        <!-- SpringBoot整合Web组件 -->
        <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>
        <!--日常通用jar包配置-->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-devtools</artifactId>
            <scope>runtime</scope>
            <optional>true</optional>
        </dependency>
        <dependency>
            <groupId>org.projectlombok</groupId>
            <artifactId>lombok</artifactId>
            <optional>true</optional>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-test</artifactId>
            <scope>test</scope>
        </dependency>
    </dependencies>
</project>

yml

server:
  port: 9004

spring:
  application:
    name: nacos-payment-provider
  cloud:
    nacos:
      discovery:
        server-addr: 192.168.83.133:8848

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

主启动

/**
 * @Author LR
 * @Date 2022/7/11 21:23
 */
@SpringBootApplication
@EnableDiscoveryClient
public class PaymentMain9004 {
    public static void main(String[] args) {
        SpringApplication.run(PaymentMain9004.class, args);
    }
}

业务类

/**
 * @Author LR
 * @Date 2022/7/11 21:21
 */
@RestController
public class PaymentController {

    @Value("${server.port}")
    private String serverPort;

    public static HashMap<Long, Payment> hashMap = new HashMap<>();
    //模拟数据库的三条数据
    static {
        hashMap.put(1L, new Payment(1L, "123"));
        hashMap.put(2L, new Payment(2L, "123"));
        hashMap.put(3L, new Payment(3L, "123"));
    }

    @GetMapping(value = "/paymentSQL/{id}")
    public CommonResult<Payment> paymentSQL(@PathVariable("id") Long id) throws InterruptedException {
        TimeUnit.SECONDS.sleep(1);
        Payment payment = hashMap.get(id);
        CommonResult<Payment> result = new CommonResult(200, "from mysql,serverPort:  " + serverPort, payment);
        return result;
    }
}

新建客户端cloudalibaba-consumer-nacos-order84

pom

<?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">
    <parent>
        <artifactId>tudou_cloud</artifactId>
        <groupId>org.example</groupId>
        <version>1.0-SNAPSHOT</version>
    </parent>
    <modelVersion>4.0.0</modelVersion>

    <artifactId>cloudalibaba-consumer-nacos-order84</artifactId>

    <properties>
        <maven.compiler.source>8</maven.compiler.source>
        <maven.compiler.target>8</maven.compiler.target>
    </properties>
    <dependencies>
        <!--SpringCloud ailibaba nacos -->
        <dependency>
            <groupId>com.alibaba.cloud</groupId>
            <artifactId>spring-cloud-starter-alibaba-nacos-discovery</artifactId>
        </dependency>
        <!--SpringCloud ailibaba sentinel -->
        <dependency>
            <groupId>com.alibaba.cloud</groupId>
            <artifactId>spring-cloud-starter-alibaba-sentinel</artifactId>
        </dependency>
        <!--SpringCloud openfeign -->
        <dependency>
            <groupId>org.springframework.cloud</groupId>
            <artifactId>spring-cloud-starter-openfeign</artifactId>
        </dependency>
        <!-- 引入自己定义的api通用包,可以使用Payment支付Entity -->
        <dependency>
            <groupId>org.example</groupId>
            <artifactId>cloud-api-commons</artifactId>
            <version>${project.version}</version>
        </dependency>
        <!-- SpringBoot整合Web组件 -->
        <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>
        <!--日常通用jar包配置-->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-devtools</artifactId>
            <scope>runtime</scope>
            <optional>true</optional>
        </dependency>
        <dependency>
            <groupId>org.projectlombok</groupId>
            <artifactId>lombok</artifactId>
            <optional>true</optional>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-test</artifactId>
            <scope>test</scope>
        </dependency>
        <dependency>
            <groupId>org.example</groupId>
            <artifactId>cloud-api-commons</artifactId>
            <version>1.0-SNAPSHOT</version>
            <scope>compile</scope>
        </dependency>
    </dependencies>
</project>

yml

server:
  port: 84


spring:
  application:
    name: nacos-order-consumer
  cloud:
    nacos:
      discovery:
        server-addr: 192.168.83.133:8848
    sentinel:
      transport:
        #配置Sentinel dashboard地址
        dashboard: 192.168.83.133:8080
        #默认8719端口,假如被占用会自动从8719开始依次+1扫描,直至找到未被占用的端口
        port: 8719


#消费者将要去访问的微服务名称(注册成功进nacos的微服务提供者)
service-url:
  nacos-user-service: http://nacos-payment-provider

# 激活Sentinel对Feign的支持
feign:
  sentinel:
    enabled: true

主启动

/**
 * @Author LR
 * @Date 2022/7/11 21:43
 */
@EnableDiscoveryClient
@SpringBootApplication
@EnableFeignClients
public class OrderNacosMain84 {
    public static void main(String[] args) {
        SpringApplication.run(OrderNacosMain84.class, args);
    }
}

业务类

PaymentService

@FeignClient(value = "nacos-payment-provider",fallback = PaymentFallbackService.class)//触发降级调用的方法
public interface PaymentService {
    @GetMapping(value = "/paymentSQL/{id}")
    public CommonResult<Payment> paymentSQL(@PathVariable("id") Long id);
}
/**
 * @Author LR
 * @Date 2022/7/11 22:58
 */
@Component
public class PaymentFallbackService implements PaymentService {
    @Override
    public CommonResult<Payment> paymentSQL(Long id) {
        return new CommonResult<>(444, "服务降级返回", new Payment(id, "errorSerial......"));
    }
}

controller

/**
 * @Author LR
 * @Date 2022/7/11 21:45
 */
@RestController
public class CircleBreakerControllerFeign {

    @Resource
    private PaymentService paymentService;

    @GetMapping(value = "/consumer/openfeign/{id}")
    public CommonResult<Payment> paymentSQL(@PathVariable("id") Long id) {
        if (id == 4) {
            throw new RuntimeException("没有该id");
        }
        return paymentService.paymentSQL(id);
    }
}

演示

如果服务端没有启动,访问 http://localhost:84/consumer/openfeign/1open in new window

或者服务端运行报错,访问 http://localhost:84/consumer/openfeign/4open in new window

image-20220711232907087

image-20220712145152025

熔断规则

案例代码,这里仅配置服务端,如下代码使用restTemplate在客户端配置也可以

/**
 * @Author LR
 * @Date 2022/7/11 21:21
 */
@RestController
public class PaymentController {

    @Value("${server.port}")
    private String serverPort;

    public static HashMap<Long, Payment> hashMap = new HashMap<>();
    //模拟数据库的三条数据
    static {
        hashMap.put(1L, new Payment(1L, "123"));
        hashMap.put(2L, new Payment(2L, "123"));
        hashMap.put(3L, new Payment(3L, "123"));
    }

    @GetMapping(value = "/paymentSQL/{id}")
    @SentinelResource(value = "paymentSQL", fallback = "handlerFallback", //定义运行时异常,触发的方法
            blockHandler = "blockHandler")//定义熔断降级触发的方法
    public CommonResult<Payment> paymentSQL(@PathVariable("id") Long id) throws InterruptedException {
        TimeUnit.SECONDS.sleep(1);
        if (id==4){
            throw new RuntimeException("参数异常");
        }
        System.out.println(new Date());
        Payment payment = hashMap.get(id);
        CommonResult<Payment> result = new CommonResult(200, "from mysql,serverPort:  " + serverPort, payment);
        return result;
    }

    public CommonResult handlerFallback(@PathVariable Long id, Throwable e) {
        Payment payment = new Payment(id, "null");
        return new CommonResult<>(444, "fallback,自己运行异常 " + e.getMessage(), payment);
    }

    public CommonResult blockHandler(@PathVariable Long id, BlockException blockException) {
        Payment payment = new Payment(id, "null");
        return new CommonResult<>(445, "触发Sentinel配置的熔断规则,进行降级  " + blockException.getMessage(), payment);
    }

}

访问:http://localhost:9004/paymentSQL/4open in new window

触发自己定义的异常方法 fallback = "handlerFallback"

image-20220712153805986

慢调用规则

以下配置的含义为:如果1秒内持续进入大于等于4个请求,并且请求响应的时间大于200ms时(可以在服务端模拟运行了1秒),这个请求即为慢调用,当慢调用的比例大于1时会触发降级,直到5秒后新的请求的响应时间小于200ms时,才结束熔断。

image-20220711225153838

测试

用jmeter并发访问

image-20220712153936873

再访问:http://localhost:9004/paymentSQL/1open in new window 触发Sentinel配置的熔断规则,进行降级

image-20220712154139139

异常比例

以下配置的含义为:如果1秒内持续进入大于等于5个请求,并且请求中报异常的比例超过0.2则触发降级(降级时间持续5秒),5秒后,新的请求若正常返回,才结束熔断。

image-20220712155729535

测试:

这里需要先把自定义运行异常方法去掉

@GetMapping(value = "/paymentSQL/{id}")
@SentinelResource(value = "paymentSQL",
        //fallback = "handlerFallback", //定义运行时异常,触发的方法
        blockHandler = "blockHandler")//定义熔断降级触发的方法
public CommonResult<Payment> paymentSQL(@PathVariable("id") Long id) throws InterruptedException {
    //TimeUnit.SECONDS.sleep(1);
    if (id==4){
        throw new RuntimeException("参数异常");
    }
    System.out.println(new Date());
    Payment payment = hashMap.get(id);
    CommonResult<Payment> result = new CommonResult(200, "from mysql,serverPort:  " + serverPort, payment);
    return result;
}

访问:http://localhost:9004/paymentSQL/4open in new window 首先会出现参数异常错误,连续点击几次,触发降级 即使访问正确的参数也会触发降级,等5秒结束,结束熔断

image-20220712160005784

image-20220712160634406

异常数

以下配置的含义为:如果1秒内持续进入大于等于5个请求,并且请求异常数超过5时,会触发降级(降级时间持续5秒),5秒后,新的请求若正常返回,才结束熔断。

image-20220712160757858

测试效果:同异常比例

参考

上次编辑于: 2022/7/12 23:00:21
贡献者: liurui_60837