Spring Cloud Alibaba Sentinel 熔断与限流(二)

一、sentinel熔断(部分内容摘自官网)

1.1.熔断规则说明

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

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

注意:本文档针对 Sentinel 1.8.0 及以上版本。1.8.0 版本对熔断降级特性进行了全新的改进升级,请使用最新版本以更好地利用熔断降级的能力。

1.2.熔断策略

Sentinel 提供以下几种熔断策略:

  • 慢调用比例 (SLOW_REQUEST_RATIO):选择以慢调用比例作为阈值,需要设置允许的慢调用 RT(即最大的响应时间),请求的响应时间大于该值则统计为慢调用。当单位统计时长(statIntervalMs)内请求数目大于设置的最小请求数目,并且慢调用的比例大于阈值,则接下来的熔断时长内请求会自动被熔断。经过熔断时长后熔断器会进入探测恢复状态(HALF-OPEN 状态),若接下来的一个请求响应时间小于设置的慢调用 RT 则结束熔断,若大于设置的慢调用 RT 则会再次被熔断。
  • 异常比例 (ERROR_RATIO):当单位统计时长(statIntervalMs)内请求数目大于设置的最小请求数目,并且异常的比例大于阈值,则接下来的熔断时长内请求会自动被熔断。经过熔断时长后熔断器会进入探测恢复状态(HALF-OPEN 状态),若接下来的一个请求成功完成(没有错误)则结束熔断,否则会再次被熔断。异常比率的阈值范围是 [0.0, 1.0],代表 0% - 100%。
  • 异常数 (ERROR_COUNT):当单位统计时长内的异常数目超过阈值之后会自动进行熔断。经过熔断时长后熔断器会进入探测恢复状态(HALF-OPEN 状态),若接下来的一个请求成功完成(没有错误)则结束熔断,否则会再次被熔断。

1.3.熔断降级规则说明

熔断降级规则(DegradeRule)包含下面几个重要的属性:

 

Field说明默认值
resource 资源名,即规则的作用对象  
grade 熔断策略,支持慢调用比例/异常比例/异常数策略 慢调用比例
count 慢调用比例模式下为慢调用临界 RT(超出该值计为慢调用);异常比例/异常数模式下为对应的阈值  
timeWindow 熔断时长,单位为 s  
minRequestAmount 熔断触发的最小请求数,请求数小于该值时即使异常比率超出阈值也不会熔断(1.7.0 引入) 5
statIntervalMs 统计时长(单位为 ms),如 60*1000 代表分钟级(1.8.0 引入) 1000 ms
slowRatioThreshold 慢调用比例阈值,仅慢调用比例模式有效(1.8.0 引入)

1.4.熔断策略一:慢调用比例 

1.4.1.慢调用比例 是什么?

慢调用比例 (SLOW_REQUEST_RATIO):选择以慢调用比例作为阈值,需要设置允许的慢调用 RT(即最大的响应时间),请求的响应时间大于该值则统计为慢调用。当单位统计时长(statIntervalMs)内请求数目大于设置的最小请求数目,并且慢调用的比例大于阈值,则接下来的熔断时长内请求会自动被熔断。

1.4.2.在控制器中添加调用方法

在 SentinelController 中添加如下内容:

@GetMapping("/testF")
    public String testF(){
        try {
            TimeUnit.SECONDS.sleep(5);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        return "服务访问成功------testF";
    }

1.4.3.设置熔断策略

1s内 的 QPS>5 并且这些请求的RT>300(即最大的响应时间) 并且大于比例阈值0.1(10%)触发熔断

1.4.4.通过jmeter发送请求模拟测试访问 /testF

线程组设置如下:

执行结果:

1.4.5.访问/testF

通过浏览器访问,http://localhost:8401/testF 发现通过JMeter测试,1秒钟发起10个线程请求/testF,此时就会触发熔断效果,停止测试以后,10秒钟以后恢复正常

经过熔断时长后熔断器会进入探测恢复状态(HALF-OPEN 状态),若接下来的一个请求响应时间小于设置的慢调用 RT 则结束熔断,若大于设置的慢调用 RT 则会再次被熔断。

1.4.6.查看sentinel实时监控

发现刚刚这里10个请求,只有一个通过,其他的出发了熔断规则,直接拒绝处理了

1.5.熔断策略二:异常比例

1.5.1.异常比例 是什么?

异常比例 (ERROR_RATIO):当单位统计时长(statIntervalMs)内请求数目大于设置的最小请求数目,并且异常的比例大于阈值,则接下来的熔断时长内请求会自动被熔断。异常比率的阈值范围是 [0.0, 1.0],代表 0% - 100%。

注意:异常降级仅针对业务异常,对 Sentinel 限流降级本身的异常(BlockException)不生效。

1.5.2.创建接口方法

在 SentinelController 中创建接口方法,代码如下:

@GetMapping("/testJ")
    public String testJ(Integer id){
        if(id != null && id > 1){
            int a = 5 / 0;
            //throw new RuntimeException("异常比例测试");
        }
        return "服务访问成功------testJ";
    }

1.5.3.设置熔断策略异常比例

给 /testJ 设置熔断策略为异常比例如下:设置异常比例阈值0.1,熔断时长10s,最小请求数为5s

1.5.4.通过jmeter测试

1s内发出10个请求 访问:http://localhost:8401/testJ?id=3,如下图

1.5.5.马上通过浏览器访问

访问:http://localhost:8401/testJ ,如下图,出现了服务降级,等一会又会回复,经过熔断时长后熔断器会进入探测恢复状态(HALF-OPEN 状态),若接下来的一个请求成功完成(没有错误)则结束熔断,否则会再次被熔断。

 

1.6.熔断策略三:异常数

1.6.1.异常数

异常数 (ERROR_COUNT):当单位统计时长内的异常数目超过阈值之后会自动进行熔断。

注意:异常降级仅针对业务异常,对 Sentinel 限流降级本身的异常(BlockException)不生效。

1.6.2.创建接口方法

在SentinelController中创建接口方法:

@GetMapping("/testH")
    public String testH(Integer id){
        if(id != null && id > 1){
            int a = 5 / 0;
            //throw new RuntimeException("异常比例测试");
        }
        return "服务访问成功------testH";
    }

1.6.3.给 /testH 设置熔断策略:异常数

设置异常数策略,当1秒钟内请求超过5并且异常数大于5个的时候触发熔断

1.6.4.通过jmeter模拟请求 /testH

设置10个线程1秒内发出10个请求 地址如:http://localhost:8401/testH?id=3

 

1.6.5.执行jmeter脚本,在通过浏览器访问http://localhost:8401/testH

会发现出现了熔断效果,如下图:

经过熔断时长后熔断器会进入探测恢复状态(HALF-OPEN 状态),若接下来的一个请求成功完成(没有错误)则结束熔断,否则会再次被熔断

二、热点规则

2.1.什么是热点

热点就是经常访问的数据。大多数情况下希望统计某个热点数据中访问频次最高的 Top K 数据,并对其访问进行限制。比如:

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

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

2.2.使用@SentinelResource注解

  • 其实这个热点限流,本质就是更加细粒度的流控规则,那么如果想使用它就必须要配合对应SentinelResource注解。Sentinel 提供了 @SentinelResource 注解用于定义资源,有很多的参数,我们这里主要关注两个参数:
value:代表资源名称,必需项,因为需要通过resource name找到对应的规则,这个是必须配置的
blockHandler:blockHandler 对应处理 BlockException 的方法名称,可选项,访问范围需要是 public,返回类型需要与原方法相匹配,参数类型需要和原方法相匹配并且最后加一个额外的参数,类型为 BlockException。
  • 案例演示:在 SentinelController 中编写代码,如下:
@GetMapping("/testHotKey")
    @SentinelResource("testHotKey")
    public String testHotKey(@RequestParam(value = "p1",required = false) String p1,
                             @RequestParam(value = "p2",required = false)String p2,
                             @RequestParam(value = "p3",required = false) String p3){
        return "服务访问成功------testHotKey";
    }
  • 在sentinel配置热点规则:

  • 在浏览器访问:localhost:8401/testHotKey?p1 , 快速刷新页面,可看到效果如下图:

  • 上面的案例中发现此限流方法的提示效果非常不友好,所以如果我们需要能够得到友好的提示,就需要使用@SentinelResource注解提供的另外一个参数blockHandler,这个参数是可以指定当出现异常时的处理方法:

对于 SentinelController的 /testHotKey方法进行修改,指定兜底的方法 handler_HotKey,代码如下:

@GetMapping("/testHotKey")
    //当出现热点规则限流后,默认的会报error page提示信息不友好,所以通过blockHandler来指定兜底降级的处理方法
    @SentinelResource(value = "testHotKey", blockHandler = "handler_HotKey")
    public String testHotKey(@RequestParam(value = "p1",required = false) String p1,
                             @RequestParam(value = "p2",required = false)String p2,
                             @RequestParam(value = "p3",required = false) String p3){
        return "服务访问成功------testHotKey";
    }

    //兜底的方法,定义的参数必须和上述参数保持一致,需要添加一个BlockException类型的参数
    public String handler_HotKey(String hot1, String hot2, String hot3, BlockException e) {
        return "系统繁忙,请稍后重试......";
    }
  • 重启项目,使用浏览器访问 http://localhost:8401/testHotKey?p1,多次刷新

会发现采用兜底方法处理了降级

2.3.参数例外项

热点设置中高级选项是为了提供更加细粒度的热点数据的流控设置,比如我们当前的例子中,目前p1参数在访问时超过阈值就会被限流,但是我们可以通过参数例外项设置p1具体等于特殊的某个值的时候,触发不同的限流效果。假如p1的值等于5时,它的阈值可以达到200。需要​注意的是,参数例外项中的参数类型仅支持一下7种数据类型,如下所示:

  • 设置热点规则的参数例外项,如下:

设置索引为0的参数p1,参数类型为 java.lang.String ,设置参数的值为5时,单机阈值是200

  • 在浏览器访问:http://localhost:8401/testHotKey?p1=1, 此时来看p1=1(除了5之外的值都会出现降级)的效果

多次访问依然会触发热点规则,出现降级的情况,如下图:

  • 在浏览器访问:http://localhost:8401/testHotKey?p1=5,此时的效果如下:

多次刷新依然没有出现降级,这是因为我们设置了参数例外项,参数的值为5的时候,阈值为200

三、系统规则

Sentinel 系统自适应限流从整体维度对应用入口流量进行控制,结合应用的 Load、CPU 使用率、总体平均 RT、入口 QPS 和并发线程数等几个维度的监控指标,通过自适应的流控策略,让系统的入口流量和系统的负载达到一个平衡,让系统尽可能跑在最大吞吐量的同时保证系统整体的稳定性,换言之,之前都是从请求方法级别限流、而这里则是对整个系统进行限流。

  • 系统规则 说明

系统保护规则是从应用级别的入口流量进行控制,从单台机器的 load、CPU 使用率、平均 RT、入口 QPS 和并发线程数等几个维度监控应用指标,让系统尽可能跑在最大吞吐量的同时保证系统整体的稳定性。

系统保护规则是应用整体维度的,而不是资源维度的,并且仅对入口流量生效。入口流量指的是进入应用的流量,比如 Web 服务或 Dubbo 服务端接收的请求,都属于入口流量。系统规则支持以下的模式:

Load 自适应(仅对 Linux/Unix-like 机器生效) 系统的 load1 作为启发指标,进行自适应系统保护。当系统 load1 超过设定的启发值,且系统当前的并发线程数超过估算的系统容量时才会触发系统保护(BBR 阶段)。系统容量由系统的 maxQps * minRt 估算得出。设定参考值一般是 CPU cores * 2.5
CPU usage(1.5.0+ 版本) 当系统 CPU 使用率超过阈值即触发系统保护(取值范围 0.0-1.0),比较灵敏。
平均 RT 当单台机器上所有入口流量的平均 RT 达到阈值即触发系统保护,单位是毫秒。
并发线程数 当单台机器上所有入口流量的并发线程数达到阈值即触发系统保护。
入口 QPS 当单台机器上所有入口流量的 QPS 达到阈值即触发系统保护。
  • 案例演示:设置全局的QPS演示,阈值为1

  • 设置后查看规则如下

  • 最后测试效果不管现在我们访问那个接口只要超过阈值就会被限流,例如访问:

四、授权规则

授权规则可以对请求方来源来限制资源是否通过。授权规则可以对调用方的来源做控制,有白名单和黑名单两种方式。

  • 白名单:来源(origin)在白名单内的调用者允许访问
  • 黑名单:来源(origin)在黑名单内的调用者不允许访问

4.1.Sentinel是通过RequestOriginParser这个接口的parseOrigin来获取请求的来源的

代码如下(从源码复制出来的):

public interface RequestOriginParser {

    /**
     * Parse the origin from given HTTP request.
     *
     * @param request HTTP request
     * @return parsed origin
     */
    String parseOrigin(HttpServletRequest request);
}

4.2.创建config包,在下面创建 RequestOriginParser 接口的实现类:

这个方法的作用就是从request对象中,获取请求者的origin(流控应用中设置的值)值并返回。默认情况下,sentinel不管请求者从哪里来,返回值永远是default,也就是说一切请求的来源都被认为是一样的值default。因此,我们需要自定义这个接口的实现,让不同的请求,返回不同的origin。

package com.augus.cloud.config;

import com.alibaba.csp.sentinel.adapter.spring.webmvc.callback.RequestOriginParser;
import com.alibaba.csp.sentinel.util.StringUtil;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Component;

import javax.servlet.http.HttpServletRequest;

@Component
@Slf4j
class SentinelRequestOriginParser implements RequestOriginParser { //解析请求
    //Origin源头
    @Override
    public String parseOrigin(HttpServletRequest request) {
        //获取请求参数origin的值
        String serverName = request.getParameter("serverName");
        if(serverName == null || "".equals(serverName)){ //请求参数为空
            //表示头信息进行的参数传递,所以获取要从头信息获取
            serverName = request.getHeader("serverName");
        }

        //判断是否为空
        if(StringUtil.isEmpty(serverName)){
            return request.getRemoteAddr();
        }

        return serverName;
    }
}

4.3.在 SentinelController,添加方法如下:

其实对于每一个请求方法都有效,为了演示方便所以创建了一个

@GetMapping("/testZ")
    public String testZ(){
        return "服务访问成功------testZ";
    }

4.4.添加授权规则

这里资源名是具体的那个请求,流控应用可以写多个值,中间使用逗号隔开,这个值实际上就是设置上述实现类中 serverName参数的值

4.5.在浏览器测试

  • 在浏览器访问 http://localhost:8401/testZ 发现不能成功,如下图:

每次参数是无法访问的

  • 在浏览器访问 http://localhost:8401/testZ?serverName=12 发现不能成功,如下图:

虽然输入了 serverName参数,但是值不是pc和app,也不能访问

  • 在浏览器访问 http://localhost:8401/testZ?serverName=pc 可以访问成功,如下图:

输入了 serverName参数,值是pc符合流控应用设置的值,所以可以成功访问

五、@SentinelResource注解

@SentinelResource 注解是 Sentinel 提供的最重要的注解之一,它还包含了多个属性,如下表。

属性说明必填与否使用要求
value 用于指定资源的名称 必填 -
entryType entry 类型 可选项(默认为 EntryType.OUT) -
blockHandler 服务限流后会抛出 BlockException 异常,而 blockHandler 则是用来指定一个函数来处理 BlockException  异常的。

简单点说,该属性用于指定服务限流后的后续处理逻辑。
可选项
  • blockHandler 函数访问范围需要是 public;
  • 返回类型需要与原方法相匹配;
  • 参数类型需要和原方法相匹配并且最后加一个额外的参数,类型为 BlockException;
  • blockHandler 函数默认需要和原方法在同一个类中,若希望使用其他类的函数,则可以指定 blockHandler 为对应的类的 Class 对象,注意对应的函数必需为 static 函数,否则无法解析。
blockHandlerClass 若 blockHandler 函数与原方法不在同一个类中,则需要使用该属性指定 blockHandler 函数所在的类。 可选项
  • 不能单独使用,必须与 blockHandler 属性配合使用;
  • 该属性指定的类中的 blockHandler 函数必须为 static 函数,否则无法解析。
fallback 用于在抛出异常(包括 BlockException)时,提供 fallback 处理逻辑。

fallback 函数可以针对所有类型的异常(除了 exceptionsToIgnore 里面排除掉的异常类型)进行处理。
可选项
  • 返回值类型必须与原函数返回值类型一致;
  • 方法参数列表需要和原函数一致,或者可以额外多一个 Throwable 类型的参数用于接收对应的异常;
  • fallback 函数默认需要和原方法在同一个类中,若希望使用其他类的函数,则可以指定 fallbackClass 为对应的类的 Class 对象,注意对应的函数必需为 static 函数,否则无法解析。
fallbackClass  若 fallback 函数与原方法不在同一个类中,则需要使用该属性指定 blockHandler 函数所在的类。 可选项
  • 不能单独使用,必须与 fallback 或 defaultFallback  属性配合使用;
  • 该属性指定的类中的 fallback 函数必须为 static 函数,否则无法解析。
defaultFallback 默认的 fallback 函数名称,通常用于通用的 fallback 逻辑(即可以用于很多服务或方法)。

默认 fallback 函数可以针对所以类型的异常(除了 exceptionsToIgnore 里面排除掉的异常类型)进行处理。
可选项
  • 返回值类型必须与原函数返回值类型一致;
  • 方法参数列表需要为空,或者可以额外多一个 Throwable 类型的参数用于接收对应的异常;
  • defaultFallback 函数默认需要和原方法在同一个类中。若希望使用其他类的函数,则可以指定 fallbackClass 为对应的类的 Class 对象,注意对应的函数必需为 static 函数,否则无法解析。
exceptionsToIgnore 用于指定哪些异常被排除掉,不会计入异常统计中,也不会进入 fallback 逻辑中,而是会原样抛出。 可选项 -
注:在 Sentinel 1.6.0 之前,fallback 函数只针对降级异常(DegradeException)进行处理,不能处理业务异常。

5.1.按URL地址限流实现后续处理

处理之前启动nacos、sentinel服务

5.1.1.在8401模块controller包下创建下面内容

创建 SentinelResourceTestController 类,内容如下:

@RestController
public class SentinelResourceTestController {

    @GetMapping("/byResource")
    @SentinelResource(value = "byResource",blockHandler = "handler_resource")
    public String byResource(){
        return "服务访问成功-----byResource";
    }

    public String handler_resource(BlockException exception){
        return "系统繁忙,请稍后重试!!!";
    }
}

5.1.2.启动8401模拟

启动8401模块,在浏览器访问, http://localhost:8401/byResource,将其添加到sentinel的簇点链路,如下图:

5.1.2.配置流控规则

下面表示1秒钟内查询次数(QPS)大于1,就使用我们自定义的兜底限流方法处理

5.1.3.测试

浏览器访问:http://localhost:8401/byResource, 快速刷新下面,触发流控规则,如下:

注意:上面使用url限流,流控后还是sentinel默认的流控效果。因为URL限流只会调用默认的方法,只有资源名限流,自定义兜底方法才能起作用

5.2.资源名限流实现后续处理

还是利用上面的

5.2.1.配置流控规则

删除掉上述添加的按照url地址限流的规则,按照资源限流(就是@SentinelResource注解value参数对应值为资源名),下面表示1秒钟内查询次数(QPS)大于1,就使用我们自定义的兜底限流方法处理:

5.2.2.测试

浏览器访问:http://localhost:8401/byResource, 快速刷新下面,触发流控规则,如下:

5.4.自定义限流处理逻辑

5.4.1.上面两个案例存在的如图如下:

  • 系统默认的兜底处理方法,没有体现出我们自己的业务要求。
  • url限流,流控后还是sentinel默认的流控效果。因为URL限流只会调用默认的方法,只有资源名限流,自定义兜底方法才能起作用
  • 依照现有条件,我们自定义的处理方法又和业务代码耦合在一块,代码显示不直观。
  • 每个业务方法都添加—个兜底的,那代码膨胀加剧。
  • 全局统一的处理方法没有体现。

解决方案:@SentinelResource除了blockHandler可以设置自定义限流处理逻辑方法以外,还提供另外一个属性来设置限流处理逻辑类型blockHandlerClass属性,此属性中设置的方法必需为 static 函数,否则无法解析。

5.4.2.创建自定义限流处理逻辑

在com.augus.cloud包下,创建myhandler子包,然后再里面创建类:CustomerBlockHandler,代码如下:

package com.augus.cloud.myhandler;

import com.alibaba.csp.sentinel.slots.block.BlockException;

/**
 * 该类处理自定义限流逻辑
 * 要求该类下设置的方法必需为 static 类型,否则无法解析
 */
public class CustomerBlockHandler {

    public static String handlerException1(BlockException exception){
        return "handlerException1:系统异常,请稍后重试!";
    }

    public static String handlerException2(BlockException exception){
        return "handlerException2:网络崩溃了,请稍后重试!";
    }
}

5.4.2.对接口请求指定自定义限流处理对应的类和方法

在SentinelResourceTestController类型中添加一个接口方法,同时设置@SentinelResource注解和blockHandlerClass属性对应的类型和这个类型中对应的处理方法:

@GetMapping("/byCustomer")
    @SentinelResource(value = "byCustomer",
            blockHandlerClass = CustomerBlockHandler.class, //指定对应的异常处理类
            blockHandler = "handlerException2" // 指定对应类下的具体兜底方法
    )
    public String byCustomer(){
        return "服务访问成功-----byCustomer";
    }

5.4.3.配置流控规则

先启动模块,在浏览器访问 http://localhost:8401/byCustomer,然后配置流控规则,然后来测试在超过限流阈值时处理方法是否为CustomerBlockHandler中handlerException2来进行处理。

5.4.4.测试

添加流控规则以后,频繁刷新浏览器访问 http://localhost:8401/byCustomer,就会发现是CustomerBlockHandler类型的handlerException2方法来处理自定义限流逻辑,如下图:

六、服务熔断功能

服务熔断:应对微服务雪崩效应的一种链路保护机制,类似保险丝。下面实现sentinel整合ribbon、openFeign和fallback

6.1.整合Ribbon

6.1.1.搭建环境说明

需要利用Ribbon进行负载均衡的调用,所以我们需要创建一个服务消费者cloudalibaba-consumer8084和两个服务提供者cloudalibaba-provider9003和cloudalibaba-provider9004,以下是结构图

6.1.2新建微服务提供者cloudalibaba-provider-payment9003/9004(由于创建内容一样,故而值创建了9003,9004只是端口不同)

  • 创建maven模块,名为:cloudalibaba-provider-payment9003

  • 在pom中引入依赖
<dependencies>
        <!--引入自定义的api通用包-->
        <dependency>
            <groupId>com.augus.springcloud</groupId>
            <artifactId>cloud-api-common</artifactId>
            <version>1.0-SNAPSHOT</version>
        </dependency>

        <dependency>
            <groupId>com.alibaba.cloud</groupId>
            <artifactId>spring-cloud-starter-alibaba-nacos-discovery</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-starter-test</artifactId>
            <scope>test</scope>
        </dependency>
    </dependencies>
  • 创建配置文件application.yml,内容如下
server:
  port: 9003

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

management:
  endpoints:
    web:
      exposure:
        include: '*'
  • 创建主启动类
package com.augus.cloud;

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

@SpringBootApplication
@EnableDiscoveryClient //将服务器注册到nacos注册中心
public class CloudalibabaProvider9003Application {
    public static void main(String[] args) {
        SpringApplication.run(CloudalibabaProvider9003Application.class,args);
    }
}
  • 创建控制器方法

在controller包下创建DataController,内容如下:

package com.augus.cloud.controller;

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;

@RestController
public class DataController {
    @Value("${server.port}")
    private String serverPort;

    @GetMapping("/info/{id}")
    public String testInfo(@PathVariable("id") Long id){
        return "服务访问成功:"+serverPort+",当前账户id:"+id;
    }
}
  • 启动项目测试

在浏览器访问:http://localhost:9003/info/5,如下图:

6.1.3新建微服务消费者cloudalibaba-consumer-nacos-order8084

  • 创建maven模块,名为:cloudalibaba-consumer-nacos-order8084

  • 在pom.xml中引入依赖
<dependencies>
        <dependency>
            <groupId>org.springframework.cloud</groupId>
            <artifactId>spring-cloud-starter-openfeign</artifactId>
        </dependency>
        <dependency>
            <groupId>com.alibaba.cloud</groupId>
            <artifactId>spring-cloud-starter-alibaba-sentinel</artifactId>
        </dependency>
        <dependency>
            <groupId>com.alibaba.cloud</groupId>
            <artifactId>spring-cloud-starter-alibaba-nacos-discovery</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-starter-test</artifactId>
            <scope>test</scope>
        </dependency>

        <dependency>
            <groupId>com.augus.springcloud</groupId>
            <artifactId>cloud-api-common</artifactId>
            <version>${project.version}</version>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-devtools</artifactId>
            <scope>runtime</scope>
            <optional>true</optional>
        </dependency>
    </dependencies>
  • 创建主启动类
package com.augus.cloud;

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

@SpringBootApplication
@EnableDiscoveryClient //注册到nacos
public class ConsumerNacosOrder8084 {
    public static void main(String[] args) {
        SpringApplication.run(ConsumerNacosOrder8084.class,args);
    }
}
  • 创建配置类

由于要使用远程调用微服务,同时要开始负载均衡,所以在创建config包,里面创建 ApplicationContextConfig,代码如下:

package com.augus.cloud.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包,在下面创建DemoController,代码内容如下:
package com.augus.cloud.controller;

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 org.springframework.web.client.RestTemplate;

import javax.annotation.Resource;

@RestController
public class DemoController {
    //获取服务提供者URL
    @Value("${server-url.nacos-user-service}")
    private String URL;

    @Resource
    private RestTemplate restTemplate;

    @GetMapping("/consumer/fallback/{id}")
    public String fallback(@PathVariable Long id){
        System.out.println(URL+"/info/"+id);
        //通过Ribbon发起远程访问,访问9003/9004
        return restTemplate.getForObject(URL+"/info/"+id, String.class);
    }
}
  • 测试

在浏览器访问 http://localhost:8084/consumer/fallback/2,发现服务提供者9003和9004交替出现,如下图:

 

6.1.4.fallback属性

fallback 用于在抛出异常的时候提供 fallback 处理逻辑。fallback 函数可以针对所有类型的异常(除了 exceptionsToIgnore 里面排除掉的异常类型)进行处理。fallback 方法签名和位置要求:

  • 返回值类型必须与原方法返回值类型一致;

  • 方法参数列表需要和原方法一致,或者可以额外多一个 Throwable 类型的参数用于接收对应的异常。

  • fallback 函数默认需要和原方法在同一个类中。若希望使用其他类的函数,则可以指定 fallbackClass 为对应的类的 Class 对象,注意对应的函数必需为 static 函数,否则无法解析。

其实通过官网上提供的概念,我们不难看出这个属性类似于blockHandler,但是fallback属性和blockHandler属性的本质不同在于他们作用的异常不同:

  • blockHandler:针对违反Sentinel控制台配置规则时触发BlockException异常时对应处理的属性

  • fallback:针对Java本身出现的异常进行处理的对应属性。

对之前环境中 DemoController中代码进行修改,修改后如下:

package com.augus.cloud.controller;

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 org.springframework.web.client.RestTemplate;

import javax.annotation.Resource;

@RestController
public class DemoController {
    //获取服务提供者URL
    @Value("${server-url.nacos-user-service}")
    private String URL;

    @Resource
    private RestTemplate restTemplate;

    @GetMapping("/consumer/fallback/{id}")
    public String fallback(@PathVariable Long id){
        if(id<=5){
            //通过Ribbon 发起远程采用负载均衡访问9003、9004
            return restTemplate.getForObject(URL+"/info/"+id, String.class);
        }else {
            //输出这么一句
            throw new NullPointerException("查无记录,没有对应的数据记录");
        }
    }
}

上面代码添加了异常,然后重启项目,访问 http://localhost:8084/consumer/fallback/8(非法id)地址时,就会出现对应的显示效果:

 

上面对用户显示效果很差,可以通过@SentinelResource注解的fallback属性来解决这种java异常,给出友好提示,代码如下:

package com.augus.cloud.controller;

import com.alibaba.csp.sentinel.annotation.SentinelResource;
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 org.springframework.web.client.RestTemplate;

import javax.annotation.Resource;

@RestController
public class DemoController {
    //获取服务提供者URL
    @Value("${server-url.nacos-user-service}")
    private String URL;

    @Resource
    private RestTemplate restTemplate;

    @GetMapping("/consumer/fallback/{id}")
    @SentinelResource(value = "fallback", fallback = "fallbackHandler")
    public String fallback(@PathVariable Long id){
        if(id<=5){
            //通过Ribbon 发起远程采用负载均衡访问9003、9004
            return restTemplate.getForObject(URL+"/info/"+id, String.class);
        }else {
            //输出这么一句
            throw new NullPointerException("查无记录,没有对应的数据记录");
        }
    }

    //需要 方法签名基本保持一致,但是要添加异常类型参数
    public String fallbackHandler(Long id, Throwable e){
        return "目前查无此记录,请稍后重试";
    }

}

重启项目,在浏览器访问:http://localhost:8084/consumer/fallback/8,显示如下图:

使用我们执行的 fallbackHandler 里面来处理了异常

fallback属性主要是处理java代码的异常,同时它的设置和blockHandler属性类似,也可以设置fallbackClass属性,来指定对应类型,来处理对应的Java异常,当然要注意和blockHandlerClass属性一样,也需要让所有的方法都必需为 static 函数,否则无法解析。

6.1.5.blockHandler属性

设置blockHandler属性主要是为了对违反Sentinel控制台配置规则时触发BlockException异常时对应处理的属性,这个可以参考第五章节的内容,不在做讲解。

6.1.6.fallback和blockHandler同时设置

现在思考一个问题:如果我们在使用@SentinelResource属性的时候,同时设置blockHandler属性和fallback属性时,并且同时出现了Sentinel异常和Java异常,这个时候会执行哪个方法那?

  • 修改DemoController中的代码如下:同时添加SentinelResource注解的fallback和blockHandler属性
package com.augus.cloud.controller;

import com.alibaba.csp.sentinel.annotation.SentinelResource;
import com.alibaba.csp.sentinel.slots.block.BlockException;
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 org.springframework.web.client.RestTemplate;

import javax.annotation.Resource;

@RestController
public class DemoController {
    //获取服务提供者URL
    @Value("${server-url.nacos-user-service}")
    private String URL;

    @Resource
    private RestTemplate restTemplate;

    @GetMapping("/consumer/fallback/{id}")
    //同时添加SentinelResource注解的fallback和blockHandler属性
    @SentinelResource(value = "fallback", fallback = "fallbackHandler",blockHandler = "blockHandler")
    public String fallback(@PathVariable Long id){
        if(id<=5){
            //通过Ribbon 发起远程采用负载均衡访问9003、9004
            return restTemplate.getForObject(URL+"/info/"+id, String.class);
        }else {
            //输出这么一句
            throw new NullPointerException("查无记录,没有对应的数据记录");
        }
    }

    //处理代码中java异常
    public String fallbackHandler(Long id, Throwable e){
        return "目前查无此记录,请稍后重试--------处理代码中java异常";
    }

    //处理sentinel限流
    public String blockHandler(Long id, BlockException e){
        return "目前查无此记录,请稍后重试--------处理sentinel限流";
    }
}
  • 设置sentinel中流控规则

此时我们来设置Sentinel配置,我们通过流控规则中QPS请求数量演示(当然也可以用其他的),设置在一秒内QPS的值大于1次,就出发限流熔断规则

  • 重启项目,此时在浏览器访问:http://localhost:8084/consumer/fallback/8 如下,在没有出发流控规则之前的异常交给fallback来处理

  • 多次刷新浏览器,一旦触发熔断规则就变成了blockHandler来处理,如下图:

6.1.7.exceptionsToIgnore属性

exceptionsToIgnore属性:用于指定哪些异常被排除掉,不会计入异常统计中,也不会进入 fallback 逻辑中,而是会原样抛出。最终的效果是,加入排除了某个异常,一旦发生该异常,不会再有fallback方法兜底,就没有降级效果了

修改DemoController中的代码,加入异常排除

package com.augus.cloud.controller;

import com.alibaba.csp.sentinel.annotation.SentinelResource;
import com.alibaba.csp.sentinel.slots.block.BlockException;
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 org.springframework.web.client.RestTemplate;

import javax.annotation.Resource;

@RestController
public class DemoController {
    //获取服务提供者URL
    @Value("${server-url.nacos-user-service}")
    private String URL;

    @Resource
    private RestTemplate restTemplate;

    @GetMapping("/consumer/fallback/{id}")
    //同时添加SentinelResource注解的fallback和blockHandler属性
    @SentinelResource(value = "fallback",
            fallback = "fallbackHandler",
            blockHandler = "blockHandler",
            exceptionsToIgnore = {NullPointerException.class} //被标记的异常将会被原样抛出
    )
    public String fallback(@PathVariable Long id){
        if(id<=5){
            //通过Ribbon 发起远程采用负载均衡访问9003、9004
            return restTemplate.getForObject(URL+"/info/"+id, String.class);
        }else {
            //输出这么一句
            throw new NullPointerException("查无记录,没有对应的数据记录");
        }
    }

    //处理代码中java异常
    public String fallbackHandler(Long id, Throwable e){
        return "目前查无此记录,请稍后重试--------处理代码中java异常";
    }

    //处理sentinel限流
    public String blockHandler(Long id, BlockException e){
        return "目前查无此记录,请稍后重试--------处理sentinel限流";
    }
}

重启模块,在浏览器访问 http://localhost:8084/consumer/fallback/8,由于排除了 NullPointerException,异常类型,所以出现了NullPointerException异常,不会再有兜底的方法,显示如下:

6.2.Sentinel整合OpenFegin

在演示下面案例的时候,我用之前的版本出现了openFeign和sentinel不兼容,所以调整了版本,

springcloud:Hoxton.SR12
springcloudalibaba:2.2.7.RELEASE
springboot:2.3.12.RELEASE

6.2.1.引入OpenFegin

需要在当前的8084项目模块中引入对应的依赖:

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

6.2.2.激活Sentinel对OpenFeign的支持

在application.yml中添加如下配置:

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

添加后如下图:

6.2.3.修改主启动类

在主启动类上添加@EnableFeignClients注解,如下:

package com.augus.cloud;

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

@SpringBootApplication
@EnableDiscoveryClient //注册到nacos
@EnableFeignClients
public class ConsumerNacosOrder8084 {
    public static void main(String[] args) {
        SpringApplication.run(ConsumerNacosOrder8084.class,args);
    }
}

6.2.3.OpenFegin接口编写

这里我们的接口写法和之前保持一致,注意这里要多增加一个FeignClient的属性,在com.augus.cloud包下创建service包,在下面创建接口:

package com.augus.cloud.service;

import org.springframework.cloud.openfeign.FeignClient;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;

//当没有成功调用/info/{id}接口时会走fallback属性标注的类型的处理方法
@FeignClient(value = "nacos-payment-provider", fallback = FallbackFeignService.class)
public interface FeignService {

    /**
     * 远程调用对应的方法
     * @param id
     * @return
     */
    @GetMapping("/info/{id}")
    String testInfo(@PathVariable("id") Long id);
}

在创建上述接口的实现类 FallbackFeignService,在实现类中指定兜底的方法,代码如下:

package com.augus.cloud.service;

import org.springframework.stereotype.Service;

@Service
public class FallbackFeignService implements FeignService{

    @Override
    public String testInfo(Long id) {
        return "服务不可用,请稍后重试.......";
    }
}

6.2.4.编写控制器

在 DemoController,添加如下内容

 @Autowired
    private FeignService feignService;

    @GetMapping("/consumer/info/{id}")
    public String testInfo(@PathVariable("id") Long id){return feignService.testInfo(id);
    }

6.2.5.测试

此时如果我们访问 http://localhost:8084/consumer/info/8 的地址,可以成功访问

但是如果此时我们人为结束9003/9004服务,这个时候就会触发fallback属性对应的处理类型,完成服务降级。

七、Sentinel持久化配置

在Sentinel Dashboard中配置规则在服务重启后就会丢失,所以实际生产环境中需要配置规则的持久化实现,Sentinel提供多种不同的数据源来持久化规则配置,包括file,redis、nacos、zk。

实现目标:将限流规则持久化进Nacos保存,只要刷新8401某个接口地址,Sentinel控制台的流控规则就能感应到,同时只要Nacos里面的配置不删除,针对8401上Sentinel的流控规则就持续有效。

7.1.修改8401,在pom.xml中添加依赖

下面依赖就是处理sentinel和nacos持久化存储的依赖

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

7.2.在application.yml中添加 添加nacos数据源配置

这里就是要设置sentinel能连接到nacos的数据源

server:
  port: 8401 #端口
spring:
  application:
    name: springcloudalibaba-sentinel-service #服务名
  cloud:
    nacos:
      discovery:
        #Nacos服务注册中心(这是是Windows上的,也可以集群)地址
        server-addr: localhost:8848
    sentinel:
      transport:
        #配置 Sentinel dashboard 地址
        dashboard: localhost:8080
        #默认8719端口,假如被占用会自动从8719开始依次+1扫描,直至找到未被占用的端口
        port: 8719
      # web-context-unify: false # 关闭 context 整合
      datasource:
        ds1-flow: # 这个名字任意起
          nacos:
            server-addr: localhost:8848
            data-id: ${spring.application.name}  # 和 nacos中保持对应
            username: nacos # 用户名
            password: nacos # 密码
            # namespace: 9f977da3-20cc-4162-904b-2324b9bf31f6  # 和nacos中保持对应
            group-id: DEFAULT_GROUP # 和nacos中保持对应
            # flow代表流程控制,degrade代表熔断规则
            rule-type: flow
            # 对于 nacos 数据类型
            data-type: json
management:
  endpoints:
    web:
      exposure:
        include: '*'

如下图所示:

7.3.添加nacos业务配置规则

新增配置,DataID就写服务名,JSON配置内容如下:

[   
    {
        "resource": "test1",
        "limitApp": "default",
        "grade": 1,
        "count": 2,
        "strategy": 0,
        "controlBehavior": 0,
        "clusterMode": false
    }
]

上述参数说明:

  • resource:资源名称;
  • limitApp:来源应用;
  • grade:阈值类型,0表示线程数,1表示QPS;
  • count:单机阈值;
  • strategy:流控模式,0表示直接,1表示关联,2表示链路;
  • controlBehavior:流控效果,0表示快速失败,1表示Warm Up,2表示排队等待;
  • clusterMode:是否集群。

配置后如下图所示:

7.3.在controller包中创建控制器方法

这里添加是为了给后续测试使用,添加方法,同时设置降级兜底的方法

package com.augus.cloud.controller;

import com.alibaba.csp.sentinel.annotation.SentinelResource;
import com.alibaba.csp.sentinel.slots.block.BlockException;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RestController;

@RestController
public class OrderController {

    @GetMapping("/order")
    @SentinelResource(value = "order",blockHandler = "handleException")
    public String test1(){
        return "访问成功--------------order ";
    }
    //兜底处理方法
    public String handleException(BlockException exception){
        return "系统繁忙,请稍后重试!!!";
    }
}

7.4.设置流控规则

启动8401模块,在浏览器访问:http://localhost:8401/order,就会在Sentinel界面上看到对应的限流规则,同时添加流控规则,如下图:

7.5.测试

  • 快速访问测试接口 http://localhost:8401/order 触发流控规则,如下图:

  • 手动停止8401再看sentinel中设置的流控规则已经消失了

  • 重新启动8401,浏览器多访问几次, http://localhost:8401/order,发现出现了兜底处理

  • 再看sentinel,流控规则再次出现了,如下图:

posted @ 2023-02-08 16:53  酒剑仙*  阅读(202)  评论(0)    收藏  举报