赞
踩
最近依赖的系统偶尔会出现超时的情况,由于该依赖属于弱依赖,所以笔者决定在依赖系统超时的情况下对该依赖进行熔断降级。
目前使用较多的限流与降低较多的框架是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
注解,它是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
注解的方法前,该注解对应的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
赞
踩
Copyright © 2003-2013 www.wpsshop.cn 版权所有,并保留所有权利。