【Sentinel】Sentinel簇点链路的形成

说明

一切节点的跟是 machine-root,同一个资源在不同链路会创建多个DefaultNode,但是在全局只会创建一个 ClusterNode

bash 复制代码
                machine-root

                     /\

                   /    \

       EntranceNode1   EntranceNode2

                /          \

              /              \

DefaultNode(nodeA)         DefaultNode(nodeA)

            |                  |

- - - - - - + - - - - - - - - - +- - - - - - -> ClusterNode(nodeA);

如我们所见,在两个上下文中为"nodeA"创建了两个 DefaultNode,但只创建了一个 ClusterNode

一切的开始

DispatcherServlet是Spring MVC框架中的核心组件,它作为前置控制器,它拦截匹配的请求,并根据相应的规则分发到目标Controller来处理。当请求进入后,首先会执行DispatcherServlet 的 doDispatch 方法

复制代码
public class DispatcherServlet extends FrameworkServlet {
    protected void doDispatch(HttpServletRequest request, HttpServletResponse response) throws Exception {
        ···
        try {
            try {
                ···
                // 执行preHandle方法 
                // 会进入AbstractSentinelInterceptor 的 preHandle
                // 会为当前访问的controller接口创建资源
                if (!mappedHandler.applyPreHandle(processedRequest, response)) {
                    return;
                }
                
                // Actually invoke the handler.
                // 最终会执行SentinelResourceAspect#invokeResourceWithSentinel(pjp);
                // 为所有添加注解的方法创建资源
                mv = ha.handle(processedRequest, response, mappedHandler.getHandler());
                if (asyncManager.isConcurrentHandlingStarted()) {
                    return;
                }
                ···
            }
            catch (Exception ex) {
                ···
            }
        }catch (Exception ex) {
            ···
        }
    }
}

因此,从这里就可以知道,簇点链路中,默认使用 controller 创建的资源一定在使用注解创建的资源之前创建,也就是说,使用注解创建的资源只能作为使用 controller 创建的资源的子节点。

链路创建过程分析

创建 EntranceNode

上面说到,执行会进入AbstractSentinelInterceptor 的 preHandle,进行资源创建

复制代码
public abstract class AbstractSentinelInterceptor implements HandlerInterceptor {
    public static final String SENTINEL_SPRING_WEB_CONTEXT_NAME = "sentinel_spring_web_context";

    @Override
    public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
        try {
            // 获取资源名,也就是 /order/{orderId}
            String resourceName = getResourceName(request);
            if (StringUtil.isEmpty(resourceName)) {
                return true;
            }
            if (increaseReferece(request, this.baseWebMvcConfig.getRequestRefName(), 1) != 1) {
                return true;
            }
            // Parse the request origin using registered origin parser.
            String origin = parseOrigin(request);
            // contextName默认是sentinel_spring_web_context,
            // 如果不想使用这个,而是使用Controller接口路径作为contextName,则需要在application.yml文件中关闭context整合
            // spring.cloud.sentinel.web-context-unify=false
            String contextName = getContextName(request);
            // 创建context
            // Context初始化的过程中,会创建EntranceNode,contextName就是EntranceNode的名称
            ContextUtil.enter(contextName, origin);
            // 创建资源,簇点链路的形成就在里面
            Entry entry = SphU.entry(resourceName, ResourceTypeConstants.COMMON_WEB, EntryType.IN);
            request.setAttribute(baseWebMvcConfig.getRequestAttributeName(), entry);
            return true;
        } catch (BlockException e) {
            ···
        }
    }
}

在获取 contextName 时,会先判断有没有关闭 context 整合,然后选择返回默认的sentinel_spring_web_contex还是从接口中获取url

复制代码
@Override
protected String getContextName(HttpServletRequest request) {
    if (config.isWebContextUnify()) {
        return super.getContextName(request);
    }
    
    return getResourceName(request);
}

@Override
protected String getResourceName(HttpServletRequest request) {
    // Resolve the Spring Web URL pattern from the request attribute.
    Object resourceNameObject = request.getAttribute(HandlerMapping.BEST_MATCHING_PATTERN_ATTRIBUTE);
    if (resourceNameObject == null || !(resourceNameObject instanceof String)) {
        return null;
    }
    String resourceName = (String) resourceNameObject;
    UrlCleaner urlCleaner = config.getUrlCleaner();
    if (urlCleaner != null) {
        resourceName = urlCleaner.clean(resourceName);
    }
    // Add method specification if necessary
    if (StringUtil.isNotEmpty(resourceName) && config.isHttpMethodSpecify()) {
        resourceName = request.getMethod().toUpperCase() + ":" + resourceName;
    }
    return resourceName;
}

然后会进行 context 的创建

复制代码
protected static Context trueEnter(String name, String origin) {
    // 第一次肯定为空
    Context context = contextHolder.get();
    if (context == null) {
        // contextNameNodeMap 有1个值(EntranceNode是DefaultNode的子类,是一种特殊的DefaultNode)
        //      1. sentinel_default_context -> {EntranceNode@10330} 
        Map<String, DefaultNode> localCacheNameMap = contextNameNodeMap;
        // 根据传入的contextName选择看有没有这个name的EntranceNode
        DefaultNode node = localCacheNameMap.get(name);
        // 如果没有就创建一个
        if (node == null) {
            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 {
                            // 创建一个新的EntranceNode
                            node = new EntranceNode(new StringResourceWrapper(name, EntryType.IN), null);
                            // Add entrance node.
                            // Constants.ROOT是一个EntranceNode, id是machine-root
                            // 将当前创建的EntranceNode添加为Constants.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
        context = new Context(node, name);
        context.setOrigin(origin);
        contextHolder.set(context);
    }

    return context;
}

创建 DefaultNode

创建资源时,首先会创建一个 slot 执行链,然后依次执行。

第一个节点是 NodeSelectSlot,在里面完成 DefaultNode 的创建。

当第一次访问时,NodeSelectorSlot 中

复制代码
// volatile保证map多线程的可见性
// 非static变量,每次创建对象时都创建一个新的
private volatile Map<String, DefaultNode> map = new HashMap<String, DefaultNode>(10);

@Override
public void entry(Context context, ResourceWrapper resourceWrapper, Object obj, int count, boolean prioritized, Object... args) throws Throwable {
	// 第一次一定是空,同一个链路中的资源之后的请求不为空
    // 不同链路中的资源,后续的请求中,第一次访问还是空
    DefaultNode node = map.get(context.getName());
    if (node == null) {
        synchronized (this) {
            node = map.get(context.getName());
            if (node == null) {
                // 创建一个DefaultNode,将他放入到map中
                node = new DefaultNode(resourceWrapper, null);
                HashMap<String, DefaultNode> cacheMap = new HashMap<String, DefaultNode>(map.size());
                cacheMap.putAll(map);
                cacheMap.put(context.getName(), node);
                // 更新map
                map = cacheMap;
                // Build invocation tree
                // 将刚创建的node设置为当前node的子节点
                ((DefaultNode) context.getLastNode()).addChild(node);
            }
        }
    }
	// 设置当前节点为刚创建的节点
    context.setCurNode(node);
    fireEntry(context, resourceWrapper, node, count, prioritized, args);
}

下面的图是访问/order/query/{name}接口创建的资源

下面的图是访问/order/query/{name}接口创建的资源,不过在这个 Controller 接口里面又调用了 service 中添加了@SentinelResource注解的方法。根据上面的分析,基于注解的资源后创建,因此它作为基于 Controller 创建的资源的子节点

第二次访问/order/query/{name}接口,在NodeSelectorSlot中,node 会获取到,因此直接执行后边的操作

如果 controller 接口上加了@SentinelResource,还是先创建 controller 资源,然后创建 controller 基于注解的资源,然后是 service 的资源。下面的图中,在/order/query/{name}Controller 接口上添加了@SentinelResource注解。

feign 对 Sentinel 支持

开启 feign 对 Sentinel 的支持后,Sentinel 会将 feign 的请求添加到簇点链路中

复制代码
feign.sentinel.enabled=true

在 Sentinel 的 jar 中,使用 spi 机制加载了一个类com.alibaba.cloud.sentinel.feign.SentinelFeignAutoConfiguration

SentinelFeignAutoConfiguration 配置类里定义了Feign.Builder 的实现类 SentinelFeign.builder()

复制代码
@Configuration(proxyBeanMethods = false)
@ConditionalOnClass({ SphU.class, Feign.class })
public class SentinelFeignAutoConfiguration {

    @Bean
    @Scope("prototype")
    @ConditionalOnMissingBean
    @ConditionalOnProperty(name = "feign.sentinel.enabled") // 配置项为true时该bean生效
    public Feign.Builder feignSentinelBuilder() {
        return SentinelFeign.builder();
    }

}

SentinelFeign.builder( )build( ) 方法

主要作用是: 创建 invocationHandlerFactory,重写create( ) 方法;invocationHandlerFactory 用于创建 SentinelInvocationHandler ,代替前面的 FeignCircuitBreakerInvocationHandler。

复制代码
public Feign build() {
    super.invocationHandlerFactory(new InvocationHandlerFactory() {
        public InvocationHandler create(Target target, Map<Method, MethodHandler> dispatch) {
            GenericApplicationContext gctx = (GenericApplicationContext)Builder.this.applicationContext;
            BeanDefinition def = gctx.getBeanDefinition(target.type().getName());
            FeignClientFactoryBean feignClientFactoryBean = (FeignClientFactoryBean)def.getAttribute("feignClientsRegistrarFactoryBean");
            // 从BeanDefinition 里获取到 fallback、fallbackFactory 
            Class fallback = feignClientFactoryBean.getFallback();
            Class fallbackFactory = feignClientFactoryBean.getFallbackFactory();
            String beanName = feignClientFactoryBean.getContextId();
            if (!StringUtils.hasText(beanName)) {
                beanName = feignClientFactoryBean.getName();
            }

            if (Void.TYPE != fallback) {
                // 创建 fallback 实例
                Object fallbackInstance = this.getFromContext(beanName, "fallback", fallback, target.type());
                // 创建 SentinelInvocationHandler
                return new SentinelInvocationHandler(target, dispatch, new org.springframework.cloud.openfeign.FallbackFactory.Default(fallbackInstance));
            } else if (Void.TYPE != fallbackFactory) {
                FallbackFactory fallbackFactoryInstance = (FallbackFactory)this.getFromContext(beanName, "fallbackFactory", fallbackFactory, FallbackFactory.class);
                return new SentinelInvocationHandler(target, dispatch, fallbackFactoryInstance);
            } else {
                return new SentinelInvocationHandler(target, dispatch);
            }
        }
        private Object getFromContext(String name, String type, Class fallbackType, Class targetType) {
            Object fallbackInstance = Builder.this.feignContext.getInstance(name, fallbackType);
            if (fallbackInstance == null) {
                throw new IllegalStateException(String.format("No %s instance of type %s found for feign client %s", type, fallbackType, name));
            } else if (!targetType.isAssignableFrom(fallbackType)) {
                throw new IllegalStateException(String.format("Incompatible %s instance. Fallback/fallbackFactory of type %s is not assignable to %s for feign client %s", type, fallbackType, targetType, name));
            } else {
                return fallbackInstance;
            }
        }
    });
    super.contract(new SentinelContractHolder(this.contract));
    return super.build();
}

在 invoke 方法里面为feign请求创建资源创建资源

复制代码
@Override
public Object invoke(final Object proxy, final Method method, final Object[] args) throws Throwable {
    if ("equals".equals(method.getName())) {
        try {
            Object otherHandler = args.length > 0 && args[0] != null
            ? Proxy.getInvocationHandler(args[0])
            : null;
            return equals(otherHandler);
        }
        catch (IllegalArgumentException e) {
            return false;
        }
    }
    else if ("hashCode".equals(method.getName())) {
        return hashCode();
    }
    else if ("toString".equals(method.getName())) {
        return toString();
    }

    Object result;
    MethodHandler methodHandler = this.dispatch.get(method);
    // only handle by HardCodedTarget
    if (target instanceof Target.HardCodedTarget) {
        Target.HardCodedTarget hardCodedTarget = (Target.HardCodedTarget) target;
        MethodMetadata methodMetadata = SentinelContractHolder.METADATA_MAP
        .get(hardCodedTarget.type().getName()
             + Feign.configKey(hardCodedTarget.type(), method));
        // resource default is HttpMethod:protocol://url
        if (methodMetadata == null) {
            result = methodHandler.invoke(args);
        }
        else {
            String resourceName = methodMetadata.template().method().toUpperCase()
            + ":" + hardCodedTarget.url() + methodMetadata.template().path();
            Entry entry = null;
            try {
                ContextUtil.enter(resourceName);
                // 为feign请求创建资源
                entry = SphU.entry(resourceName, EntryType.OUT, 1, args);
                // 调用服务端接口
                result = methodHandler.invoke(args);
            }
            catch (Throwable ex) {
                // fallback handle
                if (!BlockException.isBlockException(ex)) {
                    Tracer.trace(ex);
                }
                if (fallbackFactory != null) {
                    try {
                        //异常时 调用熔断逻辑
                        Object fallbackResult = fallbackMethodMap.get(method)
                        .invoke(fallbackFactory.create(ex), args);
                        return fallbackResult;
                    }
                    catch (IllegalAccessException e) {
                        ····
                    }
                }
                else {
					···
                }
            }
            finally {
                ···
            }
        }
    }
    else {
        result = methodHandler.invoke(args);
    }
    return result;
}

如果 service 中使用 feign,则 feign 的调用 也会现实在链路中,他和使用注解创建的service 资源是同级的,但是先创建 feign,后创建 service 注解资源

使用注解和 feign 创建的资源,EntryType 都是 OUT,只有 controller 资源的EntryType 是 IN。

EntryType:枚举标记资源调用方向。

创建ClusterNode

在创建 ClusterNode 时,使用 static 变量存储。将创建的 ClusterNode 与当前 node 进行关联。

复制代码
/**
 * 请记住,相同的资源(ResourceWrapper.equals(Object))将在全局范围内共享相同的ProcessorSlotChain,而与上下文无关。
 * 因此,如果代码进入entry(Context,ResourceWrapper,DefaultNode,int,boolean,Object...),
 * 则资源名称必须相同,但上下文名称可能不同。要获得不同上下文中相同资源的总统计数据,
 * 相同的资源在全局范围内共享相同的ClusterNode。此映射在应用运行时间越长,就会变得越稳定。
 * 因此,我们不使用并发映射,而是使用锁。因为此锁仅在开始时发生,而并发映射将始终保持锁定状态。
 */
private static volatile Map<ResourceWrapper, ClusterNode> clusterNodeMap = new HashMap<>();

@Override
public void entry(Context context, ResourceWrapper resourceWrapper, DefaultNode node, int count,
                  boolean prioritized, Object... args) throws Throwable {
    // 如果不是第一次访问这个资源,则clusterNode是一定有的
    // 所以直接将DefaultNode和ClusterNode进行关联
    // 因为保存ClusterNode的map是static 的,因此全局共享,且创建后内容一直存在
    // 因此一个资源只会创建一次ClusterNode
    if (clusterNode == null) {
        synchronized (lock) {
            if (clusterNode == null) {
                // Create the cluster node.
                clusterNode = new ClusterNode(resourceWrapper.getName(), resourceWrapper.getResourceType());
                HashMap<ResourceWrapper, ClusterNode> newMap = new HashMap<>(Math.max(clusterNodeMap.size(), 16));
                newMap.putAll(clusterNodeMap);
                newMap.put(node.getId(), clusterNode);

                clusterNodeMap = newMap;
            }
        }
    }
    node.setClusterNode(clusterNode);

    /*
     * if context origin is set, we should get or create a new {@link Node} of
     * the specific origin.
     */
    if (!"".equals(context.getOrigin())) {
        Node originNode = node.getClusterNode().getOrCreateOriginNode(context.getOrigin());
        context.getCurEntry().setOriginNode(originNode);
    }

    fireEntry(context, resourceWrapper, node, count, prioritized, args);
}

说明

如果一个请求中要经过多个资源保护的方法(controller 资源*1,注解资源*n),则上面的流程会进行多次,分别根据资源创建类型执行对应的方法,从而将每次的资源添加到前面资源的字节的中,形成于给完整的簇点链路

后面就是限流的一些 slot

相关推荐
蝎子莱莱爱打怪2 天前
XZLL-IM干货系列 04|Netty 长连接实战:Pipeline 怎么排、心跳怎么跳、连接怎么管
后端·微服务·面试
SamDeepThinking3 天前
Java微服务练习方式
java·后端·微服务
米丘6 天前
微前端之 Web Components 完全指南
微服务·html
霸道流氓气质9 天前
领域驱动设计(DDD)在 Spring Boot 微服务中的实践指南
运维·spring boot·微服务
慧一居士9 天前
Feign的GET请求如何传递对象参数?
java·spring cloud
我登哥MVP9 天前
SpringCloud Alibaba 核心组件解析:服务链路追踪
java·spring boot·后端·spring·spring cloud·java-ee·maven
慧一居士9 天前
SpringCloud 微服务Feigin 用的完整调用端和被调用的示例
java·spring cloud
霸道流氓气质9 天前
Spring Boot 微服务性能优化完全指南
spring boot·微服务·性能优化
地瓜伯伯9 天前
从MESI缓存一致性协议讲透synchronized的底层
java·spring boot·spring·spring cloud·微服务·springcloud
Devin~Y9 天前
大厂 Java 面试实录:从音视频内容社区到 AI RAG 的全链路技术设计
java·spring boot·redis·spring cloud·微服务·kafka·音视频