15. SpringCloudAlibaba Sentinel实现熔断与限流

1,366 阅读12分钟

1.sentinel概述

1.1 官网

https://github.com/alibaba/Sentinel

1.2 是什么


一句话解释,之前我们用过的Hystrix

1.3 去哪下

https://github.com/alibaba/Sentinel/releases

1.4 能干嘛

1.5 怎么玩

https://spring-cloud-alibaba-group.github.io/github-pages/greenwich/spring-cloud-alibaba.html#_spring_cloud_alibaba_sentinel

服务使用中的各种问题:

  • 服务雪崩
  • 服务降级
  • 服务熔断
  • 服务限流

2. 安装Sentinel控制台

2.1 Sentinel分为两个部分

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

2.2 安装步骤

2.2.1 运行命令

java -jar sentinel-dashboard-1.7.1.jar 前提是(java8环境OK 8080端口不能被占用)

2.2.2 访问sentinel 管理界面

http://localhost:8080/#/login
登录账号密码均为sentinel

3. 初始化演示工程

3.1 启动Nacos8848成功

3.2 新建模块 cloudalibaba-sentinel-service8401

pom文件

<dependencies>
    <!-- nacos-discovery-->
    <dependency>
        <groupId>com.alibaba.cloud</groupId>
        <artifactId>spring-cloud-starter-alibaba-nacos-discovery</artifactId>
    </dependency>
    <!-- nacos持久化-->
    <dependency>
        <groupId>com.alibaba.cloud</groupId>
        <artifactId>sentinel-datasource-nacos</artifactId>
    </dependency>
    <!-- SpringCloud alilibaba sentinel-->
    <dependency>
        <groupId>com.alibaba.cloud</groupId>
        <artifactId>spring-cloud-starter-alibaba-sentinel</artifactId>
    </dependency>
    <!-- 引入自己定义的api通用包-->
    <dependency>
        <groupId>com.jd.springcloud</groupId>
        <artifactId>cloud-api-commons</artifactId>
        <version>${project.version}</version>
    </dependency>
    <!-- boot 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>
    <!-- devtools-->
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-devtools</artifactId>
        <scope>runtime</scope>
        <optional>true</optional>
    </dependency>
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-test</artifactId>
        <scope>test</scope>
    </dependency>
</dependencies>

yml文件

server:
  port: 8401

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

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

主启动

@EnableDiscoveryClient
@SpringBootApplication
public class MainApp8401 {

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

业务类 FlowLimitController

@RestController
public class FlowLimitController {

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

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

3.3 启动sentinel8080

java -jar sentinel-dashboard-1.7.1.jar

3.4 启动微服务8401

3.5 查看sentinel控制台


空空如也,啥都没有

3.5.1 sentinel采用的懒加载说明

需要先执行一次访问

  • http://localhost:8401/testA
3.5.2 效果

4. 流控规则

4.1 基本介绍

4.2 流控模式

4.2.1 直接(默认)

直接-> 快速失败
配置及说明


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

测试
快速点击访问 http://localhost:8401/testA

结果

4.2.2 关联

是什么

  • 当关联的资源达到阈值时,就限流自己
  • 当与A关联的资源B达到阈值后,就限流A自己

配置A

当关联资源/testB的qps阀值超过1时,就限流/testA的Rest访问地址,当关联资源到阈值后限制配置好的资源名

postman模拟并发密集访问testB

(1) 访问testB成功


(2) postman里新建多线程集合组

(3) 将访问地址添加进新新线路组


大批量线程高并发访问B,导致testA挂了

4.3 流控效果

4.3.1 直接-> 快速失败(默认的流控处理)
4.3.2 预热

说明
公式: 阈值除以coldFactor(默认值为3),即请求 QPS从threshold/3开始,经过预热时长逐渐升至设定的QPS阈值
warmUp配置


多次点击 http://localhost:8401/testB 刚开始不行,后续慢慢OK

应用场景

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

4.3.3 排队等待

官网介绍


测试

5. 降级规则

5.1 官网

https://github.com/alibaba/Sentinel/wiki/%E7%86%94%E6%96%AD%E9%99%8D%E7%BA%A7

5.2 基本介绍

Sentinel 熔断降级会在调用链路中某个资源出现不稳定状态时(例如调用超时或异常比例升高),对这个资源的调用进行限制,让请求快速失败,避免影响到其它的资源而导致级联错误。当资源被降级后,在接下来的降级时间窗口之内,对该资源的调用都自动熔断(默认行为是抛出 DegradeException)。

降级策略
我们通常用以下几种方式来衡量资源是否处于稳定的状态:
平均响应时间 (DEGRADE_GRADE_RT):当 1s 内持续进入 5 个请求,对应时刻的平均响应时间(秒级)均超过阈值(count,以 ms 为单位),那么在接下的时间窗口(DegradeRule 中的 timeWindow,以 s为单位)之内,对这个方法的调用都会自动地熔断(抛出 DegradeException)。注意 Sentinel 默认统计的 RT 上限是4900 ms,超出此阈值的都会算作 4900ms,若需要变更此上限可以通过启动配置项 -Dcsp.sentinel.statistic.max.rt=xxx 来配置。

异常比例 (DEGRADE_GRADE_EXCEPTION_RATIO):当资源的每秒请求量 >= 5,并且每秒异常总数占通过量的比值超过阈值(DegradeRule 中的 count)之后,资源进入降级状态,即在接下的时间窗口(DegradeRule 中的 timeWindow,以 s 为单位)之内,对这个方法的调用都会自动地返回。异常比率的阈值范围是 [0.01.0],代表 0% - 100%。

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

sentinel断路器是没有半开状态的

5.3降级策略实战

5.3.1 RT 测试

(1)代码

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

(2)配置


(3)jmeter压测


(4)结论


按照上述配置
永远一秒钟打进来10个线程(大于5个了) 调用testD, 我们希望200毫秒处理完本次任务,如果超过200毫秒还没处理完,在未来1秒钟的时间窗口内,断路器打开(保险丝跳闸)微服务不可用,保险丝跳闸断电了后续我停止jmeter,没有这么大的访问量了,断路器关闭(保险丝恢复),微服务恢复OK

5.3.2 异常比例

(1)代码

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

(2)配置


(3)jmeter压测


(4)结论
按照上述配置,单独访问一次,必然来一次报错一次(int age=10/0),调一次错一次;


开启jmeter后,直接高并发发送请求,多次调用达到我们的配置条件了。断路器开启(保险丝跳闸),微服务
不可用了,不再报错error而是直接服务降级了

5.3.3 异常数测试

异常数是按照分钟统计的
(1) 代码

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

(2) 配置


(3)结论

http://localhost:8401/testE
第一次访问绝对报错,因为除数不能为零,我们看到error窗口,但是达到5次报错后,进入熔断后降级。

6. 热点key限流

6.1 基本介绍

6.2 官网

6.3 代码

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

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

6.4 配置

6.5 测试

访问 http://localhost:8401/testHotKey?p1=a
http://localhost:8401/testHotKey?p1=a&p2=3

@SentinelResource(value="testHotKey",blockHandler = "deal_testHotKey")
方法testHotKey里面第一个参数只要QPS超过每秒1次,马上降级处理

6.6 参数例外项

特例情况:
期望p1参数当它是某个特殊值时,它的限流值和平时不一样
例如:当p1的值等于5时,它的阈值可达到300
配置如下:


测试:
http://localhost:8401/testHotKey?p1=5
当p1=5的时候,阈值变为300

如果加个异常代码会怎么样

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

@SentinelResource
处理的是Sentinel控制台配置的违规情况,有blockHandler方法配置的兜底处理
但是 int age = 10/0,这个是 java运行时报出的异常,@SentinelResource不管

7.系统规则

7.1 是什么

7.2 各项配置参数说明

7.3 配置全局QPS

8. @SentinelResource

8.1 按资源名称限流+后续处理

8.1.1 成功启动Nacos 和sentinel
8.1.2 修改cloudalibaba-sentinel-service8401模块

pom添加

<!-- 引入自己定义的api通用包-->
<dependency>
    <groupId>com.jd.springcloud</groupId>
    <artifactId>cloud-api-commons</artifactId>
    <version>${project.version}</version>
</dependency>

新增业务类 RateLimitController

@RestController
public class RateLimitController {

    @GetMapping("/byResource")
    @SentinelResource(value="byResource",blockHandler = "handleException")
    public CommonResult byResource(){
        return new CommonResult(200,"",new Payment(2020L,"serial001"));
    }

    public CommonResult handleException(BlockException exception){
        return new CommonResult(444,exception.getClass().getCanonicalName()+"\t 服务不可用");
    }
}
8.1.3 配置流控规则
8.1.4 测试

1秒钟点击一下,ok,超过上述疯狂点击,返回自定义的限流处理信息,限流发生

8.2 按url地址限流+后续处理

通过访问的URL 来限流,会返回sentinel自带默认的限流处理信息
业务类RateLimitController

@GetMapping("/rateLimit/byUrl")
@SentinelResource(value="byUrl")
public CommonResult byUrl(){
    return new CommonResult(200"按url限流测试OK",new Payment(2020L,"serial002"));
}

配置

8.3 上面兜底方案面临的问题

  • 系统默认的,没有体现我们自己的业务要求
  • 依照现有条件,我们自定义的处理方法又和业务代码耦合在一起,不直观
  • 每个业务方法都添加一个兜底的,那代码膨胀加剧
  • 全局统一的处理方法没有体现

8.4 客户自定义限流处理逻辑

  • 创建CustomerBlockHandler类用于自定义限流逻辑
  • RateLimitController类添加逻辑
@GetMapping("/rateLimit/customerBlockHandler")
@SentinelResource(value = "customerBlockHandler",
        blockHandlerClass = CustomerBlockHandler.class,blockHandler = "handlerException2")
public CommonResult customerBlockHandler(){
    return new CommonResult(200"按客户自定义",new Payment(2020L,"serial003"));
}
  • sentinel控制台配置
  • 测试后我们自定义的出来了
  • 进一步说明

8.5 更多注解属性说明

sentinel主要有三个核心 Api,SphU定义资源, Tracer定义统计, ContextUtil定义了上下文

9. 服务熔断功能

sentinel整合ribbon+openFeign+fallback

9.1 Ribbon系列

9.1.1 启动nacos和sentinel
9.1.2 创建cloudalibaba-provider-payment9003/9004

PaymentController类

@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,"28a2181"));
        hashMap.put(2L,new Payment(2L,"bba2182"));
        hashMap.put(3L,new Payment(3L,"6ua2183"));
    }

    @GetMapping(value="/paymentSQL/{id}")
    public CommonResult<Payment> paymentSQL(@PathVariable("id") Long id){
        Payment payment = hashMap.get(id);
        CommonResult result = new CommonResult(200"from mysql, serverPort:" + serverPort, payment);
        return result;
    }
}
9.1.3 创建cloudalibaba-consumer-nacos-order84

yml文件

server:
  port: 84

spring:
  application:
    name: nacos-order-consumer
  cloud:
    nacos:
      discovery:
         server-addr: localhost:8848

    sentinel:
      transport:
        #配置sentinel dashboard
        dashboard: localhost:8080
        #默认8719端口,假如被占用会自动从8719开始依次+1扫描,直至找到未被占用的端口
        port: 8719

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

config

package com.jd.springcloud.alibaba.config;

import org.springframework.cloud.client.loadbalancer.LoadBalanced;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.client.RestTemplate;

@Configuration
public class ApplicationContextConfig {

    @Bean
    @LoadBalanced
    public RestTemplate getRestTemplate(){
        return new RestTemplate();
    }
}

controller

package com.jd.springcloud.alibaba.controller;

import com.alibaba.csp.sentinel.annotation.SentinelResource;
import com.jd.springcloud.alibaba.entities.CommonResult;
import com.jd.springcloud.alibaba.entities.Payment;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import org.springframework.web.client.RestTemplate;
import javax.annotation.Resource;


@RestController
public class CircleBreakerController {

    public static final String SERVICE_URL="http://nacos-payment-provider";

    @Resource
    private RestTemplate restTemplate;

    @RequestMapping("/consumer/fallback/{id}")
    @SentinelResource(value="fallback"
    public CommonResult<Payment> fallback(@PathVariable Long id){
        CommonResult result = restTemplate.getForObject(SERVICE_URL + "/paymentSQL/" + id, CommonResult.class, id);
        if(id == 4){
            throw new IllegalArgumentException("IllegalArgumentException, 非法参数异常");
        } else if(result.getData() == null){
            throw new NullPointerException("NullPointerException, 该ID 没有对应记录,空指针异常");
        }
        return result;
    }
}

只配置fallback
CircleBreakerController添加如下代码

@RestController
public class CircleBreakerController {

    public static final String SERVICE_URL="http://nacos-payment-provider";

    @Resource
    private RestTemplate restTemplate;

    @RequestMapping("/consumer/fallback/{id}")
    @SentinelResource(value="fallback",fallback = "handlerFallback"//fallback  只负责业务异常
    public CommonResult<Payment> fallback(@PathVariable Long id){
        CommonResult result = restTemplate.getForObject(SERVICE_URL + "/paymentSQL/" + id, CommonResult.class, id);
        if(id == 4){
            throw new IllegalArgumentException("IllegalArgumentException, 非法参数异常");
        } else if(result.getData() == null){
            throw new NullPointerException("NullPointerException, 该ID 没有对应记录,空指针异常");
        }
        return result;
    }
    //本例是fallback
    public CommonResult handlerFallback(@PathVariable Long id, Throwable e){
        Payment payment = new Payment(id,"null");
        return new CommonResult(444,"兜底异常handlerFallback,exception内容"+e.getMessage(),payment);
    }

}

只配置 blockHandler

package com.jd.springcloud.alibaba.controller;

import com.alibaba.csp.sentinel.annotation.SentinelResource;
import com.alibaba.csp.sentinel.slots.block.BlockException;
import com.jd.springcloud.alibaba.entities.CommonResult;
import com.jd.springcloud.alibaba.entities.Payment;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import org.springframework.web.client.RestTemplate;
import javax.annotation.Resource;


@RestController
public class CircleBreakerController {

    public static final String SERVICE_URL="http://nacos-payment-provider";

    @Resource
    private RestTemplate restTemplate;

    @RequestMapping("/consumer/fallback/{id}")
    @SentinelResource(value = "fallback",blockHandler = "blockHandler")
    public CommonResult<Payment> fallback(@PathVariable Long id){
        CommonResult result = restTemplate.getForObject(SERVICE_URL + "/paymentSQL/" + id, CommonResult.class, id);
        if(id == 4){
            throw new IllegalArgumentException("IllegalArgumentException, 非法参数异常");
        } else if(result.getData() == null){
            throw new NullPointerException("NullPointerException, 该ID 没有对应记录,空指针异常");
        }
        return result;
    }

    //本例是blockHandler
    public CommonResult blockHandler(@PathVariable Long id, BlockException blockException){
        Payment payment = new Payment(id,"null");
        return new CommonResult(445"blockHandler-sentinel限流,无此流水"+blockException.getMessage());
    }
}

1秒一次报运行时异常


快快的点,走服务降级


blockHandler和fallback都配置

package com.jd.springcloud.alibaba.controller;

import com.alibaba.csp.sentinel.annotation.SentinelResource;
import com.alibaba.csp.sentinel.slots.block.BlockException;
import com.jd.springcloud.alibaba.entities.CommonResult;
import com.jd.springcloud.alibaba.entities.Payment;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import org.springframework.web.client.RestTemplate;
import javax.annotation.Resource;


@RestController
public class CircleBreakerController {

    public static final String SERVICE_URL="http://nacos-payment-provider";

    @Resource
    private RestTemplate restTemplate;

    @RequestMapping("/consumer/fallback/{id}")
    //@SentinelResource(value="fallback",fallback = "handlerFallback") //fallback  只负责业务异常
    //@SentinelResource(value = "fallback",blockHandler = "blockHandler")
    @SentinelResource(value="fallback",fallback = "handlerFallback",blockHandler = "blockHandler")
    public CommonResult<Payment> fallback(@PathVariable Long id){
        CommonResult result = restTemplate.getForObject(SERVICE_URL + "/paymentSQL/" + id, CommonResult.class, id);
        if(id == 4){
            throw new IllegalArgumentException("IllegalArgumentException, 非法参数异常");
        } else if(result.getData() == null){
            throw new NullPointerException("NullPointerException, 该ID 没有对应记录,空指针异常");
        }
        return result;
    }
    //本例是fallback
    public CommonResult handlerFallback(@PathVariable Long id, Throwable e){
        Payment payment = new Payment(id,"null");
        return new CommonResult(444,"兜底异常handlerFallback,exception内容"+e.getMessage(),payment);
    }

    //本例是blockHandler
    public CommonResult blockHandler(@PathVariable Long id, BlockException blockException){
        Payment payment = new Payment(id,"null");
        return new CommonResult(445"blockHandler-sentinel限流,无此流水"+blockException.getMessage());
    }
}

若blockHandler和fallback都进行了配置,则被限流降级而抛出BlockException时只会进入blockHandler处理逻辑

9.2 Feign系列

修改cloudalibaba-consumer-nacos-order84

pom文件添加 feign依赖

<!-- SpringCloud openfeign-->
<dependency>
    <groupId>org.springframework.cloud</groupId>
    <artifactId>spring-cloud-starter-openfeign</artifactId>
</dependency>

yml 文件

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

主启动类

@EnableDiscoveryClient
@SpringBootApplication
@EnableFeignClients
public class OrderNacosMain84 {

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

业务类
带@FeignClient注解的业务接口

@FeignClient(value="nacos-payment-provider",fallback = PaymentFallbackService.class)
public interface PaymentService {

    @GetMapping(value="/paymentSQL/{id}")
    public CommonResult<Payment> paymentSQL(@PathVariable("id") Long id);

}

@Component
public class PaymentFallbackService implements  PaymentService{

    @Override
    public CommonResult<Payment> paymentSQL(Long id) {
        return new CommonResult<>(444"服务降级返回,---PaymentFallbackService",new Payment(id,"errorSerial"));
    }
}

controller 添加

//==============OpenFeign
@Resource
private PaymentService paymentService;

@GetMapping(value="/consumer/paymentSQL/{id}")
public CommonResult<Payment> paymentSQL(@PathVariable("id") Long id){
    return paymentService.paymentSQL(id);
}

访问

http://localhost:84/consumer/paymentSQL/1
测试84 调用9003,此时故意关闭9003微服务提供者,看84消费侧自动降级


熔断框架比较

10. 规则持久化

10.1 是什么

一旦我们重启应用,sentinel规则将消失,生产环境需要将配置规则进行持久化

10.2 怎么玩

将限流配置规则持久化进Nacos保存,只要刷新8401某个rest地址,sentinel控制台的流控规则就能看到,只要Nacos里面的配置不删除,针对8401上sentinel上的流控规则持续有效

10.3 步骤

10.3.1 修改cloudalibaba-sentinel-service8401

pom添加持久化依赖

<!-- nacos持久化-->
<dependency>
    <groupId>com.alibaba.csp</groupId>
    <artifactId>sentinel-datasource-nacos</artifactId>
</dependency>

yml文件添加Nacos数据源配置

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

添加Nacos业务规则配置

  • resource: 资源名称
  • limitApp: 来源应用
  • grade: 阈值类型,0表示线程数,1表示QPS
  • count: 单机阈值
  • strategy: 流控模式,0表示直接,1表示关联 2表示链路
  • controlBehavior: 流控效果,0表示快速失败,1表示WarmUp, 2表示排队等待
  • clusterMode: 是否集群
10.3.2 启动8401后刷新sentinel发现业务规则有了
10.3.3 快速访问测试接口

http://localhost:8401/rateLimit/byUrl

10.3.4 停止8401再看sentinel
10.3.5 重新启动8401再看sentinel

乍一看还是没有,稍等一会 多次调用 http://localhost:8401/rateLimit/byUrl

重新配置出现了,持久化验证通过