文章目录
sentinel降级熔断原理探究
背景
最近依赖的系统偶尔会出现超时的情况,由于该依赖属于弱依赖,所以笔者决定在依赖系统超时的情况下对该依赖进行熔断降级。
目前使用较多的限流与降低较多的框架是Hystrix与Sentinel。两者的具体对比可以参考sentinel与Hystrix对比
Sentinel支持响应平均RT等多种方式降级,支持公司目前使用的duboo框架,并且引入依赖简单配置即可快速使用,所以本次使用sentinel作为降级工具。
由于之前对sentinel的熔断降级具体原理不甚了解,导致本地调试时没有达到想要的熔断目的。当时满脸问号,源码面前无秘密,决定对它的原理一探究竟。
熔断降级应用
Sentinel通过定义的资源保护具体的业务代码或其他后方服务,用户只需要为受保护的代码或服务定义一个资源,然后定义规则就可以了。常用的SphU.entry("resourceName")
会对业务代码造成入侵,一般用注解@SentinelResource
的方式。
下面通过一个简单的栗子介绍Sentinel降级的使用方法。
假设我们有个获取用户信息的接口,如下
@RestController
@RequestMapping("/api/user")
public class SentinelTestController {
@Autowired
private UserService userService;
@GetMapping("/detail")
public BaseResponse<User> info(@RequestParam("id") Long userId) {
User user = userService.getDetails(userId);
return BaseResponse.success(user);
}
}
获取用户信息的方法userService.getDetails(useId)
方法如下,当触发降级时,自动调用blockHandlerMethod
方法。
在spring应用中,注意@SentinelResource不能用于内部调用的方法;原因是类的内部方法调用是进入不了aop的。
@Service
public class UserServicesImpl implements UserService {
@Override
@SentinelResource(value = "user.test", blockHandler = "blockHandlerMethod")
public User getDetails(Long id) {
try {
//睡眠模拟执行时长
TimeUnit.MILLISECONDS.sleep(10);
} catch (InterruptedException e) {
e.printStackTrace();
}
return new User(id);
}
// 熔断降级对应的处理方法
public User blockHandlerMethod(Long id, BlockException e) {
System.out.println("blockHandlerMethod invoke");
return null;
}
}
这里我们用TimeUnit.MILLISECONDS.sleep(10)
来模拟调用依赖方法所需的时间,触发熔断条件。用@SentinelResource
定义资源,并对该资源配置相应的降级策略。降级配置如下
{
"resource": "user.test", //资源名,与@SentinelResource中的value保持一致
"count": 5, //阈值,当策略为RT时表示5ms
"grade": 0, //熔断降级策略,支持秒级 RT(0)/秒级异常比例(1)/分钟级异常数(2)
"timeWindow": 10 //降级的时间,单位为s
}
由于本文使用的sentinel版本小于1.7.0,所以没有
rtSlowRequestAmount
配置,该配置为RT模式下1 秒内连续多少个请求的平均RT超出阈值方可触发熔断,默认为5。
最后由于本应用是基于Springboot,需要将SentinelResourceAspect
注册为一个bean,代码如下
@Configuration
public class SentinelAspectConfiguration {
@Bean
public SentinelResourceAspect sentinelResourceAspect() {
return new SentinelResourceAspect();
}
}
以上就完成了对一个方法进行熔断降级的初步栗子。
接下来尝试熔断降级的效果。当连续多次访问接口后,成功触发熔断机制,如下图
在这里要说下自己为何最开始在测试环境没有模拟出熔断!由于Sentinel的平均rt超时熔断是基于秒级的,也就是说它会统计滑动窗口1秒内请求的平均耗时,当平均耗时大于设定阈值时,不会马上熔断,而是会将超时通过的passCount
加1。当该秒内无请求或平均耗时小于阈值时,passCount
会重置为0。只有当passCount
大于等于5时,才会触发熔断机制。当时访问的速度和次数不够导致无法熔断。
接下来详细解析。
熔断降级原理
通过一个简单的示例程序,我们了解了sentinel可以对请求进行熔断降价。现在我们就拨开云雾,深入源码内部去一窥sentinel熔断降级的实现原理吧。
@SentinelResource注解解析
首选我们看下@SentinelResource
注解,它是Sentinel用于定义资源的注解,并提供了可选的异常处理和 fallback 配置项。该注解源码与解释如下,具体可见官网。
@Target({
ElementType.METHOD})
@Retention(RetentionPolicy.RUNTIME)
@Inherited
public @interface SentinelResource {
// Sentinel资源的名称
String value() default "";
//资源调用的流量类型,是入口流量EntryType.IN,还是出口流量EntryType.OUT。默认为 EntryType.OUT
EntryType entryType() default EntryType.OUT;
//blockHandler对应处理 BlockException 的函数名称,可选项。
//blockHandler 函数访问范围需要是 public,返回类型需要与原方法相匹配,参数类型需要和原方法相匹配并且最后加一个额外的参数,类型为 BlockException。
//blockHandler 函数默认需要和原方法在同一个类中
String blockHandler() default "";
//当blockHandler函数在其他类中时,需要指明函数对应的类的 Class 对象,并且对应的函数必需为 static 函数。
Class<?>[] blockHandlerClass() default {
};
//fallback 函数名称,用于在抛出异常的时候提供 fallback 处理逻辑。
//函数返回类型与参数列表需与原函数一致,方法入参可以额外多一个 Throwable 类型的参数用于接收对应的异常。
String fallback() default "";
//用于通用的 fallback 逻辑(即可以用于很多服务或方法)
//若同时配置了 fallback 和 defaultFallback,则只有 fallback 会生效。
String defaultFallback() default "";
//当fallback函数与原方法不在同一个类中,通过fallbackClass指定class
Class<?>[] fallbackClass() default {
};
//用于指定哪些异常被统计
Class<? extends Throwable>[] exceptionsToTrace() default {
Throwable.class};
//用于指定哪些异常被排除掉,不会计入异常统计中,也不会进入 fallback 逻辑中,而是会原样抛出。
Class<? extends Throwable>[] exceptionsToIgnore() default {
};
}
注意当blockHandler
和 fallback
都进行了配置,则被限流降级而抛出 BlockException
时只会进入 blockHandler
处理逻辑,之后在源码中可以看出。若未配置 blockHandler
、fallback
和 defaultFallback
,则被限流降级时会将 BlockException
直接抛出。
@SentinelResource注解执行流程
在执行@SentinelResource
注解的方法前,该注解对应的SentinelResourceAspect
切面进行拦截,判定是否触发限流或降级。我们来看下这个切面的实现:
@Aspect
public class SentinelResourceAspect extends AbstractSentinelAspectSupport {
//将@SentinelResource注解定义为切点
@Pointcut("@annotation(com.alibaba.csp.sentinel.annotation.SentinelResource)")
public void sentinelResourceAnnotationPointcut() {
}
@Around("sentinelResourceAnnotationPointcut()")
public Object invokeResourceWithSentinel(ProceedingJoinPoint pjp) throws Throwable {
Method originMethod = resolveMethod(pjp);
//获取注解
SentinelResource annotation = originMethod.getAnnotation(SentinelResource.class);
if (annotation == null) {
throw new IllegalStateException("Wrong state for SentinelResource annotation");
}
//1. 获取资源名称,@SentinelResource注解上带有value则直接取,否则解析方法名作为资源名
String resourceName = getResourceName(annotation.value(), originMethod);
//获取流量类型
EntryType entryType = annotation.entryType();
Entry entry = null;
try {
//2. 申请entry进入资源,如果申请成功,则表明没有限流或降级
entry = SphU.entry(resourceName, entryType, 1, pjp.getArgs());
//执行原方法
Object result = pjp.proceed();
//返回结果
return result;
} catch (BlockException ex) {
//3. entry资源申请不成功,抛出BlockException,根据注解上定义的异常处理函数处理该异常
return handleBlockException(pjp, annotation, ex);
//4. 处理非BlockException异常
} catch (Throwable ex) {
//获取注解上定义的exceptionsToIgnore
Class<? extends Throwable>[] exceptionsToIgnore = annotation.exceptionsToIgnore();
//如果在列表中,则直接抛出,不会计入异常统计中,也不会进入 fallback 逻辑中
if (exceptionsToIgnore.length > 0 && exceptionBelongsTo(ex, exceptionsToIgnore)) {
throw ex;
}
//如果属于注解上的exceptionsToTrace标记的异常,则计入异常统计,并执行fallback 逻辑中
if (exceptionBelongsTo(ex, annotation.exceptionsToTrace())) {
traceException(ex, annotation);
return handleFallback(pjp, annotation, ex);
}
//否则直接抛出
throw ex;
} finally {