Spring Cloud笔记-Spring Cloud Alibaba Sentinel实现熔断与限流(十九)

1.Sentinel

Sentinel的GitHub地址:https://github.com/alibaba/Sentinel

它的作用和Hystrix非常类似,但是使用起来比Hystrix更加方便。Hystrix需要程序员手工搭建监控平台,而且没有一套Web界面实现更细粒度的配置,所以还是有一定局限性的。Sentinel自称是分布式系统的流量防卫兵,它是一个单独的组件,可以独立出来,提供了界面化的细粒度的统一配置。

Sentinel 具有以下特征:

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

下载地址:https://github.com/alibaba/Sentinel/releases

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

2.安装Sentinel控制台

Sentinel由前台和后台两部分构成。后台是核心库,能够运行于所有Java运行时环境,同时对Dubbo、Spring Cloud等框架有较好的支持,前台是基于Spring Boot开发的,打包后即可直接运行,不需要额外的Tomcat容器。

1.Windows安装(建议)

这里下载对应版本后,其实就是一个jar包,使用java -jar命令运行即可,默认使用的是8080端口,浏览器访问http://localhost:8080,输入账户密码即可看到界面,用户名和密码都是sentinel。

2.Linux安装(Docker镜像不是官方的,可能有bug)

这里依旧采用Docker的方式,Docker里的端口是8858,浏览器访问http://192.168.0.123:8858/,输入用户名密码,都是sentinel,即可看到界面了,界面左侧有规则列表。

[root@bogon ~]# docker pull bladex/sentinel-dashboard:1.7.2
Trying to pull repository docker.io/bladex/sentinel-dashboard ...
1.7.2: Pulling from docker.io/bladex/sentinel-dashboard
169185f82c45: Pull complete
4346af5b5a4f: Pull complete
145353319704: Pull complete
a6b160c30643: Pull complete
Digest: sha256:e525dd34128508242f4ad96d96721900eba617d744af7f2164b43c720db0cbe0
Status: Downloaded newer image for docker.io/bladex/sentinel-dashboard:1.7.2
[root@bogon ~]# docker run -d -p 8858:8858 bladex/sentinel-dashboard:1.7.2
d350b3c3eef1d31d62dd0ad672cce34ce1812b3e77ee021a02b7f5e2d5e6235e
[root@bogon ~]# docker ps
CONTAINER ID        IMAGE                             COMMAND                  CREATED             STATUS              PORTS                              NAMES
d350b3c3eef1        bladex/sentinel-dashboard:1.7.2   "java -Djava.secur..."   5 seconds ago       Up 4 seconds        8719/tcp, 0.0.0.0:8858->8858/tcp   zen_knuth

3.初始化演示工程

新建一个cloudalibaba-sentinel-service8401模块,修改pom.xml,加入sentinel相关的坐标,加入nacos相关的坐标,将这个模块注册进Nacos,并且使用Sentinel监控这个模块。

<?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>cloud2020</artifactId>
        <groupId>com.atguigu.springcloud</groupId>
        <version>1.0-SNAPSHOT</version>
    </parent>
    <modelVersion>4.0.0</modelVersion>
    <artifactId>cloudalibaba-sentinel-service8401</artifactId>
    <dependencies>
        <dependency>
            <groupId>com.alibaba.cloud</groupId>
            <artifactId>spring-cloud-starter-alibaba-nacos-discovery</artifactId>
        </dependency>
        <!-- Spring Cloud Ailibaba sentinel-datasource-nacos持久化需要用到 -->
        <dependency>
            <groupId>com.alibaba.csp</groupId>
            <artifactId>sentinel-datasource-nacos</artifactId>
        </dependency>
        <dependency>
            <groupId>com.alibaba.cloud</groupId>
            <artifactId>spring-cloud-starter-alibaba-sentinel</artifactId>
        </dependency>
        <dependency>
            <groupId>org.springframework.cloud</groupId>
            <artifactId>spring-cloud-starter-openfeign</artifactId>
        </dependency>
        <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>
        <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>

添加application.yml。

server:
  port: 8401
spring:
  application:
    name: cloudalibaba-sentinal-service
  cloud:
    nacos:
      discovery:
        # Nacos服务注册中心地址
        server-addr: 192.168.0.123:8848
    sentinel:
      transport:
        # 配置Sentinel dashboard地址
        # dashboard: 127.0.0.1:8080 # Windows安装Sentinel,Dashboard的地址
        dashboard: 192.168.0.123:8858 # Linux里Docker运行Sentinel,Dashboard的地址
        # 应用程序与Sentinel进行交互的端口,用于传输数据,如果端口被占用,Sentinel会尝试采用端口+1,直到找到可用端口
        port: 8719
      eager: true # 取消延迟加载(默认是延迟加载的)
management:
  endpoints:
    web:
      exposure:
        include: '*'

添加主启动类(可能会有问题,下面说明),带上@EnableDiscoveryClient注解。

package com.atguigu.springcloud.alibaba;

import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.cloud.client.discovery.EnableDiscoveryClient;

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

添加业务类,用于测试。

package com.atguigu.springcloud.alibaba.controller;

import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RestController;

@RestController
public class FlowLimitController {
    @GetMapping("/testA")
    public String testA() {
        System.out.println(Thread.currentThread().getName());
        // 测试线程数时候,开启
        // try {
        //     Thread.sleep(500);
        // } catch (InterruptedException e) {
        //     e.printStackTrace();
        // }
        return "----testA";
    }

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

把Nacos以单机的方式启动起来,Sentinel的Dashboard也启动起来,再启动cloudalibaba-sentinel-service8401模块。浏览器访问http://192.168.0.123:8858/,默认情况下,Sentinel是懒加载的,必须发送一个请求,才能在“实时监控”中监控到,当然,也可以指定spring.cloud.sentinel.eager=true取消懒加载。

不过这里碰到了坑(这里建议使用Windows安装),Sentinel识别不到cloudalibaba-sentinel-service8401的请求,在“实时监控”里,也没有请求对应的QPS图像,看了下Sentinel的日志,有一大堆如下错误:

2020-06-29 15:51:45.938 ERROR 1 --- [pool-2-thread-1] c.a.c.s.dashboard.metric.MetricFetcher   : Failed to fetch metric from <http://192.168.122.1:8719/metric?startTime=1593445770000&endTime=1593445776000&refetch=false> (ConnectionException: Connection timed out)

具体原因可以看下:https://github.com/alibaba/Sentinel/issues/1361。当主机有多个网络适配器的时候,Sentinel会识别出错。上面报错信息的ip地址是VMware Network Adapter VMnet8的IP,它的IP和Sentinel不在一个网段,所以在fetch的时候超时了。后来找到一个方法,在主启动类的main()方法里,加一个行语句,告知Sentinel服务的IP地址,再次尝试,Sentinel就可以识别到请求了,看一下效果图。

package com.atguigu.springcloud.alibaba;

import com.alibaba.csp.sentinel.transport.config.TransportConfig;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.cloud.client.discovery.EnableDiscoveryClient;

@SpringBootApplication
@EnableDiscoveryClient
public class SentinelMain8401 {
    public static void main(String[] args) {
        // Linux下Docker模式运行Sentinel时使用下面这行代码,Windows下运行Sentinel不要使用
        System.setProperty(TransportConfig.HEARTBEAT_CLIENT_IP, "192.168.0.105");
        // 下面这种方式好像不行
        // System.setProperty(TransportConfig.HEARTBEAT_CLIENT_IP, IPUtils.getOutIPV4());
        SpringApplication.run(SentinelMain8401.class, args);
    }
}

另外,注意一下,如果长时间不访问,实时监控的信息会自动消失,直到下次请求才会继续出现。

4.流控规则

访问Sentinel控制台,点击左侧“流控规则”,再点击右上角的“新建流控规则”,可以看到一些选项,官网地址:https://github.com/alibaba/Sentinel/wiki/%E6%B5%81%E9%87%8F%E6%8E%A7%E5%88%B6

  • 资源名:唯一名称,默认请求路径
  • 针对来源: Sentinel可以针对调用者进行限流,填写微服务名,默认default(不区分来源)
  • 阈值类型/单机阈值:
  1. QPS(每秒请求数):当调用该API的QPS达到阈值时,进行限流
  2. 线程数:当调用该API的线程数达到阈值时,进行限流
  • 是否集群:不需要集群
  • 流控模式:
  1. 直接:API到达限流条件时,直接限流
  2. 关联:关联的资源达到限流时,限流自己
  3. 链路:记录指定链路上的流量(指定入口资源进来的流量,如果达到阈值,就进行限流,API级别的针对来源)
  • 流控效果:
  1. 快速失败:直接失败,抛出异常
  2. Warm Up:根据coldFactor(冷加载因子,默认值是3)的值,从阈值/coldFactor开始,经过预热时长,达到设置的QPS值
  3. 排队等待:匀速排队,让请求以匀速的速度进行访问,阈值类型必须是QPS,否则无效

流控模式

直接

每秒钟最多请求一次,超过1次,返回快速失败(浏览器看到Blocked by Sentinel (flow limiting)信息),通过浏览器频繁请求http://localhost:8401/testA,即可看到效果。

线程数的测试,手工模拟不出来(可能是手速慢了),那就用JMeter吧,记得开启/testA请求的Thread.sleep()方法,否则看不出来效果,用JMeter测试时候也不是完全正确,不知道为什么。

HTTP请求发给http://localhost:8401/testA,1秒钟启动10个线程,添加结果树查看返回结果。

关联

下面的配置,意思是说当访问/testB超过阈值,对/testA进行限流,应用场景:比如下游服务每秒钟只能处理5个请求,此时不希望上游发来更快的请求。

查看效果的话,需要用到JMeter给/testB发送超过阈值的高频请求,我们浏览器访问/testA,发现此时/testA直接返回了失败信息,说明配置的关联快速失败生效了。

链路

为了验证链路规则,我们需要添加一个资源类。

package com.atguigu.springcloud.alibaba.service;

import com.alibaba.csp.sentinel.annotation.SentinelResource;
import com.alibaba.csp.sentinel.slots.block.BlockException;
import org.springframework.stereotype.Service;

import java.util.UUID;

@Service
public class ResourceService {
    // 这里说几个注意的点:
    // Sentinel Dashboard中,资源名必须和@SentinelResource注解中的value保持一致
    // value:指定资源名,blockHandler:指定降级方法(注意降级方法和目标方法的返回值要一致),方法名一致这个就不用多说了
    @SentinelResource(value = "getResource", blockHandler = "blockHandler")
    public String getResource() {
        return UUID.randomUUID().toString();
    }

    // BlockException一定要带上,否则不进这个方法
    public String blockHandler(BlockException e) {
        e.printStackTrace();
        return "getResource()不可用,请稍后再试";
    }
}

假设/testA和/testB请求都会调用getResource()方法,那么这两个请求就是竞争关系,当某个请求(假设是/testA请求)调用频率高的时候,必定会影响另一个请求获取资源,此时,我们就可以对/testA添加链路模式的流控,限制/testA的高频请求,此时/testB请求是不受影响的。修改/testA和/testB,让其调用getResource()方法。

package com.atguigu.springcloud.alibaba.controller;

import com.atguigu.springcloud.alibaba.service.ResourceService;
import com.atguigu.springcloud.entities.CommonResult;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RestController;

import javax.annotation.Resource;

@RestController
public class FlowLimitController {
    @Resource
    private ResourceService resourceService;

    @GetMapping("/testA")
    public String testA() {
        // 测试线程数时候,开启
        // System.out.println(Thread.currentThread().getName());
        // try {
        //     Thread.sleep(500);
        // } catch (InterruptedException e) {
        //     e.printStackTrace();
        // }
        // 测试链路模式时候,开启
        String resource = resourceService.getResource();
        return "----testA----" + resource;
    }

    @GetMapping("/testB")
    public String testB() {
        // 测试链路模式时候,开启
        String resource = resourceService.getResource();
        return "----testB----" + resource;
    }
}

在Sentinel Dashboard中加入流控规则(入口资源/testA,别把/漏掉了,我一开始就漏掉了),使用JMeter进行测试。

根据上面的测试说明,发现测试没效果,具体原因参考这里:https://github.com/alibaba/Sentinel/issues/1213

解决方案:修改Spring Cloud Alibaba版本号为2.1.1.RELEASE,在8401模块application.yml中配置spring.cloud.sentinel.filter.enabled: false关闭自动收敛,添加FilterContextConfig配置类。

package com.atguigu.springcloud.alibaba.config;

import com.alibaba.csp.sentinel.adapter.servlet.CommonFilter;
import org.springframework.boot.web.servlet.FilterRegistrationBean;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;

@Configuration
public class FilterContextConfig {
    /**
     * @NOTE 在spring-cloud-alibaba v2.1.1.RELEASE及前,sentinel1.7.0及后,关闭URL PATH聚合需要通过该方式,spring-cloud-alibaba v2.1.1.RELEASE后,可以通过配置关闭:spring.cloud.sentinel.web-context-unify=false
     * 手动注入Sentinel的过滤器,关闭Sentinel注入CommonFilter实例,修改配置文件中的 spring.cloud.sentinel.filter.enabled=false
     * 入口资源聚合问题:https://github.com/alibaba/Sentinel/issues/1024 或 https://github.com/alibaba/Sentinel/issues/1213
     * 入口资源聚合问题解决:https://github.com/alibaba/Sentinel/pull/1111
     */
    @Bean
    public FilterRegistrationBean sentinelFilterRegistration() {
        FilterRegistrationBean registration = new FilterRegistrationBean();
        registration.setFilter(new CommonFilter());
        registration.addUrlPatterns("/*");
        // 入口资源关闭聚合
        registration.addInitParameter(CommonFilter.WEB_CONTEXT_UNIFY, "false");
        registration.setName("sentinelFilter");
        registration.setOrder(1);
        return registration;
    }
}

重新测试,用JMeter给/testA一个高频请求,浏览器也高频访问/testB,通过结果树可以看到,有好多/testA请求的返回值可以看到有好多请求直接走了blockHandler()方法进行返回,也就是触发了服务降级,另外,浏览器端访问/testB的时候,不受影响。

和“关联”流控模式有点相似,我感觉,链路模式比关联模式的粒度更细。

流控效果

快速失败

前面都是采用的快速失败的方式,当请求达到阈值后,不再继续处理,直接返回一个失败的结果。

Warm Up

Warm Up的意思是预热,比如某个资源在初期时候,初始化操作比较大量的运算,不能及时响应高并发的请求,那么在启动初期,所接受的阈值就比较低,经过“预热时长”后,初始化完成,便可以接收高并发请求了。

上图所示,每秒最多可以接收100个请求,在初期,阈值为100/3=33,这个3叫coldFactor(冷加载因子,默认值是3)。

使用JMeter,开启12个线程,添加一个“固定定时器”,线程延迟100毫秒,也就是1秒钟1个线程会发出10个请求,12个线程就是120个请求,效果图如下所示,绿色的线在刚开始的时候,呈现出一个预热的效果,所能处理的请求数逐渐提升。

排队等待

所有请求进行排队,每秒钟能处理的请求数就是阈值,处理不过来的就排队等待,如果请求达到超时时间则退出队伍。

根据效果图可以看出,每秒处理的请求数基本是恒定的,那些拒绝的,也就是到达超时时间,还没有处理的请求。

5.降级规则

熔断降级的官网介绍:https://github.com/alibaba/Sentinel/wiki/%E7%86%94%E6%96%AD%E9%99%8D%E7%BA%A7

点击“降级规则”-“新增降级规则”,可以看到,降级策略有:RT(平均响应时间)、异常比例、异常数。

时间窗口:表示触发降级后,持续的时间,超过时间窗口后,关闭降级,并重新统计。

Sentinel的熔断降级会在调用链路中某个资源不稳定(调用超时或异常比例升高)时候触发,对这个资源进行限制,让它快速失败,避免影响其他服务导致级联错误。

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

添加一个testC用于测试。

@GetMapping("/testC")
public String testC() throws InterruptedException {
    Thread.sleep(1000);
    return "----testC----";
}

降级策略

RT

给/testC添加降级规则,当平均响应时间超过500ms,触发降级,降级5s后,关闭降级重新统计。

以每秒钟12个请求发送给/testC查看效果。

异常比例

修改testC()方法,手动加入一个1/0的异常。

上图表示,当异常比例达到50%的时候,触发降级,窗口期是5秒,5秒后,重新统计。因为我们加入的是1/0,必定每次都会抛异常,使用浏览器频繁请求http://localhost:8401/testC,初期会看到1/0的异常,再之后,就是Blocked by Sentinel (flow limiting)了,表明Sentinel的异常比例起作用了,继续刷新,过了窗口期后,又会抛出1/0的异常,继续刷新,异常比例达到0.5,再次进入降级。

异常数

在官网上,有关于异常数的说明:注意由于统计时间窗口是分钟级别的,若timeWindow小于60s,则结束熔断状态后仍可能再进入熔断状态。所以,我们需要把时间窗口设置为大于60的数字。

访问10次http://localhost:8401/testC后,触发降级,再次访问,只能返回Blocked by Sentinel (flow limiting),在这70秒内,再次访问,都会返回降级的结果。

6.热点key限流

点击“热点规则”-“新增热点规则”,可以看到一些参数,这里只支持QPS模式,根据请求地址里的参数做限流。官网地址:https://github.com/alibaba/Sentinel/wiki/%E7%83%AD%E7%82%B9%E5%8F%82%E6%95%B0%E9%99%90%E6%B5%81

添加一个testHotKey()方法和降级方法。

@GetMapping("/testHotKey")
// value对应资源名,blockHandler表示服务降级自定义方法(注意降级方法和目标方法的返回值要一致)
@SentinelResource(value = "testHotKey", blockHandler = "dealWith")
// @RequestParam中,required=false表示当前请求可以不带该参数
public String testHotKey(@RequestParam(value = "param1", required = false) String param1, @RequestParam(value = "param2", required = false) String param2) {
    return "----testHotKey success----";
}

// 参数列表要和目标方法一样,BlockException一定要带上,否则不进这个方法
public String dealWith(String param1, String param2, BlockException blockException) {
    blockException.printStackTrace();
    return "----testHotKey fail----";
}

先看一个简单的情况。

当请求http://localhost:8401/testHotKey的时候,只要带了param1参数(param1在controller方法中参数索引是0),就要受到Hystrix的监控,当QPS大于1时,就会触发服务降级。

再来看这个复杂点的。这个的定义就是说,对param1(param1在controller方法中参数索引是0)做QPS=1的限流控制,但是param1=vip是一个例外项,当param1=vip的时候,QPS设置为100。

我们访问http://localhost:8401/testHotKey?param1=vip测试一下,通过刷新浏览器可以发现,不会再走到服务降级方法里了。

注意,Sentinel只处理BlockException,目标请求里,如果出现了其他异常,是不会走服务降级方法的,@SentinelResource还有一个fallback参数,可以用来处理这个情况。

7.系统规则

点击“系统规则”-“新增系统规则”,可以看到阈值类型和阈值参数,具体的含义可以去官网看下。官网地址:https://github.com/alibaba/Sentinel/wiki/%E7%B3%BB%E7%BB%9F%E8%87%AA%E9%80%82%E5%BA%94%E9%99%90%E6%B5%81

系统规则是用来设置全局的规则的,处于全局入口的地位。

8.@SentinelResource

@SentinelResource和Hystrix里的@HystrixCommand非常类似。8401模块中添加cloud-api-common坐标。

<dependency>
    <groupId>com.atguigu.springcloud</groupId>
    <artifactId>cloud-api-commons</artifactId>
    <version>${project.version}</version>
</dependency>

按照资源名称限流

添加/byResource方法和handleException方法。

@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(500, exception.getClass().getCanonicalName() + "\t 服务不可用");
}

新增流控规则,添加@SentinelResource注解value值的内容,对这个resource进行流控。通过浏览器访问,当QPS大于1时,就可以看到直接进入限流方法了。

按照URL地址限流

添加byURL方法,这里没有带blockHandler指定降级方法,会走默认的服务降级方法。

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

新增流控规则,浏览器访问http://localhost:8401/byURL,当QPS大于1时,返回了系统自带的服务降级响应。注意按照URL和按照Resource的区别,按照URL的时候,不要忘记最前面的/,如果不带/,那么就去查找@SentinelResource注解的value了。

服务限流引起降级方法面临的问题

  1. 使用系统默认的服务降级方法,没有业务提示,不符合要求
  2. 现在的解决方法,把目标方法和服务降级方法写在了一起,是一种耦合现象
  3. 每个业务都需要添加一个服务降级方法,代码量加剧,可能会出现大量的重复代码
  4. 没有全局的统一处理方法

自定义限流逻辑

添加一个customerBlockHandler方法,用于处理/customerBlockHandler请求,注意注解上的blockHandlerClass和blockHandler参数,通过这两个参数,指定服务降级需要执行的方法。

@GetMapping("/customerBlockHandler")
@SentinelResource(value = "customerBlockHandler",
        blockHandlerClass = CustomerBlockHandler.class, blockHandler = "handlerException2")
public CommonResult customerBlockHandler() {
    return new CommonResult(200, "按照客户自定义限流测试", new Payment(2020L, "serial003"));
}

我们还需要加一个CustomerBlockHandler.java用于处理自定义的限流逻辑。这里的方法名以及参数,有一定的要求,否则不生效,参考这里的blockHandler/blockHandlerClass结点的描述。

package com.atguigu.springcloud.alibaba.handler;

import com.alibaba.csp.sentinel.slots.block.BlockException;
import com.atguigu.springcloud.entities.CommonResult;
import com.atguigu.springcloud.entities.Payment;

public class CustomerBlockHandler {
    public static CommonResult handlerException(BlockException exception) {
        return new CommonResult(444, "按照客户自定义的Glogal 全局异常处理 ---- 1", new Payment(2020L, "serial003"));
    }

    public static CommonResult handlerException2(BlockException exception) {
        return new CommonResult(444, "按照客户自定义的Glogal 全局异常处理 ---- 2", new Payment(2020L, "serial003"));
    }
}

因为我们指定的降级方法是在@SentinelResource注解上的,所以在Sentinel里配置的时候,要以@SentinelResource的value为标准配置资源名,如果以@GetMapping里的URL来配置,不能走到自定义的处理方法里。

添加上流控规则后,访问http://localhost:8401/customerBlockHandler查看效果,确实走到了自定义的方法里。

注解属性说明

参考官网介绍:https://github.com/alibaba/Sentinel/wiki/%E6%B3%A8%E8%A7%A3%E6%94%AF%E6%8C%81

Sentinel监控的服务,都会放到try-catch里,在try里面会检测是否触发服务限流,如果触发,就抛出BlockException异常。

Sentinel有3个核心API:SphU(定义资源)、Tracer(定义统计)、ContextUtil(定义上下文)。

9.服务熔断功能

新建cloudalibaba-provider9003、cloudalibaba-provider9004、cloudalibaba-consumer-nacos-order84模块。这三个模块都会注册进Nacos,9003和9004作为服务提供者,消费者整合Ribbon实现负载均衡,84作为消费者,整合Feign实现服务接口调用。

cloudalibaba-provider9003、cloudalibaba-provider9004、cloudalibaba-consumer-nacos-order84的pom.xml完全一致,所以这里就只放dependency坐标了。

<dependencies>
    <dependency>
        <groupId>com.alibaba.cloud</groupId>
        <artifactId>spring-cloud-starter-alibaba-nacos-discovery</artifactId>
    </dependency>
    <dependency>
        <groupId>com.alibaba.cloud</groupId>
        <artifactId>spring-cloud-starter-alibaba-sentinel</artifactId>
    </dependency>
    <dependency>
        <groupId>org.springframework.cloud</groupId>
        <artifactId>spring-cloud-starter-openfeign</artifactId>
    </dependency>
    <dependency>
        <groupId>com.atguigu.springcloud</groupId>
        <artifactId>cloud-api-commons</artifactId>
        <version>${project.version}</version>
    </dependency>
    <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>
    <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>

cloudalibaba-provider9003和cloudalibaba-provider9004添加application.yml配置文件,注意修改server.port。

server:
  port: 9003或9004
spring:
  application:
    name: nacos-payment-provider
  cloud:
    nacos:
      discovery:
        server-addr: 192.168.0.123:8848
management:
  endpoints:
    web:
      exposure:
        include: '*'

cloudalibaba-provider9003和cloudalibaba-provider9004添加主启动类,需要注意的是,添加@EnableDiscoveryClient注解用于Nacos的服务发现,其他的正常写即可。添加业务类,这里使用HashMap构造假数据取值。

package com.atguigu.springcloud.alibaba.controller;

import com.atguigu.springcloud.entities.CommonResult;
import com.atguigu.springcloud.entities.Payment;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.RestController;

import java.util.HashMap;

@RestController
public class PaymentController {
    @Value("${server.port}")
    private String serverPort;
    public static HashMap<Long, Payment> map = new HashMap<>();

    static {
        map.put(1L, new Payment(1L, "1111"));
        map.put(1L, new Payment(2L, "2222"));
        map.put(1L, new Payment(3L, "3333"));
    }

    @GetMapping(value = "/paymentSQL/{id}")
    public CommonResult<Payment> paymentSQL(@PathVariable("id") Long id) {
        Payment payment = map.get(id);
        return new CommonResult<>(200, "from mysql,serverPort: " + serverPort, payment);
    }
}

cloudalibaba-provider9003和cloudalibaba-provider9004就搭建完了,下面继续搭建cloudalibaba-consumer-nacos84模块,添加application.yml。

server:
  port: 84
spring:
  application:
    name: nacos-order-consumer
  cloud:
    nacos:
      discovery:
        server-addr: 192.168.0.123:8848
    sentinel:
      transport:
        dashboard: 192.168.0.123:8858
        port: 8719
# 消费者将去访问的微服务名称,这里采用服务名称查找服务(成功注册进nacos的微服务提供者)
server-url:
  nacos-user-service: http://nacos-payment-provider

添加主启动类,也要加上@EnableDiscoveryClient注解用于服务发现,因为使用的docker搭建的Sentinel,所以需要额外指定TransportConfig.HEARTBEAT_CLIENT_IP。

package com.atguigu.springcloud.alibaba;

import com.alibaba.csp.sentinel.transport.config.TransportConfig;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.cloud.client.discovery.EnableDiscoveryClient;

@SpringBootApplication
@EnableDiscoveryClient
public class OrderMain84 {
    public static void main(String[] args) {
        // Linux下Docker模式运行Sentinel时使用下面这行代码,Windows下运行Sentinel不要使用
        System.setProperty(TransportConfig.HEARTBEAT_CLIENT_IP, "192.168.0.105");
        SpringApplication.run(OrderMain84.class, args);
    }
}

添加配置类,向容器中注册一个带有负载均衡的RestTemplate。

package com.atguigu.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();
    }
}

添加业务类CircleBreakerController.java,主要是用来演示服务熔断的,后面会在这个类上做一些修改。

package com.atguigu.springcloud.alibaba.controller;

import com.alibaba.csp.sentinel.annotation.SentinelResource;
import com.atguigu.springcloud.entities.CommonResult;
import com.atguigu.springcloud.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 {
    // nacos-payment-provider就是9003和9004微服务的spring.application.name的值
    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<Payment> result = restTemplate.getForObject(SERVICE_URL + "/paymentSQL/" + id, CommonResult.class, id);
        if (id == 4) {
            throw new IllegalArgumentException("IllegalArgument,非法参数异常...");
        } else if (result.getData() == null) {
            throw new NullPointerException("NullPointerException,该ID没有对应记录,空指针异常");
        }
        return result;
    }
}

Sentinel服务熔断无配置

通过浏览器访问http://localhost:84/consumer/fallback/1,多次刷新,观察响应信息可以判断负载均衡是否生效,查看Nacos注册中心,Sentinel监控中心,确保当前3个模块的环境搭建是成功的。

访问http://localhost:84/consumer/fallback/4http://localhost:84/consumer/fallback/5都会抛出我们自定义的异常信息。下面我们对这块进行改造,当程序抛出异常时候,走SentinelResource指定自定义的fallback方法。

Sentinel服务熔断只配置fallback

将@SentinelResource(value = "fallback")换成@SentinelResource(value = "fallback", fallback = "handlerFallback"),并添加handlerFallback()方法,再次访问刚才报错的两个地址,这次走了自定义的fallback方法。

public CommonResult handlerFallback(@PathVariable Long id, Throwable throwable) {
    Payment payment = new Payment(id, null);
    return new CommonResult(444, "异常fallback方法,异常内容是:" + throwable.getMessage(), payment);
}

Sentinel服务熔断只配置blockHandler

将@SentinelResource(value = "fallback", fallback = "handlerFallback")换成@SentinelResource(value = "fallback", blockHandler = "blockHandler"),并添加blockHandler()方法。

public CommonResult blockHandler(@PathVariable Long id, BlockException e) {
    Payment payment = new Payment(id, "null");
    return new CommonResult(444, "blockHandler-sentinel限流,BlockException:" + e.getMessage(), payment);
}

如果此时不设置降级规则,访问http://localhost:84/consumer/fallback/5直接抛出异常。我们添加一个降级规则(假设添加异常数>2触发降级),再次访问http://localhost:84/consumer/fallback/5,第三次访问的时候,就走到了我们自定义的blockHandler()方法里。

Sentinel服务熔断fallback和blockHandler都配置

将@SentinelResource(value = "fallback", blockHandler = "blockHandler")换成@SentinelResource(value = "fallback", fallback = "handlerFallback", blockHandler = "blockHandler")。

在流控规则里,加一个fallback资源的直接流控限制,当QPS>1时,触发blockException。通过浏览器访问http://localhost:84/consumer/fallback/5,在低频(QPS<1)的时候,由fallback来接管,在高频(QPS>1)的时候,由blockHandler来接管。

所以,我们得出结论,当fallback和blockHandler都配置后,被限流降级抛出BlockException只会进入blockHandler指定的方法。

Sentinel服务熔断exceptionsToIgnore

exceptionsToIgnore也是@SentinelResource的参数,接收一个Throwable的数组,表示哪些异常不需要走fallback指定的方法。比方说,这里把NullPointException加进去。再次访问http://localhost:84/consumer/fallback/5,就不会进fallback指定的方法了,而是直接将异常信息抛出。

Sentinel服务熔断OpenFeign

pom.xml添加OpenFeign的坐标。

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

修改application.yml,开启Sentinel对Feign的支持,即添加feign.sentinel.enable: true。修改主启动类,添加@EnableFeignClients注解。编写PaymentService.java接口,带上@FeignClient注解。

package com.atguigu.springcloud.alibaba.service;

import com.atguigu.springcloud.entities.CommonResult;
import com.atguigu.springcloud.entities.Payment;
import org.springframework.cloud.openfeign.FeignClient;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;

// 指定fallback表示Feign的服务降级处理类,PaymentFallbackService实现这个接口,在对应的方法里,编写服务降级代码
@FeignClient(value = "nacos-payment-provider", fallback = PaymentFallbackService.class)
public interface PaymentService {
    @GetMapping(value = "/paymentSQL/{id}")
    CommonResult<Payment> paymentSQL(@PathVariable("id") Long id);
}

这里还需要PaymentFallbackService.java,用于处理服务调用中的异常处理,别忘了@Component注解。

package com.atguigu.springcloud.alibaba.service;

import com.atguigu.springcloud.entities.CommonResult;
import com.atguigu.springcloud.entities.Payment;
import org.springframework.stereotype.Component;

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

 在CircleBreakerController中加入一个请求,测试通过Feign发送请求,还要用@Resource加入PaymentService对象。

@Resource
private PaymentService paymentService;

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

通过浏览器请求http://localhost:84/consumer/paymentSQL/1可以获取到数据,为了让Feign的服务降级方法起作用,我们关掉9003和9004模块,再次访问,因为Feign默认连接超时为1秒,默认读取资源超时是1秒,2s过后Feign超时,程序自动进入了Feign的自定义fallback方法里面。

熔断框架比较
 SentinelHystrixresilience4j
隔离策略信号量隔离(并发线程数限流)线程池隔离/信号量隔离信号量隔离
熔断降级策略基于响应时间、异常比率、异常数基于异常比率基于异常比率、响应时间
实时统计实现滑动窗口(LeapArray)滑动窗口(基于RxJava)Ring Bit Buffer
动态规则配置支持多种数据源支持多种数据源有限支持
扩展性多个扩展点插件的形式接口的形式
基于注解的支持支持支持支持
限流基于QPS,支持基于调用关系的限流有限的支持Rate Limiter
流量整形支持预热模式、匀速器模式、预热排队模式不支持简单的Rate Limiter模式
系统自适应保护支持不支持不支持
控制台提供开箱即用控制台,可配置规则、查看秒级监控、机器发现简单的监控查看不提供控制台,可对接其他监控系统

10.规则持久化

之前的时候,只要重启Sentinel相关的模块,Sentinel里的配置信息都会丢失,因为默认情况下,Sentinel的规则都是保存在内存里的,我们需要结合Nacos把Sentinel里的配置持久化。只要在Nacos里做一些配置,Sentinel的流控规则,就可以持久化了。这里使用cloudalibaba-sentinel-service8401做修改。

pom.xml中添加sentinel-datasource-nacos坐标。

<dependency>
    <groupId>com.alibaba.csp</groupId>
    <artifactId>sentinel-datasource-nacos</artifactId>
</dependency>

在application.yml里添加内容,指定配置文件存储在Nacos的基本信息。

spring:
  application:
    name: cloudalibaba-sentinal-service
  cloud:
    sentinel:
      datasource:
        ds1:
          nacos:
            server-addr: 192.168.0.123:8848
            dataId: ${spring.application.name}
            groupId: DEFAULT_GROUP
            data-type: json
            rule-type: flow

回到Nacos里,点击“配置管理”-“配置列表”-“新建配置”,写上dataId为cloudalibaba-sentinal-service,选择json,填充以下内容后点击发布。启动8401模块,查看Sentinel里的流控配置。

[
    {
        "resource": "/byURL",
        "limitApp": "default",
        "grade": 1,
        "count": 1,
        "strategy": 0,
        "controlBehavior": 0,
        "clusterMode": false
    }
]
# 以下是解释
# resource:资源名称
# limitApp:来源应用
# grade:阈值类型:0-线程数;1-QPS
# count:单机阈值
# strategy:流控模式:0-直接;1-关联;2-链路
# controlBehavior:流控效果:0-快速失败;1-Warm Up;2-排队等待
# clusterMode:是否集群

因为Sentinel是懒加载,所以先请求若干次http://localhost:8401/byURL,再点进去查看流控规则,即可看到从Nacos里读取到的规则了。如果看不到,把Nacos、Sentinel、8401模块都重启一下,我第一次就是配置都是正确的,但是通过8401模块的启动日志来看,报了一个WARN:converter can not convert rules because source is empty,也就是从Nacos里读取到的配置是empty的。非常迷惑,重启Nacos、Sentinel、8401模块后,就可以正常读取到了。

不过,这个方法非常鸡肋,在Nacos里手写配置文件,很容易出错,而且我试了下,在Nacos里,把count的值改掉,Sentinel里并不能感知到,还是按照1来做流控。

这个后序再研究下。

评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值