最近依赖的系统偶尔会出现超时的情况,由于该依赖属于弱依赖,所以笔者决定在依赖系统超时的情况下对该依赖进行熔断降级。
目前使用较多的限流与降低较多的框架是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 {
//退出资源
if (entry != null) {
entry.exit(1, pjp.getArgs());
}
}
}
}
SentinelResourceAspect
的执行逻辑主要分为以下四步
@SentinelResource
注解中获取,没有则从方法名中解析。SphU.entry()
申请进入资源,进入成功则执行原方法并返回结果BlockException
异常。BlockException
异常,则获取@SentinelResource
注解上的异常处理类型与方式,处理异常。上述第三步中的handleBlockException()
方法是处理BlockException
异常,源码如下:
// 处理BlockException异常
protected Object handleBlockException(ProceedingJoinPoint pjp, SentinelResource annotation, BlockException ex)
throws Throwable {
// 提取配置的blockHandler方法
Method blockHandlerMethod = extractBlockHandlerMethod(pjp, annotation.blockHandler(),
annotation.blockHandlerClass());
//如果blockHandler方法不为空,则执行该方法
if (blockHandlerMethod != null) {
// 获取标记注解的业务方法上的参数
Object[] originArgs = pjp.getArgs();
//拷贝并添加BlockException为最后一个参数
Object[] args = Arrays.copyOf(originArgs, originArgs.length + 1);
args[args.length - 1] = ex;
//根据是否是静态方法执行处理逻辑并返回
if (isStatic(blockHandlerMethod)) {
return blockHandlerMethod.invoke(null, args);
}
return blockHandlerMethod.invoke(pjp.getTarget(), args);
}
// 如果没有配置blockhandler方法,则执行配置的fallback方法
return handleFallback(pjp, annotation, ex);
}
处理BlockException异常的主要逻辑如下
blockHandler
方法,blockHandler
方法则执行该方法blockHandler
方法则执行fallBack
方法。所以上文说过,当blockHandler
和 fallback
都进行了配置,则被限流降级而抛出 BlockException
时只会进入 blockHandler
处理逻辑。
这里的其他方法不再继续做展开,我们主要再看下handleFallback
方法,它与处理非BlockException
异常的方法相同,具体实现逻辑如下:
protected Object handleFallback(ProceedingJoinPoint pjp, SentinelResource annotation, Throwable ex)
throws Throwable {
return handleFallback(pjp, annotation.fallback(), annotation.defaultFallback(), annotation.fallbackClass(), ex);
}
protected Object handleFallback(ProceedingJoinPoint pjp, String fallback, String defaultFallback, Class<?>[] fallbackClass, Throwable ex) throws Throwable {
Object[] originArgs = pjp.getArgs();
// 提取 fallback 方法
Method fallbackMethod = extractFallbackMethod(pjp, fallback, fallbackClass);
if (fallbackMethod != null) {
// 获取方法参数个数
int paramCount = fallbackMethod.getParameterTypes().length;
Object[] args;
//确认fallback方法是否包含Throwable类型的参数
if (paramCount == originArgs.length) {
args = originArgs;
} else {
args = Arrays.copyOf(originArgs, originArgs.length + 1);
args[args.length - 1] = ex;
}
//根据是否是静态方法执行处理逻辑并返回
if (isStatic(fallbackMethod)) {
return fallbackMethod.invoke(null, args);
}
return fallbackMethod.invoke(pjp.getTarget(), args);
}
// 如果没有配置fallback方法,则执行defaultFallback方法
return handleDefaultFallback(pjp, defaultFallback, fallbackClass, ex);
}
handleFallback
的方法的执行逻辑与处理handleBlockException
的逻辑基本类似,获取注解上配置的fallback
方法,如果有则根据是否是静态方法执行处理逻辑并返回。否则执行handleDefaultFallback
方法。handleDefaultFallback
方法不再详细展开,其实就是获取注解上配置的defaultFallback
方法,有则执行,没有则直接抛出异常。
综上,处理BlockException
的优先级如下
处理BlockException的优先级顺序为
blockHandler
>fallBack
>defaultFallback
> throw ex,并只会执行其中一个。
在了解@SentinelResource
注解的整理执行逻辑后,接下来,我们将详细分析Sentinel的降级原理。在SentinelResource
切面的执行流程中,申请资源的SphU.entry()
是限流,降级的入口方法。我们将从该方法深入,了解Sentinel降级的奥秘。
限流,降级的入口方法SphU.entry()
。这个方法会对指定资源申请一个entry
,如果能够申请成功,则说明没有被限流或降级,否则会抛出BlockException。那么它是怎么定义资源能否申请成功呢,我们继续往下。
从 SphU.entry()
方法往下执行会进入到 Env.sph.entry()
,Env.sph
为Sph
类型 ,Sph是一个接口,它的默认实现类是 CtSph
,所以SphU.entry()
最终会执行到在CtSph
中的entry
方法:
@Override
public Entry entry(String name, EntryType type, int count, Object... args) throws BlockException {
StringResourceWrapper resource = new StringResourceWrapper(name, type);
return entry(resource, count, args);
}
该方法根据注解上定义的资源名与资源调用的流量类型创建了一个StringResourceWrapper
资源对象,并调用了重载的entry
方法。这里我们先理解一下StringResourceWrapper
。
在上文中我们已经说过sentinel
是通过资源resource
来保护具体的业务代码。而在sentinel
中具体表示资源的类是:ResourceWrapper
,它是一个抽象的包装类,包装了资源的名称 Name
和资源调用的流量类型EntryType
。他有两个实现类,分别是:StringResourceWrapper
和 MethodResourceWrapper
。顾名思义,StringResourceWrapper
是通过将字符串包装为资源,是一个通用的资源包装类,MethodResourceWrapper
是对方法的包装。结构图如下
创建了StringResourceWrapper
资源对象后,重载的entry
方法源码如下
public Entry entry(ResourceWrapper resourceWrapper, int count, Object... args) throws BlockException {
return entryWithPriority(resourceWrapper, count, false, args);
}
private Entry entryWithPriority(ResourceWrapper resourceWrapper, int count, boolean prioritized, Object... args) throws BlockException {
//获取当前线程的上下文;context是保存在ThreadLocal中的,每次执行的时候会优先到ThreadLocal中获取
Context context = ContextUtil.getContext();
// 上下文名称对应的入口节点是否已经超过阈值2000,超过则会返回空 CtEntry。
// 不会进行规则校验
if (context instanceof NullContext) {
return new CtEntry(resourceWrapper, null, context);
}
//如果当前执行时还没有 context,那么会使用默认的 context,通过 MyContextUtil.myEnter() 创建的。
if (context == null) {
context = MyContextUtil.myEnter(Constants.CONTEXT_DEFAULT_NAME, "", resourceWrapper.getType());
}
// Global switch is close, no rule checking will do.
if (!Constants.ON) {
return new CtEntry(resourceWrapper, null, context);
}
// 获取插槽链
ProcessorSlot<Object> chain = lookProcessChain(resourceWrapper);
//表示资源(插槽链)超过6000,因此不会进行规则检查。
if (chain == null) {
return new CtEntry(resourceWrapper, null, context);
}
// 生成 Entry 对象
Entry e = new CtEntry(resourceWrapper, chain, context);
try {
// 开始执行插槽链 调用逻辑
chain.entry(context, resourceWrapper, null, count, prioritized, args);
} catch (BlockException e1) {
// 清除上下文
e.exit(count, args);
//抛出BlockExecption异常,上层捕获
throw e1;
} catch (Throwable e1) {
RecordLog.info("Sentinel unexpected exception", e1);
}
return e;
}
entryWithPriority
包含了Sentinel的核心逻辑,主要步骤为
为了便于后续理解,我们先简单了解下sentinel中slot。
sentinel的工作流程是围绕着一个个插槽slot所组成的插槽链slotchain来展开的。每个插槽都有自己的职责,通过一定的编排顺序,来达到最终的限流降级的目的。slot的执行顺序有一定要求,因为有的slot需要依赖其他的slot计算出来的结果才能进行工作。常见的7个slot及其功能职责如下:
NodeSelectorSlot
: 负责收集资源的路径,并将这些资源的调用路径,以树状结构存储起来,用于根据调用路径来限流降级;ClusterBuilderSlot
: 用于存储资源的统计信息以及调用者信息,例如该资源的 RT, QPS, thread count 等等,这些信息将用作为多维度限流,降级的依据;StatistcSlot
: Sentinel 的核心功能插槽之一,用于统计实时的调用数据。FlowSlot
: 用于根据预设的限流规则,以及前面 slot 统计的状态,来进行限流;AuthorizationSlot
: 根据黑白名单,来做黑白名单控制;DegradeSlot
: 通过统计信息,以及预设的规则,来做熔断降级;SystemSlot
:根据对于当前系统的整体情况,对入口资源的调用进行动态调配。详细的介绍可以参考Sentinel的Slot介绍
其中插槽链的获取逻辑在lookProcessChain(resourceWrapper)
,我们接下来看该方法逻辑
ProcessorSlot<Object> lookProcessChain(ResourceWrapper resourceWrapper) {
//根据资源尝试从全局缓存中获取,这里为传入的StringResourceWrapper
ProcessorSlotChain chain = chainMap.get(resourceWrapper);
if (chain == null) {
//双重检查锁
synchronized (LOCK) {
chain = chainMap.get(resourceWrapper);
if (chain == null) {
// 判断插槽链数是否大于6000
if (chainMap.size() >= Constants.MAX_SLOT_CHAIN_SIZE) {
return null;
}
// 初始化插槽链
chain = SlotChainProvider.newSlotChain();
//将该插槽链添加到全局缓存map中
Map<ResourceWrapper, ProcessorSlotChain> newMap = new HashMap<ResourceWrapper, ProcessorSlotChain>(
chainMap.size() + 1);
newMap.putAll(chainMap);
newMap.put(resourceWrapper, chain);
chainMap = newMap;
}
}
}
return chain;
}
获取slotChain方法的主要逻辑为
我们接下来详细了解下创建插槽链的源码
private static volatile SlotChainBuilder builder = null;
public static ProcessorSlotChain newSlotChain() {
// 判断builder是否已经初始化过
if (builder != null) {
return builder.build();
}
//加载SlotChain
resolveSlotChainBuilder();
//如果加载之后builder仍为null,使用默认DefaultSlotChainBuilder
if (builder == null) {
RecordLog.warn("[SlotChainProvider] Wrong state when resolving slot chain builder, using default");
builder = new DefaultSlotChainBuilder();
}
//返回构建的插槽链
return builder.build();
}
/**
* 通过SPI机制加载自定义SlotChainBuilder
*/
private static void resolveSlotChainBuilder() {
List<SlotChainBuilder> list = new ArrayList<SlotChainBuilder>();
boolean hasOther = false;
// 尝试获取自定义SlotChainBuilder,通过JAVA SPI机制扩展
for (SlotChainBuilder builder : LOADER) {
if (builder.getClass() != DefaultSlotChainBuilder.class) {
hasOther = true;
list.add(builder);
}
}
//如果有自定义的SlotChainBuilder,利用该builder构建slotchain
//否则,使用默认的DefaultSlotChainBuilder
if (hasOther) {
builder = list.get(0);
} else {
builder = new DefaultSlotChainBuilder();
}
RecordLog.info("[SlotChainProvider] Global slot chain builder resolved: "
+ builder.getClass().getCanonicalName());
}
创建插槽链的主要流程为
SlotChainBuilder
是插槽链构建器接口,该接口用于构建插槽链中的插槽类型与顺序,提供了三个实现类,并且支持自定义扩展,即通过实现 SlotsChainBuilder
接口加入自定义的 slot
并自定义编排各个 slot 之间的顺序,从而可以给 sentinel 添加自定义的功能。类图如下
本文中暂不对自定义扩展进行展开介绍,我们主要看下最常用的DefaultSlotChainBuilder
。
Chain是链条的意思,从build的方法可看出,ProcessorSlotChain是一个链表,里面添加了很多个Slot。
public class DefaultSlotChainBuilder implements SlotChainBuilder {
@Override
public ProcessorSlotChain build() {
ProcessorSlotChain chain = new DefaultProcessorSlotChain();
chain.addLast(new NodeSelectorSlot());
chain.addLast(new ClusterBuilderSlot());
chain.addLast(new LogSlot());
chain.addLast(new StatisticSlot());
chain.addLast(new SystemSlot());
chain.addLast(new AuthoritySlot());
chain.addLast(new FlowSlot());
chain.addLast(new DegradeSlot());
return chain;
}
}
该builder
方法主要构建了一个DefaultProcessorSlotChain
对象,并将7个默认的Slot依次加到chain的末尾,形成slot责任链,最终整个链表的结构图如下(引用 https://www.jianshu.com/p/0e218ef7f505
)first和end是指向链表的头结点和尾节点。
通过lookProcessChain
获取得到SlotChain
责任链后,执行chain.entry()
方法依次执行Slot的entry
方法。我们看下chain.entry
是如何顺序执行各Slot的entry
方法的。
此处lookProcessChain
方法获得的ProcessorSlotChain
的实例是DefaultProcessorSlotChain
,那么执行chain.entry
方法,就会执行DefaultProcessorSlotChain
的entry
方法,该方法如下
@Override
public void entry(Context context, ResourceWrapper resourceWrapper, Object t, int count, boolean prioritized, Object... args) throws Throwable {
first.transformEntry(context, resourceWrapper, t, count, prioritized, args);
}
chain.entry
实际上是执行执行了first属性的transformEntry
方法。该方法就是执行当前当前Slot节点的entry
方法,从上图中我们了解到first的下个Slot是 NodeSelectorSlot
;
void transformEntry(Context context, ResourceWrapper resourceWrapper, Object o, int count, boolean prioritized, Object... args) throws Throwable {
T t = (T)o;
entry(context, resourceWrapper, t, count, prioritized, args);
}
DefaultProcessorSlotChain
中first节点重写了entry
方法,最终会调用到fireEntry
方法,该方法会调用下个Slot节点的transformEntry
方法,触发下个节点的entry
方法。
@Override
public void fireEntry(Context context, ResourceWrapper resourceWrapper, Object obj, int count, Object... args)
throws Throwable {
if (next != null) {
next.transformEntry(context, resourceWrapper, obj, count, args);
}
}
与first节点同理,各个Slot的entry
方法有不同的业务处理逻辑,执行完自身的逻辑后会触发下一个节点的entry
方法,从而完成责任链的传递。
总结chain.entry
的执行逻辑如下
next Slot
为null
,结束SlotChain
的调用。在上述Slot中,StatisticSlot
用于资源的调用信息统计,如请求量。由于是责任链模式,当下游Slot执行结束返回时,根据执行结果,统计资源调用信息。大致是如果成功增加响应的成功请求数,抛出BlockException
,增加Block的数量,如果是Exception
,则增加exception的数量。这里不做展开介绍。
DegradeSlot
是针对资源的平均响应时间(RT)或异常比率,来决定资源是否在接下来的时间被自动熔断掉的Slot, 我们接下来将分析该Slot的实现原理。
DegradeSlot
的entry
的方法如下:
@Override
public void entry(Context context, ResourceWrapper resourceWrapper, DefaultNode node, int count, boolean prioritized, Object... args) throws Throwable {
DegradeRuleManager.checkDegrade(resourceWrapper, context, node, count);
fireEntry(context, resourceWrapper, node, count, prioritized, args);
}
DegradeSlot
会直接调用DegradeRuleManager
进行是否降级的检查,并调用下一个Slot的entry
。我们进入到checkDegrade
方法中
public static void checkDegrade(ResourceWrapper resource, Context context, DefaultNode node, int count) throws BlockException {
//根据resource来获取降级策略
Set<DegradeRule> rules = degradeRules.get(resource.getName());
if (rules == null) {
return;
}
//遍历规则集合调用规则的passCheck,如果返回false那么就抛出异常进行降级。
for (DegradeRule rule : rules) {
if (!rule.passCheck(context, node, count)) {
throw new DegradeException(rule.getLimitApp(), rule);
}
}
}
这个方法逻辑比较清晰,首先是根据资源名获取到降级规则,然后遍历规则集合调用规则的passCheck
,如果返回false那么就抛出异常进行降级。我们详细看下passCheck
如何判断是否达到降级标准,大致源码如下
@Override
public boolean passCheck(Context context, DefaultNode node, int acquireCount, Object... args) {
//资源是否已经降级标志,为true表示已经降级了
if (cut.get()) {
return false;
}
//获取资源的全局节点,降级是根据全局节点来进行判断降级策略的
ClusterNode clusterNode = ClusterBuilderSlot.getClusterNode(this.getResource());
if (clusterNode == null) {
return true;
}
//根据响应时间降级策略
if (grade == RuleConstant.DEGRADE_GRADE_RT) {
....
// 根据异常比例降级
} else if (grade == RuleConstant.DEGRADE_GRADE_EXCEPTION_RATIO) {
......
// 根据异常数降级
} else if (grade == RuleConstant.DEGRADE_GRADE_EXCEPTION_COUNT) {
......
}
//达到降级标准,设置降级标志,并根据设置的时间窗口进行定时重置
if (cut.compareAndSet(false, true)) {
ResetTask resetTask = new ResetTask(this);
pool.schedule(resetTask, timeWindow, TimeUnit.SECONDS);
}
return false;
}
Sentinel有三种降级策略,分别为根据平均响应时间降级,根据异常比例降级,根据异常数降级。具体使用哪种策略是配置资源信息时设定。
所以passCheck
判断是否进行降级的逻辑主要为
接下来,我们解析三种不同降级策略的具体实现逻辑
逻辑如下:
private static final int RT_MAX_EXCEED_N = 5;
//获取全局节点一秒内的平均响应时间
double rt = clusterNode.avgRt();
//如果该段时间平均响应时间小于设定的降级时间阈值,则重置统计信息
if (rt < this.count) {
passCount.set(0);
return true;
}
// passCount加1,如果passCount大于5,那么直接降级
if (passCount.incrementAndGet() < RT_MAX_EXCEED_N) {
return true;
}
首先获取资源的平均响应时间,如果平均响应时间小于用户设定的阈值,则重置大于阈值的请求数passCount
,不降级。如果平均响应时间大于阈值,那么passCount
加1,如果passCount
大于5,那么直接降级。
所以根据平均响应时间降级策略,前5个请求即使响应过长也不会立马降级,而是要等到第6个请求到来才会进行降级。每当有一秒内的平均响应时间小于阈值,passCount
就会被重置为0。
RT_MAX_EXCEED_N
在1.7之前不支持设定,在1.7之后支持用户设定。
那么全局节点的平均响应时间是如何获取的,我们继续看下
public class StatisticNode implements Node {
........
@Override
public double avgRt() {
//获取一秒时间窗口内的成功请求数
long successCount = rollingCounterInSecond.success();
if (successCount == 0) {
return 0;
}
return rollingCounterInSecond.rt() * 1.0 / successCount;
}
........
}
这个方法主要是调用rollingCounterInSecond
获取成功次数,然后再获取窗口内的响应时间,用总响应时间除以次数得到平均每次成功调用的响应时间。注意这里是获取一秒时间窗口内的平均rt。
Sentinel是基于滑动时间窗口统计实时指标。滑动窗口可以参考常用的限流算法。
rollingCounterInSecond
是按秒来进行统计的滑动时间窗口。它在StatisticNode
类的初始化如下:
//按秒统计,分成两个窗口,每个窗口500ms。SAMPLE_COUNT为2,IntervalProperty.INTERVAL为1000
private transient volatile Metric rollingCounterInSecond = new ArrayMetric(SampleCountProperty.SAMPLE_COUNT, IntervalProperty.INTERVAL);
rollingCounterInSecond
是一个按秒统计的时间窗口,将1秒的窗口分成2格,每格的时间大小为500ms。它对应的ArrayMetric
类包含LeapArray
类型的对象,new ArrayMetric
最终会初始化一个LeapArray
对象,设置每个小窗口的跨度(这里为500ms),窗口的长度(这里为1s),窗口中小窗口的个数(这里为2个),以及窗口的数组(这里数组长度为2)。
public LeapArray(int sampleCount, int intervalInMs) {
....
//每个小窗口的时间跨度
this.windowLengthInMs = intervalInMs / sampleCount;
//窗口的长度
this.intervalInMs = intervalInMs;
//小窗口个数
this.sampleCount = sampleCount;
//窗口数组 数组大小 = sampleCount
this.array = new AtomicReferenceArray<>(sampleCount);
}
我们再看下ArrayMetric类
的rollingCounterInSecond.success()
的逻辑
@Override
public long success() {
//更新当前的窗口
data.currentWindow();
long success = 0;
//获取窗口数组中每个小窗口的统计值
List<MetricBucket> list = data.values();
//得到整个大窗口的成功请求数
for (MetricBucket window : list) {
success += window.success();
}
return success;
}
data.currentWindow()
方法在这里的作用主要是设置或更新当前时间的窗口。
public WindowWrap<T> currentWindow(long timeMillis) {
if (timeMillis < 0) {
return null;
}
// 计算当前时间戳所属的窗口数组索引下标
int idx = calculateTimeIdx(timeMillis);
// 计算当前窗口的开始时间戳
long windowStart = calculateWindowStart(timeMillis);
/*
* 从窗口数组中获取当前窗口项,分为三种情况
* (1) 当前小窗口为空还未创建,则初始化一个并设置
* (2) 当前窗口的开始时间和上面计算出的窗口开始时间一致,表明当前窗口还未过期,直接返回当前窗口
* (3) 当前窗口的开始时间 小于 上面计算出的窗口开始时间,表明当前窗口已过期,需要替换当前窗口
*/
while (true) {
WindowWrap<T> old = array.get(idx);
//当前小窗口为空还未创建,则初始化一个并设置
if (old == null) {
WindowWrap<T> window = new WindowWrap<T>(windowLengthInMs, windowStart, newEmptyBucket(timeMillis));
if (array.compareAndSet(idx, null, window)) {
return window;
} else {
// Contention failed, the thread will yield its time slice to wait for bucket available.
Thread.yield();
}
//当前窗口的开始时间和上面计算出的窗口开始时间一致,表明当前窗口还未过期,直接返回当前窗口
} else if (windowStart == old.windowStart()) {
return old;
//当前窗口已过期,需要替换当前窗口
} else if (windowStart > old.windowStart()) {
if (updateLock.tryLock()) {
try {
// Successfully get the update lock, now we reset the bucket.
return resetWindowTo(old, windowStart);
} finally {
updateLock.unlock();
}
} else {
Thread.yield();
}
} else if (windowStart < old.windowStart()) {
// Should not go through here, as the provided time is already behind.
return new WindowWrap<T>(windowLengthInMs, windowStart, newEmptyBucket(timeMillis));
}
}
}
//根据当前时间戳计算当前所属的窗口数组索引下标
private int calculateTimeIdx(/*@Valid*/ long timeMillis) {
long timeId = timeMillis / windowLengthInMs;
return (int)(timeId % array.length());
}
//计算当前窗口的开始时间戳
protected long calculateWindowStart(/*@Valid*/ long timeMillis) {
return timeMillis - timeMillis % windowLengthInMs;
}
currentWindow()
是整个滑动窗口逻辑的核心代码,该方法主要分为以下几步
示意图如下
1. 如果array数据里面的bucket数据如下:
NULL B4
|_______|_______|
800 1000 1200
^
time=888
正好当前时间所对应的槽位里面的数据是空的,那么就用CAS更新
2. 如果array里面已经有数据了,并且槽位里面的窗口开始时间和当前的开始时间相等,那么直接返回
B3 B4
|_______|_______|
800 1000 1200
^
time=888
3. 例如当前时间是1676,所对应窗口里面的数据的窗口开始时间小于当前的窗口开始时间,那么加上锁,然后设置槽位的窗口开始时间为当前窗口开始时间,并把槽位里面的数据重置
(old)
B0
|_______||_______|
1200 1400
^
time=1676
通过rollingCounterInSecond.success
获取窗口的调用的成功数后,再调用rollingCounterInSecond.rt() * 1.0 / successCount
获取平均rt。ollingCounterInSecond.rt()
方法如下,这个方法和上面的success方法差不多,获取所有的MetricBucket
的rt数据求和返回。
@Override
public long rt() {
data.currentWindow();
long rt = 0;
//获取当前时间窗口的统计数据
List<MetricBucket> list = data.values();
//统计当前时间窗口的响应时间之和
for (MetricBucket window : list) {
rt += window.rt();
}
return rt;
}
至此,根据请求平均响应时间的的逻辑介绍完了。主要逻辑顺序为
passCount
,不降级。passCount
加1,如果passCount
大于5,那么直接降级。 //获取每秒异常的次数
double exception = clusterNode.exceptionQps();
//获取每秒请求完成的次数,返回的是成功执行完了Slot链且没有被规则拦截的数量
double success = clusterNode.successQps();
//获取每秒总调用次数 总qps=passQps+blockQps
double total = clusterNode.totalQps();
// 如果总调用次数少于5,那么不进行降级
if (total < RT_MAX_EXCEED_N) {
return true;
}
// 在相同的对齐统计时间窗口中,
// 如果调用全部失败,并且失败次数小于5,不降级
double realSuccess = success - exception;
if (realSuccess <= 0 && exception < RT_MAX_EXCEED_N) {
return true;
}
//异常率比设定的阈值小,不降级
if (exception / success < count) {
return true;
}
...
return false;
注意:clusterNode.successQps()
返回的是成功执行Slot链且没有被规则拦截的数量。clusterNode.exceptionQps()
返回的是成功执行Slot链且没有被规则拦截的数量并且业务处理中出现异常的数量,该异常被Tracer.trace(t)
捕获,才会计入统计。
所以realSuccess需要减去重合的部分才是真正成功的数量
上述获取完成调用的Qps和异常调用的Qps,验证后,然后求一下比率,如果没有大于设定的阈值或调用次数小于5次,那么就返回true,否则返回false抛出异常。
获取异常请求数,完成调用请求数与总的调用请求数的逻辑如下
public double exceptionQps() {
return rollingCounterInSecond.exception() / rollingCounterInSecond.getWindowIntervalInSec();
}
public double successQps() {
return rollingCounterInSecond.success() / rollingCounterInSecond.getWindowIntervalInSec();
}
public double totalQps() {
return passQps() + blockQps();
}
public double passQps() {
return rollingCounterInSecond.pass() / rollingCounterInSecond.getWindowIntervalInSec();
}
public double blockQps() {
return rollingCounterInSecond.block() / rollingCounterInSecond.getWindowIntervalInSec();
}
上述这些方法的逻辑与rollingCounterInSecond.success()的基本类似,这里不再继续展开分析。
double exception = clusterNode.totalException();
if (exception < count) {
return true;
}
根据异常数降级是非常的直接的,直接根据统计的异常总次数判断是否超过count。注意这里是rollingCounterInMinute
,也就是异常数只能分钟纬度设定。
public long totalException() {
return rollingCounterInMinute.exception();
}
到这里就讲完了降级的实现。
本文介绍了sentinel熔断机制的使用及其实现原理,在Sentinel的流量统计与加载限流规则的原理上未做详细介绍,后续继续学习。上述描述如有问题,欢迎大家批评指正。