Análisis del código fuente de Sentinel Notas de seguimiento de Dark Horse

Análisis del código fuente de Sentinel

1. El concepto básico de Sentinel

Sentinel implementa funciones como limitación de corriente, aislamiento, degradación y fusión Esencialmente, hay dos cosas que hacer:

  • Datos estadísticos: Datos estadísticos de acceso de un recurso (QPS, RT, etc.)
  • Juicio de regla: juzgar si se cumplen las reglas de limitación, las reglas de aislamiento, las reglas de degradación y las reglas de fusión actuales

El recurso aquí es el negocio que quiere ser protegido por Sentinel Por ejemplo, el método de controlador definido en el proyecto es el recurso protegido por Sentinel por defecto.

1.1.Cadena de ranuras del procesador

El esqueleto central para lograr las funciones anteriores es una clase llamada ProcessorSlotChain. Esta clase está diseñada según el modelo de cadena de responsabilidad, que encapsula diferentes funciones (limitación de corriente, degradación, protección del sistema) en ranuras una por una y las ejecuta una por una después de que ingresa la solicitud.

Su flujo de trabajo se muestra en la figura:
inserte la descripción de la imagen aquí

Los espacios en la cadena de responsabilidad también se dividen en dos categorías:

  • Parte de construcción de datos estadísticos (estadística)
    • NodeSelectorSlot: responsable de construir los nodos (DefaultNode) en el enlace del punto de clúster y formar estos nodos en un árbol de enlace
    • ClusterBuilderSlot: ClusterNode responsable de construir un determinado recurso. ClusterNode puede guardar información de operación de recursos (tiempo de respuesta, QPS, número de bloque, número de hilo, número de excepción, etc.) e información de fuente (nombre de origen)
    • StatisticSlot: responsable de las estadísticas de los datos de llamadas en tiempo real, incluida la información de ejecución, la información de origen, etc.
  • Parte de comprobación de reglas (comprobación de reglas)
    • AuthoritySlot: responsable de las reglas de autorización (control de fuente)
    • SystemSlot: responsable de las reglas de protección del sistema
    • ParamFlowSlot: responsable de las reglas de limitación de corriente de los parámetros de punto de acceso
    • FlowSlot: responsable de las reglas de limitación de flujo
    • DegradeSlot: responsable de degradar las reglas

1.2.Nodo

El enlace del punto de clúster en Sentinel se compone de Nodos uno por uno. El Nodo es una interfaz, que incluye las siguientes implementaciones:
inserte la descripción de la imagen aquí

Todos los nodos pueden registrar estadísticas de acceso a los recursos, por lo que todos son subclases de StatisticNode.

Según la función, se divide en dos tipos de Nodo:

  • DefaultNode: Representa cada recurso en el árbol de enlaces, cuando un recurso aparece en diferentes enlaces, se crearán diferentes nodos DefaultNode. El nodo de entrada del árbol se llama EntryNode, que es un DefaultNode especial
  • ClusterNode: Representa recursos, no importa en cuantos enlaces aparezca un recurso, solo habrá un ClusterNode. Lo que se registra es la suma de todos los datos estadísticos a los que se accede al recurso actual.

DefaultNode registra los datos de acceso a los recursos en el enlace actual, que se utiliza para implementar las reglas de limitación actuales basadas en el modo de enlace. ClusterNode registra los datos de acceso de los recursos en todos los enlaces y se da cuenta de las reglas de limitación actuales del modo predeterminado y el modo de asociación.

Por ejemplo: Tenemos dos negocios en un proyecto SpringMVC:

  • Negocio 1: Los recursos en el controlador /order/queryacceden a los recursos en el servicio/goods
  • Negocio 2: Los recursos en el controlador /order/saveacceden a los recursos en el servicio/goods

El gráfico de enlace creado es el siguiente:
inserte la descripción de la imagen aquí

1.3.Entrada

De forma predeterminada, Sentinel utilizará el método en el controlador como un recurso protegido, por lo que la pregunta es, ¿cómo marcamos un fragmento de código como recurso de Sentinel?

Los recursos en Sentinel están representados por Entry. Ejemplo de API para declarar Entrada:

// 资源名可使用任意有业务语义的字符串,比如方法名、接口名或其它可唯一标识的字符串。
try (Entry entry = SphU.entry("resourceName")) {
    
    
  // 被保护的业务逻辑
  // do something here...
} catch (BlockException ex) {
    
    
  // 资源访问阻止,被限流或被降级
  // 在此处进行相应的处理操作
}

1.3.1 Recursos personalizados

OrderServicePor ejemplo, marcamos el método como un recurso en el servicio de servicio de pedidos queryOrderById().

1) Primero introduzca la dependencia centinela en el servicio de pedidos

<!--sentinel-->
<dependency>
    <groupId>com.alibaba.cloud</groupId>
    <artifactId>spring-cloud-starter-alibaba-sentinel</artifactId>
</dependency>

2) Luego configure la dirección Sentinel

spring:
  cloud:
    sentinel:
      transport:
        dashboard: localhost:8089 # 这里我的sentinel用了8089的端口

3) Modificar el método queryOrderById de la clase OrderService

El código se implementa así:

public Order queryOrderById(Long orderId) {
    
    
    // 创建Entry,标记资源,资源名为resource1
    try (Entry entry = SphU.entry("resource1")) {
    
    
        // 1.查询订单,这里是假数据
        Order order = Order.build(101L, 4999L, "小米 MIX4", 1, 1L, null);
        // 2.查询用户,基于Feign的远程调用
        User user = userClient.findById(order.getUserId());
        // 3.设置
        order.setUser(user);
        // 4.返回
        return order;
    }catch (BlockException e){
    
    
        log.error("被限流或降级", e);
        return null;
    }
}

4) Acceso

Abra el navegador, visite el servicio de pedidos: http://localhost:8080/order/101
y luego abra la consola Sentinel para ver el enlace del clúster:
inserte la descripción de la imagen aquí

1.3.2 Marcado de recursos basado en anotaciones

Al aprender Sentinel anteriormente, sabemos que podemos marcar recursos agregando la anotación @SentinelResource al método.
inserte la descripción de la imagen aquí
¿Cómo se logra esto?
Echemos un vistazo al paquete de dependencia de Sentinel que presentamos:
inserte la descripción de la imagen aquí
la instrucción spring.factories debe ser una clase de configuración ensamblada automáticamente, el contenido es el siguiente:
inserte la descripción de la imagen aquí
Echemos un vistazo a SentinelAutoConfigurationesta clase:
inserte la descripción de la imagen aquí
puede ver que un Bean es declarado aquí SentinelResourceAspect:


/**
 * 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());
            }
        }
    }
}

En pocas palabras, la anotación @SentinelResource es un marcador y Sentinel, basado en la idea de AOP, realiza mejoras envolventes en los métodos marcados para completar la Entrycreación de recursos ( ).

1.4.Contexto

En la sección anterior, encontramos que además de los dos recursos del método de controlador y el método de servicio en el enlace del clúster, también hay un nodo de entrada predeterminado:

sentinel_spring_web_context es un nodo de tipo EntryNode

Sentinel crea este nodo para nosotros al inicializar Context.

1.4.1 ¿Qué es el contexto?

Entonces, ¿qué es Contexto?

  • El contexto representa el contexto del enlace de llamada, se ejecuta a través de todos los recursos en un enlace de llamada ( Entry) y se basa en ThreadLocal.
  • El contexto mantiene información como el nodo de entrada ( entranceNode), el curNode (nodo de recurso actual) de este enlace de llamada y el origen de la llamada ( ).origin
  • Las ranuras subsiguientes pueden obtener DefaultNode o ClusterNode a través de Context, para obtener datos estadísticos y completar el juicio de reglas
  • Durante el proceso de inicialización de Context, se creará EntryNode y contextName es el nombre de EntryNode

Las API correspondientes son las siguientes:

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

1.4.2 Inicialización del contexto

Entonces, ¿cuándo se inicializa este Contexto?

1.4.2.1 Autocableado

Echemos un vistazo a los paquetes de dependencia de Sentinel que presentamos:
inserte la descripción de la imagen aquí

La instrucción spring.factories debe ser la clase de configuración para el ensamblaje automático, de la siguiente manera:
inserte la descripción de la imagen aquí

Primero veamos la clase SentinelWebAutoConfiguration:
inserte la descripción de la imagen aquí
esta clase implementa WebMvcConfigurer, sabemos que esta es la clase utilizada por la configuración personalizada de SpringMVC, y se puede configurar HandlerInterceptor:
inserte la descripción de la imagen aquí

SentinelWebInterceptorPuede ver que un interceptor está configurado aquí .

SentinelWebInterceptorEl comunicado es el siguiente:
inserte la descripción de la imagen aquí

Se encontró que heredó AbstractSentinelInterceptoresta clase.
inserte la descripción de la imagen aquí

HandlerInterceptorEl interceptor interceptará todos los métodos que ingresan al controlador y ejecutará preHandleel método de intercepción previa, y aquí se completa la inicialización del Contexto.

1.4.2.2.AbstractSentinelInterceptor

HandlerInterceptorEl interceptor interceptará todos los métodos que ingresan al controlador y ejecutará preHandleel método de intercepción previa, y aquí se completa la inicialización del Contexto.

Echemos un vistazo a la implementación de esta clase 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.4.2.3.ContextUtil

La forma de crear Contexto es ContextUtil.enter(contextName, origin);

Ingresamos al método:

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);
}

Cómo entrar 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;
}

2. Proceso de ejecución de ProcessorSlotChain

A continuación, rastreamos el código fuente y verificamos el proceso de ejecución de ProcessorSlotChain.

2.1 Entrada

Primero, de vuelta al punto de entrada de todo, el método AbstractSentinelInterceptorde la clase preHandle:

[Error en la transferencia de la imagen del enlace externo, el sitio de origen puede tener un mecanismo de enlace antirrobo, se recomienda guardar la imagen y cargarla directamente (img-03ynZotv-1680236644238)(assets/image-20210925142313050.png)]

Además, SentinelResourceAspectel método de mejora de sonido envolvente:
inserte la descripción de la imagen aquí

Como puedes ver, cualquier recurso debe ejecutar SphU.entry()este método:

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

Continuar para ingresar Env.sph.entryWithType(name, resourceType, trafficType, 1, args);:

@Override
public Entry entryWithType(String name, int resourceType, EntryType entryType, int count, boolean prioritized,
                           Object[] args) throws BlockException {
    
    
    // 将 资源名称等基本信息 封装为一个 StringResourceWrapper对象
    StringResourceWrapper resource = new StringResourceWrapper(name, entryType, resourceType);
    // 继续
    return entryWithPriority(resource, count, prioritized, args);
}

Cómo entrar entryWithPriority:

private Entry entryWithPriority(ResourceWrapper resourceWrapper, int count, boolean prioritized, Object... args)
    throws BlockException {
    
    
    // 获取 Context
    Context context = ContextUtil.getContext();

    if (context == null) {
    
    
        // Using default context.
        context = InternalContextUtil.internalEnter(Constants.CONTEXT_DEFAULT_NAME);
    }// 获取 Slot执行链,同一个资源,会创建一个执行链,放入缓存
    ProcessorSlot<Object> chain = lookProcessChain(resourceWrapper);

	// 创建 Entry,并将 resource、chain、context 记录在 Entry中
    Entry e = new CtEntry(resourceWrapper, chain, context);
    try {
    
    
        // 执行 slotChain
        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;
}

En este código, ProcessorSlotChainse obtendrá el objeto y luego se ejecutará cada ranura en slotChain en función de chain.entry (). Y aquí está su clase de implementación: DefaultProcessorSlotChain.

Después de obtener ProcessorSlotChain, se guardará en un mapa, la clave es ResourceWrapper y el valor es ProcessorSlotChain.

Por lo tanto, un recurso solo tendrá un ProcessorSlotChain .

2.2.Cadena de ranuras del procesador por defecto

Ingresamos el método de entrada de DefaultProcessorSlotChain:

@Override
public void entry(Context context, ResourceWrapper resourceWrapper, Object t, int count, boolean prioritized, Object... args)
    throws Throwable {
    
    
    // first,就是责任链中的第一个 slot
    first.transformEntry(context, resourceWrapper, t, count, prioritized, args);
}

Aquí primero, el tipo es AbstractLinkedProcessorSlot:
inserte la descripción de la imagen aquí

Mira la relación de herencia:
inserte la descripción de la imagen aquí

Por lo tanto, primero debe ser una de estas clases de implementación. De acuerdo con la secuencia de la cadena de responsabilidad mencionada anteriormente, primero debe ser NodeSelectorSlot.

Sin embargo, dado que se basa en el modelo de cadena de responsabilidad, solo necesita recordar el siguiente espacio aquí, que es el siguiente:
inserte la descripción de la imagen aquí

next es de hecho del tipo NodeSelectSlot.

Y el siguiente de NodeSelectSlot debe ser ClusterBuilderSlot, y así sucesivamente:
inserte la descripción de la imagen aquí
se establece la cadena de responsabilidad.

2.3.Ranura del selector de nodo

NodeSelectorSlot es responsable de construir los nodos (DefaultNode) en el enlace del punto de clúster y formar estos nodos en un árbol de enlaces.

Código central:

@Override
public void entry(Context context, ResourceWrapper resourceWrapper, Object obj, int count, boolean prioritized, Object... args)
    throws Throwable {
    
    
  	// 尝试获取 当前资源的 DefaultNode
    DefaultNode node = map.get(context.getName());
    if (node == null) {
    
    
        synchronized (this) {
    
    
            node = map.get(context.getName());
            if (node == null) {
    
    
                // 如果为空,为当前资源创建一个新的 DefaultNode
                node = new DefaultNode(resourceWrapper, null);
                HashMap<String, DefaultNode> cacheMap = new HashMap<String, DefaultNode>(map.size());
                cacheMap.putAll(map);
                // 放入缓存中,注意这里的 key是contextName,
                // 这样不同链路进入相同资源,就会创建多个 DefaultNode
                cacheMap.put(context.getName(), node);
                map = cacheMap;
                // 当前节点加入上一节点的 child中,这样就构成了调用链路树
                ((DefaultNode) context.getLastNode()).addChild(node);
            }

        }
    }
	// context中的curNode(当前节点)设置为新的 node
    context.setCurNode(node);
    // 执行下一个 slot
    fireEntry(context, resourceWrapper, node, count, prioritized, args);
}

Esta tragamonedas logra varias cosas:

  • Crear un DefaultNode para el recurso actual
  • Coloque el DefaultNode en el caché, y la clave es contextName, de modo que las solicitudes de diferentes entradas de enlace crearán múltiples DefaultNodes, y solo un DefaultNode para el mismo enlace.
  • Establezca el DefaultNode del recurso actual en el childNode del recurso anterior
  • Establezca el DefaultNode del recurso actual en curNode (el nodo actual) en el Contexto

La siguiente ranura es ClusterBuilderSlot

2.4.Ranura de ClusterBuilder

ClusterBuilderSlot es responsable de construir un ClusterNode de un recurso, el código central:

@Override
public void entry(Context context, ResourceWrapper resourceWrapper, DefaultNode node,
                  int count, boolean prioritized, Object... args)
    throws Throwable {
    
    
    // 判空,注意ClusterNode是共享的成员变量,也就是说一个资源只有一个ClusterNode,与链路无关
    if (clusterNode == null) {
    
    
        synchronized (lock) {
    
    
            if (clusterNode == null) {
    
    
                // 创建 cluster node.
                clusterNode = new ClusterNode(resourceWrapper.getName(), resourceWrapper.getResourceType());
                HashMap<ResourceWrapper, ClusterNode> newMap = new HashMap<>(Math.max(clusterNodeMap.size(), 16));
                newMap.putAll(clusterNodeMap);
                // 放入缓存,可以是nodeId,也就是resource名称
                newMap.put(node.getId(), clusterNode);
                clusterNodeMap = newMap;
            }
        }
    }
    // 将资源的 DefaultNode与 ClusterNode关联
    node.setClusterNode(clusterNode);
	// 记录请求来源 origin 将 origin放入 entry
    if (!"".equals(context.getOrigin())) {
    
    
        Node originNode = node.getClusterNode().getOrCreateOriginNode(context.getOrigin());
        context.getCurEntry().setOriginNode(originNode);
    }
	// 继续下一个slot
    fireEntry(context, resourceWrapper, node, count, prioritized, args);
}

2.5.Ranura estadística

StatisticSlot es responsable de contar los datos de llamadas en tiempo real, incluida la información de ejecución (número de visitas, número de hilos), información de origen, etc.

StatisticSlot es la clave para la limitación actual, en la que se mantiene un contador basado en el algoritmo de ventana de tiempo deslizante para contar la cantidad de solicitudes que ingresan a un determinado recurso.

Código central:

@Override
public void entry(Context context, ResourceWrapper resourceWrapper, DefaultNode node, 
                  int count, boolean prioritized, Object... args) throws Throwable {
    
    
    try {
    
    
        // 放行到下一个 slot,做限流、降级等判断
        fireEntry(context, resourceWrapper, node, count, prioritized, args);

        // 请求通过了, 线程计数器 +1 ,用作线程隔离
        node.increaseThreadNum();
        // 请求计数器 +1 用作限流
        node.addPassRequest(count);

        if (context.getCurEntry().getOriginNode() != null) {
    
    
            // 如果有 origin,来源计数器也都要 +1
            context.getCurEntry().getOriginNode().increaseThreadNum();
            context.getCurEntry().getOriginNode().addPassRequest(count);
        }

        if (resourceWrapper.getEntryType() == EntryType.IN) {
    
    
            // 如果是入口资源,还要给全局计数器 +1.
            Constants.ENTRY_NODE.increaseThreadNum();
            Constants.ENTRY_NODE.addPassRequest(count);
        }

        // 请求通过后的回调.
        for (ProcessorSlotEntryCallback<DefaultNode> handler : StatisticSlotCallbackRegistry.getEntryCallbacks()) {
    
    
            handler.onPass(context, resourceWrapper, node, count, args);
        }
    } catch (Throwable e) {
    
    
        // 各种异常处理就省略了。。。
        context.getCurEntry().setError(e);

        throw e;
    }
}

Además, cabe señalar que todas las acciones de cuenta +1 incluyen dos partes, a modo de node.addPassRequest(count);ejemplo:

@Override
public void addPassRequest(int count) {
    
    
    // DefaultNode的计数器,代表当前链路的 计数器
    super.addPassRequest(count);
    // ClusterNode计数器,代表当前资源的 总计数器
    this.clusterNode.addPassRequest(count);
}

El método de conteo específico se verá más adelante.

A continuación, ingrese los espacios relevantes para la verificación de la regla, en orden:

  • AuthoritySlot: responsable de las reglas de autorización (control de fuente)
  • SystemSlot: responsable de las reglas de protección del sistema
  • ParamFlowSlot: responsable de las reglas de limitación de corriente de los parámetros de punto de acceso
  • FlowSlot: responsable de las reglas de limitación de flujo
  • DegradeSlot: responsable de degradar las reglas

2.6.Ranura de autoridad

Responsable de juzgar las reglas de autorización del origen de la solicitud, como se muestra en la figura:
inserte la descripción de la imagen aquí

API principales:

@Override
public void entry(Context context, ResourceWrapper resourceWrapper, DefaultNode node, int count, boolean prioritized, Object... args)
    throws Throwable {
    
    
    // 校验黑白名单
    checkBlackWhiteAuthority(resourceWrapper, context);
    // 进入下一个 slot
    fireEntry(context, resourceWrapper, node, count, prioritized, args);
}

La lógica de la verificación de listas blancas y negras:

void checkBlackWhiteAuthority(ResourceWrapper resource, Context context) throws AuthorityException {
    
    
    // 获取授权规则
    Map<String, Set<AuthorityRule>> authorityRules = AuthorityRuleManager.getAuthorityRules();

    if (authorityRules == null) {
    
    
        return;
    }

    Set<AuthorityRule> rules = authorityRules.get(resource.getName());
    if (rules == null) {
    
    
        return;
    }
	// 遍历规则并判断
    for (AuthorityRule rule : rules) {
    
    
        if (!AuthorityRuleChecker.passCheck(rule, context)) {
    
    
            // 规则不通过,直接抛出异常
            throw new AuthorityException(context.getOrigin(), rule);
        }
    }
}

Mira el método de nuevo AuthorityRuleChecker.passCheck(rule, context):

static boolean passCheck(AuthorityRule rule, Context context) {
    
    
    // 得到请求来源 origin
    String requester = context.getOrigin();

    // 来源为空,或者规则为空,都直接放行
    if (StringUtil.isEmpty(requester) || StringUtil.isEmpty(rule.getLimitApp())) {
    
    
        return true;
    }

    // rule.getLimitApp()得到的就是 白名单 或 黑名单 的字符串,这里先用 indexOf方法判断
    int pos = rule.getLimitApp().indexOf(requester);
    boolean contain = pos > -1;

    if (contain) {
    
    
        // 如果包含 origin,还要进一步做精确判断,把名单列表以","分割,逐个判断
        boolean exactlyMatch = false;
        String[] appArray = rule.getLimitApp().split(",");
        for (String app : appArray) {
    
    
            if (requester.equals(app)) {
    
    
                exactlyMatch = true;
                break;
            }
        }
        contain = exactlyMatch;
    }
	// 如果是黑名单,并且包含origin,则返回false
    int strategy = rule.getStrategy();
    if (strategy == RuleConstant.AUTHORITY_BLACK && contain) {
    
    
        return false;
    }
	// 如果是白名单,并且不包含origin,则返回false
    if (strategy == RuleConstant.AUTHORITY_WHITE && !contain) {
    
    
        return false;
    }
	// 其它情况返回true
    return true;
}

2.7.Ranura del sistema

SystemSlot es una verificación de reglas para la protección del sistema:
inserte la descripción de la imagen aquí

API principales:

@Override
public void entry(Context context, ResourceWrapper resourceWrapper, DefaultNode node, 
                  int count,boolean prioritized, Object... args) throws Throwable {
    
    
    // 系统规则校验
    SystemRuleManager.checkSystem(resourceWrapper);
    // 进入下一个 slot
    fireEntry(context, resourceWrapper, node, count, prioritized, args);
}

Mira el siguiente SystemRuleManager.checkSystem(resourceWrapper);código:

public static void checkSystem(ResourceWrapper resourceWrapper) throws BlockException {
    
    
    if (resourceWrapper == null) {
    
    
        return;
    }
    // Ensure the checking switch is on.
    if (!checkSystemStatus.get()) {
    
    
        return;
    }

    // 只针对入口资源做校验,其它直接返回
    if (resourceWrapper.getEntryType() != EntryType.IN) {
    
    
        return;
    }

    // 全局 QPS校验
    double currentQps = Constants.ENTRY_NODE == null ? 0.0 : Constants.ENTRY_NODE.successQps();
    if (currentQps > qps) {
    
    
        throw new SystemBlockException(resourceWrapper.getName(), "qps");
    }

    // 全局 线程数 校验
    int currentThread = Constants.ENTRY_NODE == null ? 0 : Constants.ENTRY_NODE.curThreadNum();
    if (currentThread > maxThread) {
    
    
        throw new SystemBlockException(resourceWrapper.getName(), "thread");
    }
	// 全局平均 RT校验
    double rt = Constants.ENTRY_NODE == null ? 0 : Constants.ENTRY_NODE.avgRt();
    if (rt > maxRt) {
    
    
        throw new SystemBlockException(resourceWrapper.getName(), "rt");
    }

    // 全局 系统负载 校验
    if (highestSystemLoadIsSet && getCurrentSystemAvgLoad() > highestSystemLoad) {
    
    
        if (!checkBbr(currentThread)) {
    
    
            throw new SystemBlockException(resourceWrapper.getName(), "load");
        }
    }

    // 全局 CPU使用率 校验
    if (highestCpuUsageIsSet && getCurrentCpuUsage() > highestCpuUsage) {
    
    
        throw new SystemBlockException(resourceWrapper.getName(), "cpu");
    }
}

2.8.ParamFlowSlot

ParamFlowSlot es el límite actual de los parámetros del punto de acceso, como se muestra en la figura:
inserte la descripción de la imagen aquí

Es un método que limita el flujo de contar QPS para diferentes valores de parámetros de solicitud para solicitudes de recursos entrantes.

  • El umbral independiente aquí es el número máximo de tokens: maxCount

  • La duración de la ventana estadística aquí es la duración estadística: duración

El significado es que los tokens maxCount se producen como máximo en cada duración, y la configuración en la figura anterior significa que se producen 2 tokens cada 1 segundo.

API principales:

@Override
public void entry(Context context, ResourceWrapper resourceWrapper, DefaultNode node,
                  int count, boolean prioritized, Object... args) throws Throwable {
    
    
    // 如果没有设置热点规则,直接放行
    if (!ParamFlowRuleManager.hasRules(resourceWrapper.getName())) {
    
    
        fireEntry(context, resourceWrapper, node, count, prioritized, args);
        return;
    }
	// 热点规则判断
    checkFlow(resourceWrapper, count, args);
    // 进入下一个 slot
    fireEntry(context, resourceWrapper, node, count, prioritized, args);
}

2.8.1 Cubo de fichas

El juicio de la regla de punto caliente adopta el algoritmo del cubo de fichas para realizar el límite actual del parámetro y establece el cubo de fichas para cada valor de parámetro diferente. El cubo de fichas de Sentinel consta de dos partes:
inserte la descripción de la imagen aquí

Las claves de estos dos Maps son los valores de los parámetros solicitados, pero los valores son diferentes, entre ellos:

  • tokenCounters: se utiliza para registrar la cantidad de tokens restantes
  • timeCounters: se utiliza para registrar la hora de la última solicitud

Cuando llega una solicitud con parámetros, el proceso básico de evaluación es el siguiente:
inserte la descripción de la imagen aquí

2.9.Ranura de flujo

FlowSlot es responsable del juicio de las reglas de limitación actuales, como se muestra en la figura:
inserte la descripción de la imagen aquí
incluyendo:

  • Tres modos de control de flujo: modo directo, modo de asociación, modo de enlace
  • Tres efectos de control de flujo: falla rápida, calentamiento, espera en cola

Los tres modos de control de flujo se dividen en dos categorías desde la perspectiva de las estadísticas de datos subyacentes :

  • Realizar estadísticas de limitación actuales en todas las solicitudes (ClusterNode) que ingresan recursos: modo directo, modo de asociación
  • Realizar estadísticas de limitación actual en algunos enlaces (Nodo predeterminado) que ingresan recursos: modo de enlace

Los tres efectos de control de flujo se pueden dividir en dos categorías desde la perspectiva del algoritmo de limitación de corriente :

  • Algoritmo de ventana de tiempo deslizante: falla rápido, calienta
  • Algoritmo de cubeta con fugas: esperando en la cola para que surta efecto

2.9.1 Proceso central

Las API principales son las siguientes:

@Override
public void entry(Context context, ResourceWrapper resourceWrapper, DefaultNode node, int count,
                  boolean prioritized, Object... args) throws Throwable {
    
    
    // 限流规则检测
    checkFlow(resourceWrapper, context, node, count, prioritized);
	// 放行
    fireEntry(context, resourceWrapper, node, count, prioritized, args);
}

método de control de flujo:

void checkFlow(ResourceWrapper resource, Context context, DefaultNode node, int count, boolean prioritized)
    throws BlockException {
    
    
    // checker是 FlowRuleChecker 类的一个对象
    checker.checkFlow(ruleProvider, resource, context, node, count, prioritized);
}

FlowRuleChecker instalado:

public void checkFlow(Function<String, Collection<FlowRule>> ruleProvider, 
                      ResourceWrapper resource,Context context, DefaultNode node,
                      int count, boolean prioritized) throws BlockException {
    
    
        if (ruleProvider == null || resource == null) {
    
    
            return;
        }
        // 获取当前资源的所有限流规则
        Collection<FlowRule> rules = ruleProvider.apply(resource.getName());
        if (rules != null) {
    
    
            for (FlowRule rule : rules) {
    
    
                // 遍历,逐个规则做校验
                if (!canPassCheck(rule, context, node, count, prioritized)) {
    
    
                    throw new FlowException(rule.getLimitApp(), rule);
                }
            }
        }
    }

FlowRule aquí es la interfaz de la regla que limita el flujo, y varias variables miembro en ella solo corresponden a los parámetros del formulario:

public class FlowRule extends AbstractRule {
    
    
    /**
     * 阈值类型 (0: 线程, 1: QPS).
     */
    private int grade = RuleConstant.FLOW_GRADE_QPS;
    /**
     * 阈值.
     */
    private double count;
    /**
     * 三种限流模式.
     *
     * {@link RuleConstant#STRATEGY_DIRECT} 直连模式;
     * {@link RuleConstant#STRATEGY_RELATE} 关联模式;
     * {@link RuleConstant#STRATEGY_CHAIN} 链路模式.
     */
    private int strategy = RuleConstant.STRATEGY_DIRECT;
    /**
     * 关联模式关联的资源名称.
     */
    private String refResource;
    /**
     * 3种流控效果.
     * 0. 快速失败, 1. warm up, 2. 排队等待, 3. warm up + 排队等待
     */
    private int controlBehavior = RuleConstant.CONTROL_BEHAVIOR_DEFAULT;
	// 预热时长
    private int warmUpPeriodSec = 10;
    /**
     * 队列最大等待时间.
     */
    private int maxQueueingTimeMs = 500;
    // 。。。 略
}

La lógica de validación se define FlowRuleCheckeren canPassCheckel método:

public boolean canPassCheck(/*@NonNull*/ FlowRule rule, Context context, DefaultNode node, int acquireCount,
                            boolean prioritized) {
    
    
    // 获取限流资源名称
    String limitApp = rule.getLimitApp();
    if (limitApp == null) {
    
    
        return true;
    }
	// 校验规则
    return passLocalCheck(rule, context, node, acquireCount, prioritized);
}

Ingrese passLocalCheck():

private static boolean passLocalCheck(FlowRule rule, Context context, DefaultNode node,
                                      int acquireCount,  boolean prioritized) {
    
    
    // 基于限流模式判断要统计的节点, 
    // 如果是直连模式,关联模式,对ClusterNode统计,如果是链路模式,则对DefaultNode统计
    Node selectedNode = selectNodeByRequesterAndStrategy(rule, context, node);
    if (selectedNode == null) {
    
    
        return true;
    }
	// 判断规则
    return rule.getRater().canPass(selectedNode, acquireCount, prioritized);
}

FlowRule#getRater()El juicio de las reglas aquí debe obtener primero el controlador de flujo TrafficShapingControllery luego hacer el límite de flujo.

Y TrafficShapingControllerhay 3 implementaciones:
inserte la descripción de la imagen aquí

  • DefaultController: fail fast, el método predeterminado, basado en el algoritmo de ventana de tiempo deslizante
  • WarmUpController: modo de calentamiento, basado en el algoritmo de ventana de tiempo deslizante, pero el umbral es dinámico
  • RateLimiterController: modo de cola de espera, basado en el algoritmo de cubeta con fugas

El juicio final de limitación de corriente está en el método canPass de TrafficShapingController.

2.9.2 Ventana de tiempo móvil

La función de la ventana de tiempo deslizante se divide en dos partes:

  • Una es la función de conteo de QPS de la ventana de intervalo de tiempo, que se llama en StatisticSlot
  • El segundo es acumular la ventana de intervalo de tiempo QPS en la ventana deslizante, que se llama en FlowRule

Veamos primero la función de conteo de QPS de la ventana de intervalo de tiempo.

2.9.2.1 Estadísticas de volumen de solicitud de ventana de tiempo

Mirando hacia atrás en la sección StatisticSlot en el Capítulo 2.5, hay un código de este tipo:
inserte la descripción de la imagen aquí

Está contando los QPS que pasan por el nodo, sigamos y veamos, aquí está el interior del DefaultNode:
inserte la descripción de la imagen aquí

Se encuentra que las estadísticas de QPS se están realizando en DefaultNodey al mismo tiempo ClusterNode. Lo sabemos DefaultNodey ClusterNodeson StatisticNodesubclases. addPassRequest()Los métodos de llamada aquí eventualmente entrarán StatisticNode.

Sigue a cualquiera:
inserte la descripción de la imagen aquí

Hay dos estadísticas de latitud de segundos y minutos, correspondientes a dos contadores. Encuentre la variable miembro correspondiente, puede ver:
inserte la descripción de la imagen aquí

Ambos contadores son de tipo ArrayMetric y se pasan en dos parámetros:

// intervalInMs:是滑动窗口的时间间隔,默认为 1 秒
// sampleCount: 时间窗口的分隔数量,默认为 2,就是把 1秒分为 2个小时间窗
public ArrayMetric(int sampleCount, int intervalInMs) {
    
    
    this.data = new OccupiableBucketLeapArray(sampleCount, intervalInMs);
}

Como se muestra en la imagen:
inserte la descripción de la imagen aquí

A continuación, ingresamos los métodos ArrayMetricde la clase addPass:

@Override
public void addPass(int count) {
    
    
    // 获取当前时间所在的时间窗
    WindowWrap<MetricBucket> wrap = data.currentWindow();
    // 计数器 +1
    wrap.value().addPass(count);
}

Entonces, ¿cómo sabe el contador en qué ventana se encuentra actualmente?

Los datos aquí son un LeapArray:
inserte la descripción de la imagen aquí
Cuatro propiedades de LeapArray:

public abstract class LeapArray<T> {
    
    
    // 小窗口的时间长度,默认是500ms ,值 = intervalInMs / sampleCount
    protected int windowLengthInMs;
    // 滑动窗口内的 小窗口 数量,默认为 2
    protected int sampleCount;
    // 滑动窗口的时间间隔,默认为 1000ms
    protected int intervalInMs;
    // 滑动窗口的时间间隔,单位为秒,默认为 1
    private double intervalInSecond;
}

LeapArray es una matriz circular. Debido a que el tiempo es infinito, la longitud de la matriz no puede ser infinita. Por lo tanto, cada cuadrícula de la matriz se coloca en una ventana de tiempo (ventana). Cuando la matriz está llena, la marca de la esquina vuelve a 0, cubriendo la ventana original.
inserte la descripción de la imagen aquí

Debido a que la ventana deslizante se divide en ventanas pequeñas de sampleCount como máximo, siempre que la longitud de la matriz sea mayor que sampleCount, las dos ventanas pequeñas en la ventana deslizante más cercana nunca se cubrirán, por lo que no hay necesidad de preocuparse por el problema. de datos antiguos que están cubiertos.

Cómo hacemos el seguimiento data.currentWindow();:

public WindowWrap<T> currentWindow(long timeMillis) {
    
    
    if (timeMillis < 0) {
    
    
        return null;
    }
	// 计算当前时间对应的数组角标
    int idx = calculateTimeIdx(timeMillis);
    // 计算当前时间所在窗口的开始时间.
    long windowStart = calculateWindowStart(timeMillis);

    /*
         * 先根据角标获取数组中保存的 oldWindow 对象,可能是旧数据,需要判断.
         *
         * (1) oldWindow 不存在, 说明是第一次,创建新 window并存入,然后返回即可
         * (2) oldWindow的 starTime = 本次请求的 windowStar, 说明正是要找的窗口,直接返回.
         * (3) oldWindow的 starTime < 本次请求的 windowStar, 说明是旧数据,需要被覆盖,创建 
         *     新窗口,覆盖旧窗口
         */
    while (true) {
    
    
        WindowWrap<T> old = array.get(idx);
        if (old == null) {
    
    
            // 创建新 window
            WindowWrap<T> window = new WindowWrap<T>(windowLengthInMs, windowStart, newEmptyBucket(timeMillis));
            // 基于CAS写入数组,避免线程安全问题
            if (array.compareAndSet(idx, null, window)) {
    
    
                // 写入成功,返回新的 window
                return window;
            } else {
    
    
                // 写入失败,说明有并发更新,等待其它人更新完成即可
                Thread.yield();
            }
        } else if (windowStart == old.windowStart()) {
    
    
            return old;
        } else if (windowStart > old.windowStart()) {
    
    
            if (updateLock.tryLock()) {
    
    
                try {
    
    
                    // 获取并发锁,覆盖旧窗口并返回
                    return resetWindowTo(old, windowStart);
                } finally {
    
    
                    updateLock.unlock();
                }
            } else {
    
    
                // 获取锁失败,等待其它线程处理就可以了
                Thread.yield();
            }
        } else if (windowStart < old.windowStart()) {
    
    
            // 这种情况不应该存在,写这里只是以防万一。
            return new WindowWrap<T>(windowLengthInMs, windowStart, newEmptyBucket(timeMillis));
        }
    }
}

Después de encontrar la ventana (WindowWrap) donde se encuentra la hora actual, simplemente llame al método add en el objeto WindowWrap y el contador será +1.

Solo se encarga de contabilizar el volumen de solicitudes de cada ventana, no de la interceptación. La intercepción de limitación de corriente depende de la lógica en FlowSlot.

2.9.2.2 Cálculo QPS de ventana deslizante

Como mencionamos en la sección 2.9.1, el juicio de límite actual de FlowSlot se implementa en última instancia mediante los métodos TrafficShapingControlleren la interfaz . canPassEsta interfaz tiene tres clases de implementación:

  • DefaultController: fail fast, el método predeterminado, basado en el algoritmo de ventana de tiempo deslizante
  • WarmUpController: modo de calentamiento, basado en el algoritmo de ventana de tiempo deslizante, pero el umbral es dinámico
  • RateLimiterController: modo de cola de espera, basado en el algoritmo de cubeta con fugas

Por lo tanto, seguimos el método canPass en el DefaultController predeterminado para analizar:

@Override
public boolean canPass(Node node, int acquireCount, boolean prioritized) {
    
    
    // 计算目前为止滑动窗口内已经存在的请求量
    int curCount = avgUsedTokens(node);
    // 判断:已使用请求量 + 需要的请求量(1) 是否大于 窗口的请求阈值
    if (curCount + acquireCount > count) {
    
    
        // 大于,说明超出阈值,返回false
        if (prioritized && grade == RuleConstant.FLOW_GRADE_QPS) {
    
    
            long currentTime;
            long waitInMs;
            currentTime = TimeUtil.currentTimeMillis();
            waitInMs = node.tryOccupyNext(currentTime, acquireCount, count);
            if (waitInMs < OccupyTimeoutProperty.getOccupyTimeout()) {
    
    
                node.addWaitingRequest(currentTime + waitInMs, acquireCount);
                node.addOccupiedPass(acquireCount);
                sleep(waitInMs);

                // PriorityWaitException indicates that the request will pass after waiting for {@link @waitInMs}.
                throw new PriorityWaitException(waitInMs);
            }
        }
        return false;
    }
    // 小于等于,说明在阈值范围内,返回true
    return true;
}

Por lo tanto, la clave para juzgarint curCount = avgUsedTokens(node);

private int avgUsedTokens(Node node) {
    
    
    if (node == null) {
    
    
        return DEFAULT_AVG_USED_TOKENS;
    }
    return grade == RuleConstant.FLOW_GRADE_THREAD ? node.curThreadNum() : (int)(node.passQps());
}

Debido a que usamos limitación de corriente, siga node.passQps()la lógica:

// 这里又进入了 StatisticNode类
@Override
public double passQps() {
    
    
    // 请求量 ÷ 滑动窗口时间间隔 ,得到的就是QPS
    return rollingCounterInSecond.pass() / rollingCounterInSecond.getWindowIntervalInSec();
}

Entonces, rollingCounterInSecond.pass()¿cómo se obtiene la cantidad solicitada?

// rollingCounterInSecond 本质是ArrayMetric,之前说过
@Override
public long pass() {
    
    
    // 获取当前窗口
    data.currentWindow();
    long pass = 0;
    // 获取 当前时间的 滑动窗口范围内 的所有小窗口
    List<MetricBucket> list = data.values();
	// 遍历
    for (MetricBucket window : list) {
    
    
        // 累加求和
        pass += window.pass();
    }
    // 返回
    return pass;
}

Veamos data.values()cómo poner todas las ventanas pequeñas dentro del rango de la ventana corrediza:

// 此处进入LeapArray类中:

public List<T> values(long timeMillis) {
    
    
    if (timeMillis < 0) {
    
    
        return new ArrayList<T>();
    }
    // 创建空集合,大小等于 LeapArray长度
    int size = array.length();
    List<T> result = new ArrayList<T>(size);
	// 遍历 LeapArray
    for (int i = 0; i < size; i++) {
    
    
        // 获取每一个小窗口
        WindowWrap<T> windowWrap = array.get(i);
        // 判断这个小窗口是否在 滑动窗口时间范围内(1秒内)
        if (windowWrap == null || isWindowDeprecated(timeMillis, windowWrap)) {
    
    
            // 不在范围内,则跳过
            continue;
        }
        // 在范围内,则添加到集合中
        result.add(windowWrap.value());
    }
    // 返回集合
    return result;
}

Entonces, isWindowDeprecated(timeMillis, windowWrap)¿cómo juzgar si la ventana cumple con los requisitos?

public boolean isWindowDeprecated(long time, WindowWrap<T> windowWrap) {
    
    
    // 当前时间 - 窗口开始时间  是否大于 滑动窗口的最大间隔(1秒)
    // 也就是说,我们要统计的时 距离当前时间1秒内的 小窗口的 count之和
    return time - windowWrap.windowStart() > intervalInMs;
}

2.9.3 Cubo con fugas

Como mencionamos en la sección anterior, el juicio de límite actual de FlowSlot se implementa en última instancia mediante los métodos TrafficShapingControlleren la interfaz . canPassEsta interfaz tiene tres clases de implementación:

  • DefaultController: fail fast, el método predeterminado, basado en el algoritmo de ventana de tiempo deslizante
  • WarmUpController: modo de calentamiento, basado en el algoritmo de ventana de tiempo deslizante, pero el umbral es dinámico
  • RateLimiterController: modo de cola de espera, basado en el algoritmo de cubeta con fugas

Por lo tanto, seguimos el método canPass en el RateLimiterController predeterminado para analizar:

@Override
public boolean canPass(Node node, int acquireCount, boolean prioritized) {
    
    
    // Pass when acquire count is less or equal than 0.
    if (acquireCount <= 0) {
    
    
        return true;
    }
    // 阈值小于等于 0 ,阻止请求
    if (count <= 0) {
    
    
        return false;
    }
	// 获取当前时间
    long currentTime = TimeUtil.currentTimeMillis();
    // 计算两次请求之间允许的最小时间间隔
    long costTime = Math.round(1.0 * (acquireCount) / count * 1000);

    // 计算本次请求 允许执行的时间点 = 最近一次请求的可执行时间 + 两次请求的最小间隔
    long expectedTime = costTime + latestPassedTime.get();
	// 如果允许执行的时间点小于当前时间,说明可以立即执行
    if (expectedTime <= currentTime) {
    
    
        // 更新上一次的请求的执行时间
        latestPassedTime.set(currentTime);
        return true;
    } else {
    
    
        // 不能立即执行,需要计算 预期等待时长
        // 预期等待时长 = 两次请求的最小间隔 +最近一次请求的可执行时间 - 当前时间
        long waitTime = costTime + latestPassedTime.get() - TimeUtil.currentTimeMillis();
        // 如果预期等待时间超出阈值,则拒绝请求
        if (waitTime > maxQueueingTimeMs) {
    
    
            return false;
        } else {
    
    
            // 预期等待时间小于阈值,更新最近一次请求的可执行时间,加上costTime
            long oldTime = latestPassedTime.addAndGet(costTime);
            try {
    
    
                // 保险起见,再判断一次预期等待时间,是否超过阈值
                waitTime = oldTime - TimeUtil.currentTimeMillis();
                if (waitTime > maxQueueingTimeMs) {
    
    
                    // 如果超过,则把刚才 加 的时间再 减回来
                    latestPassedTime.addAndGet(-costTime);
                    // 拒绝
                    return false;
                }
                // in race condition waitTime may <= 0
                if (waitTime > 0) {
    
    
                    // 预期等待时间在阈值范围内,休眠要等待的时间,醒来后继续执行
                    Thread.sleep(waitTime);
                }
                return true;
            } catch (InterruptedException e) {
    
    
            }
        }
    }
    return false;
}

Es básicamente lo mismo que el algoritmo de balde con fugas que analizamos antes:
inserte la descripción de la imagen aquí

2.10.DegradeSlot

El último obstáculo es juzgar las reglas de degradación.

La degradación de Sentinel se implementa en base a una máquina de estado:
inserte la descripción de la imagen aquí

La implementación correspondiente está en la clase DegradeSlot, la API central:

@Override
public void entry(Context context, ResourceWrapper resourceWrapper, DefaultNode node, 
                  int count, boolean prioritized, Object... args) throws Throwable {
    
    
    // 熔断降级规则判断
    performChecking(context, resourceWrapper);
	// 继续下一个slot
    fireEntry(context, resourceWrapper, node, count, prioritized, args);
}

Continúe para ingresar performCheckingel método:

void performChecking(Context context, ResourceWrapper r) throws BlockException {
    
    
    // 获取当前资源上的所有的断路器 CircuitBreaker
    List<CircuitBreaker> circuitBreakers = DegradeRuleManager.getCircuitBreakers(r.getName());
    if (circuitBreakers == null || circuitBreakers.isEmpty()) {
    
    
        return;
    }
    for (CircuitBreaker cb : circuitBreakers) {
    
    
        // 遍历断路器,逐个判断
        if (!cb.tryPass(context)) {
    
    
            throw new DegradeException(cb.getRule().getLimitApp(), cb.getRule());
        }
    }
}

2.10.1.Disyuntor

Ingresamos al método tryPass de CircuitBreaker:

@Override
public boolean tryPass(Context context) {
    
    
    // 判断状态机状态
    if (currentState.get() == State.CLOSED) {
    
    
        // 如果是closed状态,直接放行
        return true;
    }
    if (currentState.get() == State.OPEN) {
    
    
        // 如果是OPEN状态,断路器打开
        // 继续判断OPEN时间窗是否结束,如果是则把状态从OPEN切换到 HALF_OPEN,返回true
        return retryTimeoutArrived() && fromOpenToHalfOpen(context);
    }
    // OPEN状态,并且时间窗未到,返回false
    return false;
}

El juicio sobre la ventana de tiempo está en retryTimeoutArrived()el método:

protected boolean retryTimeoutArrived() {
    
    
    // 当前时间 大于 下一次 HalfOpen的重试时间
    return TimeUtil.currentTimeMillis() >= nextRetryTimestamp;
}

OPEN a HALF_OPEN interruptor en el fromOpenToHalfOpen(context)método:

protected boolean fromOpenToHalfOpen(Context context) {
    
    
    // 基于CAS修改状态,从 OPEN到 HALF_OPEN
    if (currentState.compareAndSet(State.OPEN, State.HALF_OPEN)) {
    
    
        // 状态变更的事件通知
        notifyObservers(State.OPEN, State.HALF_OPEN, null);
        // 得到当前资源
        Entry entry = context.getCurEntry();
        // 给资源设置监听器,在资源Entry销毁时(资源业务执行完毕时)触发
        entry.whenTerminate(new BiConsumer<Context, Entry>() {
    
    
            @Override
            public void accept(Context context, Entry entry) {
    
    
                // 判断 资源业务是否异常
                if (entry.getBlockError() != null) {
    
    
                    // 如果异常,则再次进入OPEN状态
                    currentState.compareAndSet(State.HALF_OPEN, State.OPEN);
                    notifyObservers(State.HALF_OPEN, State.OPEN, 1.0d);
                }
            }
        });
        return true;
    }
    return false;
}

Hay cambios de OPEN a HALF_OPEN y de HALF_OPEN a OPEN, pero todavía hay algunos cambios:

  • De CERRADO a ABIERTO
  • De HALF_OPEN a CERRADO

2.10.2 Activación del interruptor automático

Después de que la solicitud pase por todas las ranuras, se debe ejecutar el método de salida, y en el método de salida de DegradeSlot:
inserte la descripción de la imagen aquí

Se llamará al método onRequestComplete de CircuitBreaker. Y CircuitBreaker tiene dos implementaciones:
inserte la descripción de la imagen aquí

Tomemos el fusible de relación anormal como ejemplo para ver cómo ExceptionCircuitBreakeringresar onRequestComplete:

@Override
public void onRequestComplete(Context context) {
    
    
    // 获取资源 Entry
    Entry entry = context.getCurEntry();
    if (entry == null) {
    
    
        return;
    }
    // 尝试获取 资源中的 异常
    Throwable error = entry.getError();
    // 获取计数器,同样采用了滑动窗口来计数
    SimpleErrorCounter counter = stat.currentWindow().value();
    if (error != null) {
    
    
        // 如果出现异常,则 error计数器 +1
        counter.getErrorCount().add(1);
    }
    // 不管是否出现异常,total计数器 +1
    counter.getTotalCount().add(1);
	// 判断异常比例是否超出阈值
    handleStateChangeWhenThresholdExceeded(error);
}

Veamos el método de juicio de umbral:

private void handleStateChangeWhenThresholdExceeded(Throwable error) {
    
    
    // 如果当前已经是OPEN状态,不做处理
    if (currentState.get() == State.OPEN) {
    
    
        return;
    }
	// 如果已经是 HALF_OPEN 状态,判断是否需求切换状态
    if (currentState.get() == State.HALF_OPEN) {
    
    
        if (error == null) {
    
    
            // 没有异常,则从 HALF_OPEN 到 CLOSED
            fromHalfOpenToClose();
        } else {
    
    
            // 有一次,再次进入OPEN
            fromHalfOpenToOpen(1.0d);
        }
        return;
    }
	// 说明当前是CLOSE状态,需要判断是否触发阈值
    List<SimpleErrorCounter> counters = stat.values();
    long errCount = 0;
    long totalCount = 0;
    // 累加计算 异常请求数量、总请求数量
    for (SimpleErrorCounter counter : counters) {
    
    
        errCount += counter.errorCount.sum();
        totalCount += counter.totalCount.sum();
    }
    // 如果总请求数量未达到阈值,什么都不做
    if (totalCount < minRequestAmount) {
    
    
        return;
    }
    double curCount = errCount;
    if (strategy == DEGRADE_GRADE_EXCEPTION_RATIO) {
    
    
        // 计算请求的异常比例
        curCount = errCount * 1.0d / totalCount;
    }
    // 如果比例超过阈值,切换到 OPEN
    if (curCount > threshold) {
    
    
        transformToOpen(curCount);
    }
}

Supongo que te gusta

Origin blog.csdn.net/sinat_38316216/article/details/129877251
Recomendado
Clasificación