• 【Sentinel】核心API-Entry与Context


    一、Entry

    1、Entry的声明

    默认情况下,Sentinel会将controller中的方法作为被保护资源,那么问题来了,我们该如何将自己的一段代码标记为一个Sentinel的资源呢?

    Sentinel中的资源用Entry来表示。

    声明Entry的API示例:(try后面直接加括号写上对应的资源,就不用自己再加finally语句去关闭了,即try-with-resource

    // 资源名可使用任意有业务语义的字符串,比如方法名、接口名或其它可唯一标识的字符串。
    try (Entry entry = SphU.entry("resourceName")) {
      // 被保护的业务逻辑
      // 你想做的操作,比如异常次数+1、调用次数+1
      // do something here...
    } catch (BlockException ex) {
      // 业务代码抛的异常+限流的异常
      // 资源访问阻止,被限流或被降级
      // 在此处进行相应的处理操作
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10

    try-with-resource的写法需要注意:

    特别地,若 entry 的时候传入了热点参数,那么 exit 的时候也一定要带上对应的参数(exit(count, args)),否则可能会有统计错误。这个时候不能使用 try-with-resources 的方式。另外通过 Tracer.trace(ex) 来统计异常信息时,由于 try-with-resources 语法中 catch 调用顺序的问题,会导致无法正确统计异常数,因此统计异常信息时也不能在 try-with-resources 的 catch 块中调用 Tracer.trace(ex)。

    不用try-with-resource,手动exit的写法:

    Entry entry = null;
    // 务必保证 finally 会被执行
    try {
      // 资源名可使用任意有业务语义的字符串,注意数目不能太多(超过 1K),超出几千请作为参数传入而不要直接作为资源名
      // EntryType 代表流量类型(inbound/outbound),其中系统规则只对 IN 类型的埋点生效
      entry = SphU.entry("自定义资源名");
      // 被保护的业务逻辑
      // do something...
    } catch (BlockException ex) {
      // 资源访问阻止,被限流或被降级
      // 进行相应的处理操作
    } catch (Exception ex) {
      // 若需要配置降级规则,需要通过这种方式记录业务异常
      Tracer.traceEntry(ex, entry);
    } finally {
      // 务必保证 exit,务必保证每个 entry 与 exit 配对
      if (entry != null) {
        entry.exit();
      }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20

    关于SphU.entry()方法的参数:

    在这里插入图片描述
    注意:SphU.entry(xxx) 需要与 entry.exit() 方法成对出现,匹配调用,否则会导致调用链记录异常,抛出 ErrorEntryFreeException 异常。常见的错误:

    1、自定义埋点只调用 SphU.entry(),没有调用 entry.exit()
    2、顺序错误,比如:entry1 -> entry2 -> exit1 -> exit2,应该为 entry1 -> entry2 -> exit2 -> exit1
    
    • 1
    • 2

    2、使用API自定义资源

    在demo工程的order-service服务中,将OrderServicequeryOrderById()方法标记为一个资源:queryOrderById()方法未修改前:

    在这里插入图片描述

    • 首先处理下依赖与配置
    
    <dependency>
        <groupId>com.alibaba.cloudgroupId>
        <artifactId>spring-cloud-starter-alibaba-sentinelartifactId>
    dependency>
    
    • 1
    • 2
    • 3
    • 4
    • 5
    spring:
      cloud:
        sentinel:
          transport:
            dashboard: localhost:8090 # 这里我的sentinel用了8089的端口
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 修改OrderService类的queryOrderById方法,创建Entry资源,将要保护的一段代码放入try语句来做为一个资源:
    public Order queryOrderById(Long orderId) {
        // 创建Entry,标记资源,资源名为resource1
        try (Entry entry = SphU.entry("resource1")) {
            // 1.查询订单
            Order order = orderMapper.findById(orderId);
            // 2.查询用户,基于Feign的远程调用
            User user = userClient.findById(order.getUserId());
            // 3.设置
            order.setUser(user);
            // 4.返回
            return order;
        }catch (BlockException e){
            log.error("被限流或降级", e);
            return null;
        }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16

    重启后可以看到自定义资源成功:

    在这里插入图片描述
    很明显,上面的逻辑在自定义资源时是重复的动作,即创建Entry,再拿个try-catch把要定义的代码包起来。 ⇒ AOP环绕实现,在前面之前try,在之后catchAOP再兑换成一个注解,这个注解官方已实现,就是@SentinelResource

    3、基于@SentinelResource注解标记资源

    同样标记Service里的queryOrderById方法:

    在这里插入图片描述
    从自动装配开始找到源码看下实现:

    在这里插入图片描述

    匹配@SentinelResource注解做AOP环绕增强的源码:

    
    /**
     * Aspect for methods with {@link SentinelResource} annotation.
     *
     * @author Eric Zhao
     */
    @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注解
            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
                entry = SphU.entry(resourceName, resourceType, entryType, pjp.getArgs());
                // 执行受保护的方法
                Object result = pjp.proceed();
                return result;
            } 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());
                }
            }
        }
    }
    
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    • 40
    • 41
    • 42
    • 43
    • 44
    • 45
    • 46
    • 47
    • 48
    • 49
    • 50
    • 51
    • 52
    • 53
    • 54
    • 55
    • 56
    • 57
    • 58

    简单来说,@SentinelResource注解就是一个标记,而Sentinel基于AOP思想,对被标记的方法做环绕增强,完成资源(Entry)的创建。

    二、Context

    在这里插入图片描述
    如图,除了簇点链路中的controller方法,以及我自定义的资源,还有一个默认的入口节点:sentinel_spring_web_context,它是一个EntranceNode类型的节点,这个节点是在初始化Context的时候由Sentinel帮我们创建的。

    1、Context介绍

    • Context 代表调用链路上下文,贯穿一次调用链路中的所有资源( Entry),基于ThreadLocal。
    • Context 维护着入口节点(entranceNode)、本次调用链路的 curNode(当前资源节点)、调用来源(origin)等信息。
    • 后续的Slot都可以通过Context拿到DefaultNode或者ClusterNode,从而获取统计数据,完成规则判断
    • Context初始化的过程中,会创建EntranceNode,contextName就是EntranceNode的名称

    创建Context的API为:

    // 创建context,包含两个参数:context名称、 来源名称
    ContextUtil.enter("contextName", "originName");
    
    • 1
    • 2

    2、Context的初始化

    查看Sentinel依赖包下的spring.factories:
    在这里插入图片描述

    spring.factories声明需要就是自动装配的配置类:

    在这里插入图片描述
    先看SentinelWebAutoConfiguration这个类:

    在这里插入图片描述

    这个类实现了WebMvcConfigurer,我们知道这个是SpringMVC自定义配置用到的类,可以配置HandlerInterceptor:

    在这里插入图片描述
    可以看到这里泛型中配置了一个SentinelWebInterceptor的拦截器。SentinelWebInterceptor的声明如下:

    在这里插入图片描述
    发现它继承了AbstractSentinelInterceptor这个类,而AbstractSentinelInterceptor最终实现了HandlerInterceptor

    在这里插入图片描述
    HandlerInterceptor拦截器会拦截一切进入controller的方法,执行preHandle前置拦截方法,而Context的初始化就是在这里完成的。

    3、AbstractSentinelInterceptor

    HandlerInterceptor拦截器会拦截一切进入controller的方法,执行preHandle前置拦截方法,而Context的初始化就是在这里完成的。来看看这个类的preHandle`实现:

    @Override
    public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler)
        throws Exception {
        try {
            // 获取资源名称,一般是controller方法的@RequestMapping路径,例如/order/{orderId}
            String resourceName = getResourceName(request);
            if (StringUtil.isEmpty(resourceName)) {
                return true;
            }
            // 从request中获取请求来源,将来做 授权规则 判断时会用
            String origin = parseOrigin(request);
            
            // 获取 contextName,默认是sentinel_spring_web_context
            String contextName = getContextName(request);
            // 创建 Context
            ContextUtil.enter(contextName, origin);
            // 创建资源,名称就是当前请求的controller方法的映射路径
            Entry entry = SphU.entry(resourceName, ResourceTypeConstants.COMMON_WEB, EntryType.IN);
            request.setAttribute(baseWebMvcConfig.getRequestAttributeName(), entry);
            return true;
        } catch (BlockException e) {
            try {
                handleBlockException(request, response, e);
            } finally {
                ContextUtil.exit();
            }
            return false;
        }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29

    4、ContextUtil

    创建Context的方法就是 ContextUtil.enter(contextName, origin); 我们进入entry方法:

    public static Context enter(String name, String origin) {
        if (Constants.CONTEXT_DEFAULT_NAME.equals(name)) {
            throw new ContextNameDefineException(
                "The " + Constants.CONTEXT_DEFAULT_NAME + " can't be permit to defined!");
        }
        return trueEnter(name, origin);
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7

    进入trueEnter方法:

    protected static Context trueEnter(String name, String origin) {
        // 尝试获取context
        Context context = contextHolder.get();
        // 判空
        if (context == null) {
            // 如果为空,开始初始化
            Map<String, DefaultNode> localCacheNameMap = contextNameNodeMap;
            // 尝试获取入口节点
            DefaultNode node = localCacheNameMap.get(name);
            if (node == null) {
                LOCK.lock();
                try {
                    node = contextNameNodeMap.get(name);
                    if (node == null) {
                        // 入口节点为空,初始化入口节点 EntranceNode
                        node = new EntranceNode(new StringResourceWrapper(name, EntryType.IN), null);
                        // 添加入口节点到 ROOT
                        Constants.ROOT.addChild(node);
                        // 将入口节点放入缓存
                        Map<String, DefaultNode> newMap = new HashMap<>(contextNameNodeMap.size() + 1);
                        newMap.putAll(contextNameNodeMap);
                        newMap.put(name, node);
                        contextNameNodeMap = newMap;
                    }
                } finally {
                    LOCK.unlock();
                }
            }
            // 创建Context,参数为:入口节点 和 contextName
            context = new Context(node, name);
            // 设置请求来源 origin
            context.setOrigin(origin);
            // 放入ThreadLocal
            contextHolder.set(context);
        }
        // 返回
        return context;
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38

    画图表示Entry和Context两个核心API完成资源的创建,不管是controller中的资源还是自定义的资源,接下来就是执行ProcessorSlotChain插槽链,关于ProcessorSlotChain的执行,见下篇。

    在这里插入图片描述

  • 相关阅读:
    OSPF协议LSDB同步过程和邻居状态机
    GET 和 POST请求的区别是什么
    【Java集合类面试二十六】、介绍一下ArrayList的数据结构?
    爬虫项目(四):批量下载高清美女桌面壁纸
    基于量子粒子群算法(QPSO)优化LSTM的风电、负荷等时间序列预测算法(Matlab代码实现)
    2022合肥站 G-Game Plan
    北约网络安全防御演习:Locked Shields
    RibbonGroup
    LeetCode 热题 100(九):回溯复习。77. 组合、17. 电话号码的字母组合、39. 组合总和
    JavaScript高阶班之ES6 → ES11(八)
  • 原文地址:https://blog.csdn.net/llg___/article/details/132664395