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监控这个模块。



    
        cloud2020
        com.atguigu.springcloud
        1.0-SNAPSHOT
    
    4.0.0
    cloudalibaba-sentinel-service8401
    
        
            com.alibaba.cloud
            spring-cloud-starter-alibaba-nacos-discovery
        
        
        
            com.alibaba.csp
            sentinel-datasource-nacos
        
        
            com.alibaba.cloud
            spring-cloud-starter-alibaba-sentinel
        
        
            org.springframework.cloud
            spring-cloud-starter-openfeign
        
        
            org.springframework.boot
            spring-boot-starter-web
        
        
            org.springframework.boot
            spring-boot-starter-actuator
        
        
            org.springframework.boot
            spring-boot-devtools
            runtime
            true
        
        
            org.projectlombok
            lombok
            true
        
        
            org.springframework.boot
            spring-boot-starter-test
            test
        
    

添加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  (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);
    }
}

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

Spring Cloud笔记-Spring Cloud Alibaba Sentinel实现熔断与限流(十九)_第1张图片

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,即可看到效果。

Spring Cloud笔记-Spring Cloud Alibaba Sentinel实现熔断与限流(十九)_第2张图片

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

Spring Cloud笔记-Spring Cloud Alibaba Sentinel实现熔断与限流(十九)_第3张图片

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

Spring Cloud笔记-Spring Cloud Alibaba Sentinel实现熔断与限流(十九)_第4张图片

关联

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

Spring Cloud笔记-Spring Cloud Alibaba Sentinel实现熔断与限流(十九)_第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进行测试。

Spring Cloud笔记-Spring Cloud Alibaba Sentinel实现熔断与限流(十九)_第6张图片

根据上面的测试说明,发现测试没效果,具体原因参考这里: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

Spring Cloud笔记-Spring Cloud Alibaba Sentinel实现熔断与限流(十九)_第7张图片

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

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

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

Spring Cloud笔记-Spring Cloud Alibaba Sentinel实现熔断与限流(十九)_第8张图片

排队等待

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

Spring Cloud笔记-Spring Cloud Alibaba Sentinel实现熔断与限流(十九)_第9张图片

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

Spring Cloud笔记-Spring Cloud Alibaba Sentinel实现熔断与限流(十九)_第10张图片

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后,关闭降级重新统计。

Spring Cloud笔记-Spring Cloud Alibaba Sentinel实现熔断与限流(十九)_第11张图片

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

Spring Cloud笔记-Spring Cloud Alibaba Sentinel实现熔断与限流(十九)_第12张图片

异常比例

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

Spring Cloud笔记-Spring Cloud Alibaba Sentinel实现熔断与限流(十九)_第13张图片

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

异常数

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

Spring Cloud笔记-Spring Cloud Alibaba Sentinel实现熔断与限流(十九)_第14张图片

访问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----";
}

先看一个简单的情况。

Spring Cloud笔记-Spring Cloud Alibaba Sentinel实现熔断与限流(十九)_第15张图片

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

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

Spring Cloud笔记-Spring Cloud Alibaba Sentinel实现熔断与限流(十九)_第16张图片

我们访问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坐标。


    com.atguigu.springcloud
    cloud-api-commons
    ${project.version}

按照资源名称限流

添加/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时,就可以看到直接进入限流方法了。

Spring Cloud笔记-Spring Cloud Alibaba Sentinel实现熔断与限流(十九)_第17张图片

按照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了。

Spring Cloud笔记-Spring Cloud Alibaba Sentinel实现熔断与限流(十九)_第18张图片

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

  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来配置,不能走到自定义的处理方法里。

Spring Cloud笔记-Spring Cloud Alibaba Sentinel实现熔断与限流(十九)_第19张图片

添加上流控规则后,访问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坐标了。


    
        com.alibaba.cloud
        spring-cloud-starter-alibaba-nacos-discovery
    
    
        com.alibaba.cloud
        spring-cloud-starter-alibaba-sentinel
    
    
        org.springframework.cloud
        spring-cloud-starter-openfeign
    
    
        com.atguigu.springcloud
        cloud-api-commons
        ${project.version}
    
    
        org.springframework.boot
        spring-boot-starter-web
    
    
        org.springframework.boot
        spring-boot-starter-actuator
    
    
        org.springframework.boot
        spring-boot-devtools
        runtime
        true
    
    
        org.projectlombok
        lombok
        true
    
    
        org.springframework.boot
        spring-boot-starter-test
        test
    

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 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 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 fallback(@PathVariable Long id) {
        CommonResult 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/4和http://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的坐标。


    org.springframework.cloud
    spring-cloud-starter-openfeign

修改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 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 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 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方法里面。

熔断框架比较
  Sentinel Hystrix resilience4j
隔离策略 信号量隔离(并发线程数限流) 线程池隔离/信号量隔离 信号量隔离
熔断降级策略 基于响应时间、异常比率、异常数 基于异常比率 基于异常比率、响应时间
实时统计实现 滑动窗口(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坐标。


    com.alibaba.csp
    sentinel-datasource-nacos

在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来做流控。

这个后序再研究下。

你可能感兴趣的:(Spring,Cloud)