Loading

Sentinel源码之源码入口和构建Context

1.分析源码入口

在微服务使用Sentinel实际工作场景中,我们只需要引入对应依赖:spring-cloud-starter-alibaba-sentinel就会进行自动装配,所以我们直接看META-INF/spring.factories,然后这里从SentinelAutoConfiguration开始看起

image

Sentinel是通过AOP的方式进行切入的,从这里我们看到了Aspect关键字,所以我们就从这里跟进去

@Bean
@ConditionalOnMissingBean
public SentinelResourceAspect sentinelResourceAspect() {
 return new SentinelResourceAspect();
}

跟进去以后我们就会发现这里就是再利用AOP通过@SentinelResource为注解来作为切入点,进行切入

@Aspect //切面
public class SentinelResourceAspect extends AbstractSentinelAspectSupport {

 //指定切入点为SentinelResource注解
 @Pointcut("@annotation(com.alibaba.csp.sentinel.annotation.SentinelResource)")
 public void sentinelResourceAnnotationPointcut() {
 }
...
}

再往下跟我们会发现,还有@Around注解进行环绕通知,其实这就是是利用@SentinelResource注解作为切点,然后在通过AOP环绕通知,来进行增强,在执行原方法前,来执行对应操作,当然这里我们可以看出,一旦出现了限流或者限流就会走BlockException。

// 环绕通知
@Around("sentinelResourceAnnotationPointcut()")
public Object invokeResourceWithSentinel(ProceedingJoinPoint pjp) throws Throwable {
 Method originMethod = resolveMethod(pjp);

 SentinelResource annotation = originMethod.getAnnotation(SentinelResource.class);
 if (annotation == null) {
     // Should not go through here.
     throw new IllegalStateException("Wrong state for SentinelResource annotation");
 }
 String resourceName = getResourceName(annotation.value(), originMethod);
 EntryType entryType = annotation.entryType();
 int resourceType = annotation.resourceType();
 Entry entry = null;
 try {
     // 创建资源操作对象
     entry = SphU.entry(resourceName, resourceType, entryType, pjp.getArgs());
     // 调用原方法
     return pjp.proceed();
 } catch (BlockException ex) {
     return handleBlockException(pjp, annotation, ex);
 } catch (Throwable ex) {
     Class<? extends Throwable>[] exceptionsToIgnore = annotation.exceptionsToIgnore();
     // The ignore list will be checked first.
     if (exceptionsToIgnore.length > 0 && exceptionBelongsTo(ex, exceptionsToIgnore)) {
         throw ex;
     }
     if (exceptionBelongsTo(ex, annotation.exceptionsToTrace())) {
         traceException(ex);
         return handleFallback(pjp, annotation, ex);
     }

     // No fallback function can handle the exception, so throw it out.
     throw ex;
 } finally {
     if (entry != null) {
         entry.exit(1, pjp.getArgs());
     }
 }
}

在以上的这段代码中还有一个问题,我们在之前就说过在创建资源操作对象的时候我们需要先创建Context,但是明显这里没有显示创建,但是实际上我们如果看Context概念的话,就会知道,如果程序中未指定Context,会创建name为"sentinel_default_context"的默认Context,然后我们继续往下跟踪。

public static Entry entry(String name, int resourceType, EntryType trafficType, Object[] args) throws BlockException {
 //限流方法
 return Env.sph.entryWithType(name, resourceType, trafficType, 1, args);
}

进入到entry方法中,这里的entryWithType方法就是我们要看的真正的限流的方法,具体的实现方法在com.alibaba.csp.sentinel.CtSph.entryWithType

@Override
public Entry entryWithType(String name, int resourceType, EntryType entryType, int count, Object[] args) 
throws BlockException {
 return entryWithType(name, resourceType, entryType, count, false, args);
}

@Override
public Entry entryWithType(String name, int resourceType, EntryType entryType, int count, boolean prioritized,
                     Object[] args) throws BlockException {
 // 这里将资源的名称和信息封装称为资源对象
 StringResourceWrapper resource = new StringResourceWrapper(name, entryType, resourceType);
 // 返回一个Entry资源操作对象
 // prioritized属性表示优先级,默认值为false,表示当前请求不按照优先级执行,直接执行
 return entryWithPriority(resource, count, prioritized, args);
}

我们接下来来具体分析核心方法entryWithPriority

private Entry entryWithPriority(ResourceWrapper resourceWrapper, int count, boolean prioritized, Object... args)
throws BlockException {
 // 从当前线程中获取Context
 // 一个请求会占用一个线程,并且绑定一个Context
 Context context = ContextUtil.getContext();
 // 一个请求对应一个Context
 // 如果当前类型为NullContext,表示此时请求已经超出了阈值,无需检测规则
 if (context instanceof NullContext) {
     // The {@link NullContext} indicates that the amount of context has exceeded the threshold,
     // so here init the entry only. No rule checking will be done.
     return new CtEntry(resourceWrapper, null, context);
 }

	 // 此时如果获取Context为空,就创建默认的sentinel_default_context,并且会放入到当前线程中
 if (context == null) {
     // Using default context.
     context = InternalContextUtil.internalEnter(Constants.CONTEXT_DEFAULT_NAME);
 }

// 判断全局开关,如果是关闭状态,直接返回无需检测规则
 // Global switch is close, no rule checking will do.
 if (!Constants.ON) {
     return new CtEntry(resourceWrapper, null, context);
 }
 /**
        * 这里是整个架构的核心所在,这里是在构建一个处理链,这个处理链是一个单向链表结构,类似于Filter一样,构建这个链条的
     * 原因是对业务进行解耦,像限流资源保护有很多,比如限流、降级、热点参数、系统降级等等,如果都写在一起就耦合很严重,我们知道oop的
     * 思想就是让每个类确定各自的职责,不要让他做不相干的事情,所以这里将业务进行全面解耦,然后在解耦的同时又通过链式编程将它们串起来
     */
    ProcessorSlot<Object> chain = lookProcessChain(resourceWrapper);

    /*
         * Means amount of resources (slot chain) exceeds {@link Constants.MAX_SLOT_CHAIN_SIZE},
         * so no rule checking will be done.
         */
    if (chain == null) {
        return new CtEntry(resourceWrapper, null, context);
    }

    Entry e = new CtEntry(resourceWrapper, chain, context);
    try {
        // 针对资源操作
        chain.entry(context, resourceWrapper, null, count, prioritized, args);
    } catch (BlockException e1) {
        e.exit(count, args);
        throw e1;
    } catch (Throwable e1) {
        // This should not happen, unless there are errors existing in Sentinel internal.
        RecordLog.info("Sentinel unexpected exception", e1);
    }
    return e;
}

2.构建Context

分析这个类型中的InternalContextUtil.internalEnter(Constants.CONTEXT_DEFAULT_NAME);

/**
* This class is used for skip context name checking.
此类型是用于跳过Context名称的检测
*/
private final static class InternalContextUtil extends ContextUtil {
    static Context internalEnter(String name) {
        // 从这里继续跟踪
        return trueEnter(name, "");
    }

    static Context internalEnter(String name, String origin) {
        return trueEnter(name, origin);
    }
}

首先这里要明确一下,一个Context的组成实际上需要name(名称)和origin(来源),所以方法上传入这两个参数

protected static Context trueEnter(String name, String origin) {
    // 从当前线程中获取当前context名称
    Context context = contextHolder.get();
    // 如果当前context为空
    if (context == null) {
        // 从缓存中获取,当前缓存中key值为:Context名称,value值为:EntranceNode
        // (因为后续创建的是EntranceNode),需要它的原因是因为构建Context需要EntranceNode
        Map<String, DefaultNode> localCacheNameMap = contextNameNodeMap;
        // 在缓存中获取EntranceNode
        DefaultNode node = localCacheNameMap.get(name);
        // 如果node为空
        if (node == null) {
            // 当前缓存的size>Context的最大数量,返回NULL_Context类型
            if (localCacheNameMap.size() > Constants.MAX_CONTEXT_NAME_SIZE) {
                setNullContext();
                return NULL_CONTEXT;
            } else {
                // 加锁
                LOCK.lock();
                try {
                    node = contextNameNodeMap.get(name);
                    // 这里两次判断是采用了双重检测锁的机制:为了防止并发创建
                    if (node == null) {
                        if (contextNameNodeMap.size() > Constants.MAX_CONTEXT_NAME_SIZE) {
                            setNullContext();
                            return NULL_CONTEXT;
                        } else {
                            // node赋值为EntranceNode
                            node = new EntranceNode(new StringResourceWrapper(name, EntryType.IN), null);
                            // Add entrance node.
                            // 将新建的EntranceNode添加到ROOT中
                            Constants.ROOT.addChild(node);
                            // 将新建的EntranceNode添加到缓存中
                            Map<String, DefaultNode> newMap = new HashMap<>(contextNameNodeMap.size() + 1);
                            newMap.putAll(contextNameNodeMap);
                            newMap.put(name, node);
                            contextNameNodeMap = newMap;
                        }
                    }
                } finally {
                    LOCK.unlock();
                }
            }
        }
        // 将name和node封装成Context
        context = new Context(node, name);
        // 设定来源
        context.setOrigin(origin);
        // 将context写入到当前线程中
        contextHolder.set(context);
    }
	// 返回Context
    return context;
}
posted @ 2022-05-21 10:20  ZT丶  阅读(118)  评论(0编辑  收藏  举报