@SentinelResource
之前我们仅依靠引入Spring Cloud Alibaba对Sentinel的整合封装spring-cloud-starter-alibaba-sentinel
,就完成了对所有Spring MVC接口的限流控制。然而,在实际应用过程中,我们可能需要限流的层面不仅限于接口。可能对于某个方法的调用限流,对于某个外部资源的调用限流等都希望做到控制。
对此,我们需要学习使用@SentinelResource
注解,灵活的定义控制资源以及如何配置控制策略。
自定义资源点
1、导入sentinel依赖:
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-alibaba-sentinel</artifactId>
</dependency>
第一步:在应用主类中增加注解支持的配置:
@SpringBootApplication
public class TestApplication {
public static void main(String[] args) {
SpringApplication.run(TestApplication.class, args);
}
// 注解支持的配置Bean
@Bean
public SentinelResourceAspect sentinelResourceAspect() {
return new SentinelResourceAspect();
}
}
第二步:在需要通过Sentinel来控制流量的地方使用@SentinelResource注解,比如下面以控制Service逻辑层的某个方法为例:
@Slf4j
@Service
public class TestService {
@SentinelResource(value = "doSomeThing")
public void doSomeThing(String str) {
log.info(str);
}
}
到这里一个需要被保护的方法就定义完成了。
如何实现限流与熔断降级
在定义了资源点之后,我们就可以通 过Dashboard来设置限流和降级策略来对资源点进行保护了。同时,也可以通过@SentinelResource
来指定出现限流和降级时候的异常处理策略。
实现限流控制
第一步:在Web层调用这个被保护的方法:
@RestController
public class TestController {
@Autowired
private TestService testService;
@GetMapping("/hello")
public String hello() {
estService.doSomeThing("hello " + new Date());
return "didispace.com";
}
}
第二步:启动测试应用,启动Sentinel-Dashboard。发一个请求到/hello接口上,使得Sentinel-Dashboard上可以看到如下图所示的几个控制点:
可以看到,除了有/hello
资源点之外,多了一个doSomeThing
资源点。可以通过界面为这个资源点设置限流规则,比如将其QPS设置为2。由于/hello资源不设置限流规则,所以只要请求/hello接口,就可以直接模拟调用doSomeThing资源,来观察限流规则是否生效。
此时,服务端的控制台也会有对应的限流报错日志:
INFO 36898 --- [nio-8001-exec-3] c.d.a.sentinel.service.TestService : aaa
ERROR 36898 --- [nio-8001-exec-4] o.a.c.c.C.[.[.[/].[dispatcherServlet] : Servlet.service() for servlet [dispatcherServlet] in context with path [] threw exception [Request processing failed; nested exception is java.lang.reflect.UndeclaredThrowableException] with root cause
com.alibaba.csp.sentinel.slots.block.flow.FlowException: null
实现限流的异常处理
默认情况下,Sentinel对控制资源的限流处理是直接抛出异常【即上面日志抛出的异常】。在没有合理的业务承接或者前端对接情况下可以这样,但是正常情况为了更好的用户业务,都会实现一些被限流之后的特殊处理,我们不希望展示一个生硬的报错。那么只需要基于上面的例子做一些加工,比如:
@Slf4j
@Service
public class TestService {
@SentinelResource(value = "doSomeThing", blockHandler = "exceptionHandler")
public void doSomeThing(String str) {
log.info(str);
}
// 限流与阻塞处理
public void exceptionHandler(String str, BlockException ex) {
log.error( "blockHandler:" + str, ex);
}
}
主要做了两件事:
- 通过
@SentinelResource
注解的blockHandler
属性制定具体的处理函数 - 实现处理函数,该函数的传参必须与资源点的传参一样,并且最后加上
BlockException
异常参数;同时,返回类型也必须一样。
此时前端就不会返回异常信息了,后端会打印exceptionHandler中定义的日志输出。
实现熔断降级
@SentinelResource
注解除了可以用来做限流控制之外,还能实现与Hystrix类似的熔断降级策略。下面就来具体看看如何使用吧。
什么是熔断降级
除了流量控制以外,降低调用链路中的不稳定资源也是 Sentinel 的使命之一。由于调用关系的复杂性,如果调用链路中的某个资源出现了不稳定,最终会导致请求发生堆积。这个问题和 Hystrix 里面描述的问题是一样的。
Sentinel 和 Hystrix 的原则是一致的: 当调用链路中某个资源出现不稳定,例如,表现为 timeout,异常比例升高的时候,则对这个资源的调用进行限制,并让请求快速失败,避免影响到其它的资源,最终产生雪崩的效果。
在限制的手段上,Sentinel 和 Hystrix 采取了完全不一样的方法。
Hystrix: 线程池隔离
Sentinel:通过并发线程数进行限制、通过响应时间对资源进行降级
第一步:与限流控制一样,使用@SentinelResource
注解标记资源点,比如:
@Slf4j
@Service
public class TestService {
@SentinelResource(value = "doSomeThing2")
public void doSomeThing2(String str) {
log.info(str);
throw new RuntimeException("发生异常");
}
}
这里在TestService
类中创建了一个新的方法,并使用@SentinelResource
将该资源命名为doSomeThing2
。该方法会抛出异常,以配合后续制定基于异常比例的降级策略(类似Hystrix)。Sentinel相比Hystrix更丰富,还有基于响应时间和异常数的降级策略。
第二步:在Web层调用这个被保护的方法:
@RestController
public class TestController {
@Autowired
private TestService testService;
@GetMapping("/hello2")
public String hello2() {
testService.doSomeThing2("hello2 " + new Date());
return "didispace.com";
}
}
第三步:启动测试应用,启动Sentinel-Dashboard。发一个请求到/hello2接口上,使得Sentinel-Dashboard上可以看到名为doSomeThing2的资源点。然后点击”降级“按钮,为该资源设置降级规则。这里使用异常比例策略,比例设置为0.5(即:50%的异常率),时间窗口设置为2(秒)。
第四步:验证熔断降级,根据上面的降级策略配置,当doSomeThing2方法的调用QPS >= 5,如果异常率超过50%,那么后续2秒内的调用将直接出发熔断降级,默认情况会直接抛出DegradeException
异常,比如:
ERROR 99863 --- [nio-8001-exec-2] o.a.c.c.C.[.[.[/].[dispatcherServlet] : Servlet.service() for servlet [dispatcherServlet] in context with path [] threw exception [Request processing failed; nested exception is java.lang.reflect.UndeclaredThrowableException] with root cause
com.alibaba.csp.sentinel.slots.block.degrade.DegradeException: null
与限流相比,限流抛出的异常是com.alibaba.csp.sentinel.slots.block.flow.FlowException: null
而熔断降级抛出的异常是com.alibaba.csp.sentinel.slots.block.degrade.DegradeException: null
熔断的降级处理
在Sentinel
中定义熔断的降级处理方法非常简单,与Hystrix非常相似。只需要使用@SentinelResource
注解的fallback
属性来指定具体的方法名即可。这里也需要注意传参与返回必须一致。比如:
@Slf4j
@Service
public class TestService {
// 熔断与降级处理
@SentinelResource(value = "doSomeThing2", fallback = "fallbackHandler")
public void doSomeThing2(String str) {
log.info(str);
throw new RuntimeException("发生异常");
}
public void fallbackHandler(String str) {
log.error("fallbackHandler:" + str);
}
}
完成上面的改造之后,重启应用,并设置doSomeThing2
资源的熔断降级策略(使用异常百分比),然后频繁的请求/hello2接口。在QPS>=5之后,由于这个接口一直在抛出异常,所以一定会满足熔断降级条件,这时候就会执行fallbackHandler
方法,不断的打印如下日志:
ERROR 58471 --- [nio-8001-exec-1] c.d.a.sentinel.service.TestService : fallbackHandler:hello2 Thu Jun 27 23:44:19 CST 2019
ERROR 58471 --- [nio-8001-exec-2] c.d.a.sentinel.service.TestService : fallbackHandler:hello2 Thu Jun 27 23:44:19 CST 2019
ERROR 58471 --- [nio-8001-exec-3] c.d.a.sentinel.service.TestService : fallbackHandler:hello2 Thu Jun 27 23:44:19 CST 2019
ERROR 58471 --- [nio-8001-exec-4] c.d.a.sentinel.service.TestService : fallbackHandler:hello2 Thu Jun 27 23:44:19 CST 2019
ERROR 58471 --- [nio-8001-exec-5] c.d.a.sentinel.service.TestService : fallbackHandler:hello2 Thu Jun 27 23:44:20 CST 2019
@SentinelResource注解 更多属性说明:
value
:资源名称,必需项(不能为空)entryType
:entry 类型,可选项(默认为 EntryType.OUT)blockHandler / blockHandlerClass
:blockHandler
对应处理BlockException
的函数名称,可选项。blockHandler
函数访问范围需要是public
,返回类型需要与原方法相匹配,参数类型需要和原方法相匹配并且最后加一个额外的参数,类型为BlockException
。
blockHandler
函数默认需要和原方法在同一个类中。若希望使用其他类的函数,则可以指定blockHandlerClass
为对应的类的 Class 对象,注意对应的函数必需为static
函数,否则无法解析。fallback
:fallback 函数名称,可选项,用于在抛出异常的时候提供 fallback 处理逻辑。fallback 函数可以针对所有类型的异常(除了exceptionsToIgnore
里面排除掉的异常类型)进行处理。fallback 函数签名和位置要求:- 返回值类型必须与原函数返回值类型一致;
- 方法参数列表需要和原函数一致,或者可以额外多一个
Throwable
类型的参数用于接收对应的异常。 - fallback 函数默认需要和原方法在同一个类中。若希望使用其他类的函数,则可以指定
fallbackClass
为对应的类的 Class 对象,注意对应的函数必需为 static 函数,否则无法解析。
defaultFallback
(since 1.6.0):默认的 fallback 函数名称,可选项,通常用于通用的 fallback 逻辑(即可以用于很多服务或方法)。默认 fallback 函数可以针对所有类型的异常(除了exceptionsToIgnore里面排除掉的异常类型)进行处理。若同时配置了 fallback 和 defaultFallback,则只有 fallback 会生效。defaultFallback 函数签名要求:- 返回值类型必须与原函数返回值类型一致;
- 方法参数列表需要为空,或者可以额外多一个
Throwable
类型的参数用于接收对应的异常。 defaultFallback
函数默认需要和原方法在同一个类中。若希望使用其他类的函数,则可以指定fallbackClass
为对应的类的 Class 对象,注意对应的函数必需为static
函数,否则无法解析。
exceptionsToIgnore
(since 1.6.0):用于指定哪些异常被排除掉,不会计入异常统计中,也不会进入 fallback 逻辑中,而是会原样抛出。
注:1.6.0 之前的版本 fallback 函数只针对降级异常(
DegradeException
)进行处理,不能针对业务异常进行处理。
特别地,若 blockHandler 和 fallback 都进行了配置,则被限流降级而抛出 BlockException
时只会进入 blockHandler
处理逻辑。若未配置 blockHandler
、fallback
和 defaultFallback
,则被限流降级时会将 BlockException
直接抛出。
Sentinel 配置项
application.yaml
配置文件中Sentinel
配置项:
配置文件如下:
spring:
application:
name: demo-provider
cloud:
# Sentinel 配置项,对应 SentinelProperties 配置属性类
sentinel:
enabled: true # 是否开启。默认为 true 开启
eager: true # 是否饥饿加载。默认为 false 关闭
transport:
dashboard: 127.0.0.1:7070 # Sentinel 控制台地址
filter:
url-patterns: /** # 拦截请求的地址。默认为 /*
Sentinel 配置项,以 spring.cloud.sentinel
开头,对应SentinelProperties
配置属性类。
enabled
配置项,设置是否开启Sentinel
,默认为 true 开启,所以一般不用主动设置。如果胖友关闭 Sentinel 的功能,例如说在本地开发的时候,可以设置为 false 关闭。eager
配置项,设置是否饥饿加载,默认为 false 关闭。默认情况下,Sentinel 是延迟初始化,在首次使用到 Sentinel 才进行初始化。通过设置为 true 时,在项目启动时就会将 Sentinel 直接初始化,完成向 Sentinel 控制台进行注册。transport.dashboard
配置项,设置 Sentinel 控制台地址。filter.url-patterns
配置项,设置拦截请求的地址,默认为/*
。
在 Sentinel 的子项目 sentinel-spring-webmvc-adapter
中,对 SpringMVC
进行适配,通过 SentinelWebInterceptor
拦截器,实现对 SpringMVC 的请求的拦截,使用 Sentinel 进行保护。通过 filter.url-patterns
配置项,可以定义该拦截器的拦截请求地址。
不过要注意,因为 filter.url-patterns
配置项的默认值为 /*
,只能拦截根目录的请求,显然不满足我们的日常需求,因此修改成了 /**
拦截所有请求。
BlockException 处理器
BlockException 是一个异常抽象基类,其有 5 个实现类,刚好对应 Sentinel 的 5 种流量控制手段,如下图所示: