Analyse und Lernen des Sentinel-Quellcodes

Vorwort

  • Dieser Artikel stammt aus dem Dark Horse Microservice-Teil. Da das Referenzdokument sehr detailliert ist, werde ich das Originaldokument direkt zum Teilen verwenden! ! !
  • Dieser Artikel dient nur zum Lernen und zur Kommunikation. Bei Verstößen wenden Sie sich bitte rechtzeitig an den Autor, um das Problem zu beheben!

Analyse des Sentinel-Quellcodes

1. Das Grundkonzept von Sentinel

Sentinel implementiert Funktionen wie Strombegrenzung, Isolierung, Verschlechterung und Sicherung. Im Wesentlichen sind zwei Dinge zu tun:

  • Statistische Daten: Statistische Zugriffsdaten einer Ressource (QPS, RT, etc.)
  • Regelbeurteilung: Beurteilen, ob die aktuellen Begrenzungsregeln, Isolationsregeln, Herabstufungsregeln und Sicherungsregeln erfüllt sind

Die Ressource hier ist das Unternehmen, das durch Sentinel geschützt werden möchte. Beispielsweise ist die im Projekt definierte Controller-Methode die standardmäßig durch Sentinel geschützte Ressource.

1.1.ProzessorSlotChain

Das Kerngerüst zum Erreichen der oben genannten Funktionen ist eine Klasse namens ProcessorSlotChain. Diese Klasse basiert auf dem Chain-of-Responsibility-Modell, das verschiedene Funktionen (Strombegrenzung, Herabstufung, Systemschutz) nacheinander in Slots kapselt und sie nach Eingang der Anforderung nacheinander ausführt.

Der Arbeitsablauf ist in der Abbildung dargestellt:
Fügen Sie hier eine Bildbeschreibung ein

Slots in der Verantwortungskette werden ebenfalls in zwei Kategorien unterteilt:

  • Statistischer Datenkonstruktionsteil (Statistik)
    • NodeSelectorSlot: Verantwortlich für den Aufbau der Knoten (DefaultNode) im Cluster-Punkt-Link und die Bildung dieser Knoten in einem Link-Baum
    • ClusterBuilderSlot: ClusterNode, der für den Aufbau einer bestimmten Ressource verantwortlich ist. ClusterNode kann Informationen zum Ressourcenbetrieb (Antwortzeit, QPS, Blocknummer, Thread-Nummer, Ausnahmenummer usw.) und Quellinformationen (Ursprungsname) speichern.
    • StatisticSlot: Verantwortlich für Statistiken zu Echtzeit-Anrufdaten, einschließlich laufender Informationen, Quelleninformationen usw.
  • Regelprüfungsteil (Regelprüfung)
    • AuthoritySlot: verantwortlich für Autorisierungsregeln (Quellcodeverwaltung)
    • SystemSlot: Verantwortlich für Systemschutzregeln
    • ParamFlowSlot: Verantwortlich für die Strombegrenzungsregeln für Hotspot-Parameter
    • FlowSlot: Verantwortlich für die Flussbegrenzungsregeln
    • DegradeSlot: Verantwortlich für die Verschlechterung von Regeln

1.2.Knoten

Die Clusterpunktverbindung in Sentinel besteht nacheinander aus Knoten. Knoten ist eine Schnittstelle, einschließlich der folgenden Implementierungen:
Fügen Sie hier eine Bildbeschreibung ein

Alle Knoten können Zugriffsstatistiken auf Ressourcen aufzeichnen, daher sind sie alle Unterklassen von StatisticNode.

Je nach Funktion wird es in zwei Arten von Knoten unterteilt:

  • DefaultNode: Stellt jede Ressource im Linkbaum dar. Wenn eine Ressource in verschiedenen Links erscheint, werden verschiedene DefaultNode-Knoten erstellt. Der Eingangsknoten des Baums heißt EntranceNode und ist ein spezieller DefaultNode
  • ClusterNode: Stellt Ressourcen dar. Unabhängig davon, in wie vielen Links eine Ressource vorkommt, gibt es nur einen ClusterNode. Aufgezeichnet wird die Summe aller statistischen Daten über den Zugriff auf die aktuelle Ressource.

DefaultNode zeichnet die Zugriffsdaten von Ressourcen im aktuellen Link auf, die zur Implementierung der aktuellen Begrenzungsregeln basierend auf dem Linkmodus verwendet werden. ClusterNode zeichnet die Zugriffsdaten von Ressourcen in allen Links auf und realisiert die aktuellen Begrenzungsregeln des Standardmodus und des Assoziationsmodus.

Beispiel: Wir haben zwei Unternehmen in einem SpringMVC-Projekt:

  • Geschäft 1: Die Ressourcen im Controller /order/querygreifen auf die Ressourcen im Dienst zu/goods
  • Geschäft 2: Die Ressourcen im Controller /order/savegreifen auf die Ressourcen im Dienst zu/goods

Das erstellte Linkdiagramm sieht wie folgt aus:

Fügen Sie hier eine Bildbeschreibung ein

1.3.Eintrag

Standardmäßig verwendet Sentinel die Methode im Controller als geschützte Ressource. Die Frage ist also: Wie markieren wir einen Codeabschnitt als Sentinel-Ressource?

Die Ressourcen in Sentinel werden durch Eintrag dargestellt. API-Beispiel zum Deklarieren von Entry:

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

1.3.1. Benutzerdefinierte Ressourcen

OrderServiceBeispielsweise markieren wir die Methode als Ressource im order-service service queryOrderById().

1) Führen Sie zunächst die Sentinel-Abhängigkeit im Bestellservice ein

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

2) Anschließend konfigurieren Sie die Sentinel-Adresse

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

3) Ändern Sie die queryOrderById-Methode der OrderService-Klasse

Der Code wird wie folgt implementiert:

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) Zugang

Öffnen Sie den Browser und besuchen Sie den Bestellservice: http://localhost:8080/order/101

Öffnen Sie dann die Sentinel-Konsole, um den Cluster-Link anzuzeigen:

Fügen Sie hier eine Bildbeschreibung ein

1.3.2. Ressourcen anhand von Anmerkungen markieren

Wenn wir Sentinel zuvor kennengelernt haben, wissen wir, dass wir Ressourcen markieren können, indem wir der Methode die Annotation @SentinelResource hinzufügen.
Fügen Sie hier eine Bildbeschreibung ein

Wie wird dies erreicht?

Werfen wir einen Blick auf die Sentinel-Abhängigkeitspakete, die wir eingeführt haben:
Fügen Sie hier eine Bildbeschreibung ein

Die Anweisung spring.factories muss wie folgt die Konfigurationsklasse für die automatische Assemblierung sein:
Fügen Sie hier eine Bildbeschreibung ein

Schauen wir uns SentinelAutoConfigurationdiese Klasse an:
Fügen Sie hier eine Bildbeschreibung ein

Wie Sie sehen können, wird hier eine Bean deklariert 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());
            }
        }
    }
}

Einfach ausgedrückt ist die Annotation @SentinelResource ein Marker, und Sentinel, basierend auf der AOP-Idee, umgibt und erweitert die markierte Methode, um die Erstellung Entryvon Ressourcen abzuschließen ().

1.4.Kontext

Im vorherigen Abschnitt haben wir festgestellt, dass es zusätzlich zu den beiden Ressourcen der Controller-Methode und der Service-Methode im Cluster-Link auch einen Standardeintragsknoten gibt:

sentinel_spring_web_context ist ein Knoten vom Typ EntranceNode

Dieser Knoten wird von Sentinel für uns erstellt, wenn der Kontext initialisiert wird.

1.4.1. Was ist Kontext?

Was ist also Kontext?

  • Der Kontext stellt den Aufruflink-Kontext dar, durchläuft alle Ressourcen in einem Aufruflink ( Entry) und basiert auf ThreadLocal.
  • Der Kontext verwaltet Informationen wie den Einstiegsknoten ( entranceNode), den CurNode (aktueller Ressourcenknoten) dieses Anruflinks und die Anrufquelle ( ).origin
  • Nachfolgende Slots können DefaultNode oder ClusterNode über den Kontext abrufen, um statistische Daten zu erhalten und eine vollständige Regelbeurteilung durchzuführen
  • Während des Initialisierungsprozesses von Context wird EntranceNode erstellt und contextName ist der Name von EntranceNode

Die entsprechenden APIs lauten wie folgt:

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

1.4.2. Kontextinitialisierung

Wann wird dieser Kontext initialisiert?

1.4.2.1. Autowiring

Werfen wir einen Blick auf die Sentinel-Abhängigkeitspakete, die wir eingeführt haben:
Fügen Sie hier eine Bildbeschreibung ein

Die Anweisung spring.factories muss wie folgt die Konfigurationsklasse für die automatische Assemblierung sein:
Fügen Sie hier eine Bildbeschreibung ein

Schauen wir uns zunächst die SentinelWebAutoConfiguration-Klasse an:
Fügen Sie hier eine Bildbeschreibung ein

Diese Klasse implementiert WebMvcConfigurer. Wir wissen, dass dies die Klasse ist, die in der benutzerdefinierten SpringMVC-Konfiguration verwendet wird. Sie können HandlerInterceptor konfigurieren:

Fügen Sie hier eine Bildbeschreibung ein

Sie können sehen, dass hier ein SentinelWebInterceptorInterceptor konfiguriert ist.

SentinelWebInterceptorDie Aussage lautet wie folgt:
Fügen Sie hier eine Bildbeschreibung ein

Es wurde festgestellt, dass es AbstractSentinelInterceptordiese Klasse geerbt hat.
Fügen Sie hier eine Bildbeschreibung ein

HandlerInterceptorDer Interceptor fängt alle in den Controller eintretenden Methoden ab und führt preHandledie Pre-Intercept-Methode aus. Die Initialisierung des Kontexts ist hier abgeschlossen.

1.4.2.2.AbstractSentinelInterceptor

HandlerInterceptorDer Interceptor fängt alle in den Controller eintretenden Methoden ab und führt preHandledie Pre-Intercept-Methode aus. Die Initialisierung des Kontexts ist hier abgeschlossen.

Werfen wir einen Blick auf die Implementierung dieser Klasse 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

Der Weg, Kontext zu erstellen, ist ContextUtil.enter(contextName, origin);

Wir geben die Methode ein:

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

So geben Sie ein 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. ProcessorSlotChain-Ausführungsprozess

Als nächstes verfolgen wir den Quellcode und überprüfen den Ausführungsprozess von ProcessorSlotChain.

2.1. Eintritt

Zunächst zurück zum Einstiegspunkt von allem, der Methode AbstractSentinelInterceptorder Klasse preHandle:

Fügen Sie hier eine Bildbeschreibung ein

Außerdem SentinelResourceAspectdie Surround-Verstärkungsmethode:
Fügen Sie hier eine Bildbeschreibung ein

Wie Sie sehen, muss jede Ressource SphU.entry()diese Methode ausführen:

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

Geben Sie weiter ein 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);
}

So geben Sie ein 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;
}

In diesem Code ProcessorSlotChainwird das Objekt abgerufen und dann wird jeder Slot in der SlotChain basierend auf chain.entry() ausgeführt. Und hier ist seine Implementierungsklasse: DefaultProcessorSlotChain.

Nachdem Sie die ProcessorSlotChain erhalten haben, wird sie in einer Karte gespeichert. Der Schlüssel ist ResourceWrapper und der Wert ist ProcessorSlotChain.

Daher verfügt eine Ressource nur über eine ProcessorSlotChain .

2.2.DefaultProcessorSlotChain

Wir geben die Eingabemethode von DefaultProcessorSlotChain ein:

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

Hier ist der Typ zunächst AbstractLinkedProcessorSlot:
Fügen Sie hier eine Bildbeschreibung ein

Schauen Sie sich die Vererbungsbeziehung an:
Fügen Sie hier eine Bildbeschreibung ein

Daher muss „first“ eine dieser Implementierungsklassen sein. Gemäß der zuvor erwähnten Reihenfolge der Verantwortungskette sollte „first“ sein NodeSelectorSlot.

Da es jedoch auf dem Chain-of-Responsibility-Modell basiert, müssen Sie sich hier nur den nächsten Slot merken, der der nächste ist:
Fügen Sie hier eine Bildbeschreibung ein

next ist tatsächlich vom Typ NodeSelectSlot.

Und der nächste von NodeSelectSlot muss ClusterBuilderSlot sein, und so weiter:
Fügen Sie hier eine Bildbeschreibung ein

Es entsteht eine Verantwortungskette.

2.3.NodeSelectorSlot

NodeSelectorSlot ist dafür verantwortlich, die Knoten (DefaultNode) im Cluster-Punkt-Link zu erstellen und diese Knoten zu einem Link-Baum zu formen.

Kerncode:

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

Dieser Slot erreicht mehrere Dinge:

  • Erstellen Sie einen DefaultNode für die aktuelle Ressource
  • Legen Sie den DefaultNode im Cache ab, und der Schlüssel ist contextName, sodass Anfragen für verschiedene Linkeinträge mehrere DefaultNodes und nur einen DefaultNode für denselben Link erstellen
  • Setzen Sie den DefaultNode der aktuellen Ressource auf den childNode der vorherigen Ressource
  • Setzen Sie den DefaultNode der aktuellen Ressource im Kontext auf curNode (den aktuellen Knoten).

Der nächste Slot ist ClusterBuilderSlot

2.4.ClusterBuilderSlot

ClusterBuilderSlot ist für den Aufbau eines ClusterNode einer Ressource verantwortlich, der Kerncode:

@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.StatisticSlot

StatisticSlot ist für die Zählung von Anrufdaten in Echtzeit verantwortlich, einschließlich laufender Informationen (Anzahl der Besuche, Anzahl der Threads), Quelleninformationen usw.

StatisticSlot ist der Schlüssel zur Strombegrenzung, bei der ein Zähler basierend auf dem gleitenden Zeitfensteralgorithmus verwaltet wird , um die Anzahl der Anforderungen zu zählen, die eine bestimmte Ressource erreichen.

Kerncode:

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

Darüber hinaus ist zu beachten, dass alle Count +1-Aktionen beispielsweise aus zwei Teilen bestehen node.addPassRequest(count);:

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

Die spezifische Zählmethode wird später erläutert.

Geben Sie als Nächstes die relevanten Slots für die Regelüberprüfung ein, in der folgenden Reihenfolge:

  • AuthoritySlot: verantwortlich für Autorisierungsregeln (Quellcodeverwaltung)
  • SystemSlot: Verantwortlich für Systemschutzregeln
  • ParamFlowSlot: Verantwortlich für die Strombegrenzungsregeln für Hotspot-Parameter
  • FlowSlot: Verantwortlich für die Flussbegrenzungsregeln
  • DegradeSlot: Verantwortlich für die Verschlechterung von Regeln

2.6.AuthoritySlot

Verantwortlich für die Beurteilung der Autorisierungsregeln des Ursprungs der Anfrage, wie in der Abbildung dargestellt:
Fügen Sie hier eine Bildbeschreibung ein

Kern-APIs:

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

Die Logik der Black- und White-List-Verifizierung:

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

Schauen Sie sich die Methode noch einmal an 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.SystemSlot

SystemSlot ist eine Regelprüfung zum Systemschutz:
Fügen Sie hier eine Bildbeschreibung ein

Kern-APIs:

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

Schauen Sie sich den folgenden SystemRuleManager.checkSystem(resourceWrapper);Code an:

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 ist die aktuelle Grenze der Hotspot-Parameter, wie in der Abbildung dargestellt:

Fügen Sie hier eine Bildbeschreibung ein

Es handelt sich um eine flussbegrenzende Methode zum Zählen von QPS für verschiedene Anforderungsparameterwerte für eingehende Ressourcenanforderungen.

  • Der eigenständige Schwellenwert ist hier die maximale Anzahl an Token: maxCount

  • Die statistische Fensterdauer ist hier die statistische Dauer: Dauer

Das bedeutet, dass maxCount-Token höchstens jede Dauer erzeugt werden und die Konfiguration in der obigen Abbildung bedeutet, dass alle 1 Sekunde 2 Token erzeugt werden.

Kern-APIs:

@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. Token-Bucket

Bei der Beurteilung der Hotspot-Regel wird der Token-Bucket-Algorithmus verwendet, um die aktuelle Parametergrenze zu erreichen, und der Token-Bucket wird für jeden unterschiedlichen Parameterwert festgelegt. Der Token-Bucket von Sentinel besteht aus zwei Teilen:
Fügen Sie hier eine Bildbeschreibung ein

Die Schlüssel dieser beiden Karten sind die angeforderten Parameterwerte, die Werte sind jedoch unterschiedlich. Darunter:

  • tokenCounters: Wird verwendet, um die Anzahl der verbleibenden Token aufzuzeichnen
  • timeCounters: Wird zum Aufzeichnen der Zeit der letzten Anfrage verwendet

Wenn eine Anfrage mit Parametern eintrifft, ist der grundlegende Beurteilungsprozess wie folgt:
Fügen Sie hier eine Bildbeschreibung ein

2.9.FlowSlot

FlowSlot ist für die Beurteilung der aktuellen Begrenzungsregeln verantwortlich, wie in der Abbildung dargestellt:
Fügen Sie hier eine Bildbeschreibung ein
einschließlich:

  • Drei Flusskontrollmodi: Direktmodus, Assoziationsmodus, Verbindungsmodus
  • Drei Flusskontrolleffekte: Fast Fail, Warm Up, Queue Waiting

Die drei Flusskontrollmodi sind aus Sicht der zugrunde liegenden Datenstatistik in zwei Kategorien unterteilt :

  • Führen Sie aktuelle Begrenzungsstatistiken für alle Anfragen (ClusterNode) durch, die Ressourcen eingeben: Direktmodus, Assoziationsmodus
  • Führen Sie aktuelle Begrenzungsstatistiken für einige Links durch (DefaultNode) und geben Sie Ressourcen ein: Linkmodus

Die drei Flusskontrolleffekte können aus Sicht des Strombegrenzungsalgorithmus in zwei Kategorien unterteilt werden :

  • Algorithmus mit gleitendem Zeitfenster: schnell ausfallen, aufwärmen
  • Leaky-Bucket-Algorithmus: Warten auf Wirkung

2.9.1. Kernprozess

Die Kern-APIs lauten wie folgt:

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

checkFlow-Methode:

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

Installierter FlowRuleChecker:

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

Die FlowRule ist hier die flussbegrenzende Regelschnittstelle, und mehrere darin enthaltene Mitgliedsvariablen entsprechen lediglich den Formularparametern:

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;
    // 。。。 略
}

Die Validierungslogik ist FlowRuleCheckerin canPassCheckder Methode definiert:

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

Geben Sie ein 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);
}

Die Beurteilung der Regeln hier muss zuerst FlowRule#getRater()den Durchflussregler ermitteln TrafficShapingControllerund dann die Durchflussbegrenzung vornehmen.

Und TrafficShapingControlleres gibt 3 Implementierungen:
Fügen Sie hier eine Bildbeschreibung ein

  • DefaultController: Fail Fast, die Standardmethode, basierend auf dem Sliding-Time-Window-Algorithmus
  • WarmUpController: Aufwärmmodus, basierend auf dem gleitenden Zeitfensteralgorithmus, aber der Schwellenwert ist dynamisch
  • RateLimiterController: Warteschlangenwartemodus, basierend auf dem Leaky-Bucket-Algorithmus

Die endgültige Beurteilung der Strombegrenzung erfolgt in der canPass-Methode von TrafficShapingController.

2.9.2. Gleitendes Zeitfenster

Die Funktion des gleitenden Zeitfensters gliedert sich in zwei Teile:

  • Eine davon ist die QPS-Zählfunktion des Zeitintervallfensters, die in StatisticSlot aufgerufen wird
  • Die zweite besteht darin, das Zeitintervallfenster QPS im Schiebefenster zu akkumulieren, das in FlowRule aufgerufen wird

Schauen wir uns zunächst die QPS-Zählfunktion des Zeitintervallfensters an.

2.9.2.1. Statistiken zum Zeitfenster-Anfragevolumen

Wenn Sie auf den Abschnitt „StatisticSlot“ in Kapitel 2.5 zurückblicken, gibt es einen solchen Code:
Fügen Sie hier eine Bildbeschreibung ein

Es zählt die QPS, die durch den Knoten laufen. Schauen wir mal nach, hier ist das Innere des DefaultNode:

Fügen Sie hier eine Bildbeschreibung ein

DefaultNodeEs wurde festgestellt, dass QPS-Statistiken auf und gleichzeitig erstellt werden ClusterNode. Wir wissen, dass es DefaultNodesich um Unterklassen ClusterNodehandelt StatisticNode. Aufrufende addPassRequest()Methoden werden hier irgendwann eintreten StatisticNode.

Folgen Sie einem:
Fügen Sie hier eine Bildbeschreibung ein

Es gibt zwei Breitengradstatistiken für Sekunden und Minuten, die zwei Zählern entsprechen. Suchen Sie die entsprechende Mitgliedsvariable. Sie können Folgendes sehen:

Fügen Sie hier eine Bildbeschreibung ein

Beide Zähler sind vom Typ ArrayMetric und werden in zwei Parametern übergeben:

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

Wie im Bild gezeigt:
Fügen Sie hier eine Bildbeschreibung ein

Als nächstes geben wir die Methoden ArrayMetricder Klasse ein addPass:

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

Woher weiß der Zähler also, in welchem ​​Fenster er sich gerade befindet?

Die Daten hier sind ein LeapArray:
Fügen Sie hier eine Bildbeschreibung ein

Vier Eigenschaften von 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 ist ein kreisförmiges Array. Da die Zeit unendlich ist, kann die Länge des Arrays nicht unendlich sein. Daher wird jedes Gitter im Array in einem Zeitfenster (Fenster) platziert. Wenn das Array voll ist, kehrt die Eckmarkierung auf 0 zurück. das Originalfenster abdecken.

Fügen Sie hier eine Bildbeschreibung ein

Da das Schiebefenster höchstens in kleine Fenster von SampleCount unterteilt ist, werden die beiden kleinen Fenster im nächstgelegenen Schiebefenster niemals abgedeckt, solange die Array-Länge größer als SampleCount ist, sodass kein Grund zur Sorge besteht der Abdeckung alter Daten.

So verfolgen wir 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));
        }
    }
}

Nachdem Sie das Fenster (WindowWrap) gefunden haben, in dem sich die aktuelle Zeit befindet, rufen Sie einfach die Add-Methode im WindowWrap-Objekt auf, und der Zähler beträgt +1.

Es ist nur für die Zählung des Anforderungsvolumens jedes Fensters verantwortlich, nicht für das Abfangen. Das Abfangen der Strombegrenzung hängt von der Logik in FlowSlot ab.

2.9.2.2. QPS-Berechnung mit gleitendem Fenster

Wie wir in Abschnitt 2.9.1 erwähnt haben, wird die aktuelle Grenzwertbeurteilung von FlowSlot letztendlich durch die Methoden TrafficShapingControllerin der Schnittstelle implementiert. canPassDiese Schnittstelle verfügt über drei Implementierungsklassen:

  • DefaultController: Fail Fast, die Standardmethode, basierend auf dem Sliding-Time-Window-Algorithmus
  • WarmUpController: Aufwärmmodus, basierend auf dem gleitenden Zeitfensteralgorithmus, aber der Schwellenwert ist dynamisch
  • RateLimiterController: Warteschlangenwartemodus, basierend auf dem Leaky-Bucket-Algorithmus

Daher folgen wir der canPass-Methode im Standard-DefaultController zur Analyse:

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

Daher der Schlüssel zur Beurteilungint 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());
}

Da wir die Strombegrenzung verwenden, folgen Sie node.passQps()der Logik:

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

rollingCounterInSecond.pass()Wie erhalten Sie also den angeforderten Betrag?

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

Mal sehen, data.values()wie man alle kleinen Fenster in den Bereich des Schiebefensters bringt:

// 此处进入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;
}

Wie lässt sich also isWindowDeprecated(timeMillis, windowWrap)beurteilen, ob das Fenster die Anforderungen erfüllt?

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

2.9.3. Undichte Schaufel

Wie wir im vorherigen Abschnitt erwähnt haben, wird die aktuelle Grenzwertbeurteilung von FlowSlot letztendlich durch die Methoden TrafficShapingControllerin der Schnittstelle implementiert. canPassDiese Schnittstelle verfügt über drei Implementierungsklassen:

  • DefaultController: Fail Fast, die Standardmethode, basierend auf dem Sliding-Time-Window-Algorithmus
  • WarmUpController: Aufwärmmodus, basierend auf dem gleitenden Zeitfensteralgorithmus, aber der Schwellenwert ist dynamisch
  • RateLimiterController: Warteschlangenwartemodus, basierend auf dem Leaky-Bucket-Algorithmus

Daher folgen wir der canPass-Methode im Standard-RateLimiterController zur Analyse:

@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 ist im Grunde dasselbe wie der Leaky-Bucket-Algorithmus, den wir zuvor analysiert haben:

Fügen Sie hier eine Bildbeschreibung ein

2.10.DegradeSlot

Die letzte Hürde besteht darin, die Downgrade-Regeln zu beurteilen.

Die Degradierung von Sentinel wird basierend auf einer Zustandsmaschine implementiert:

Fügen Sie hier eine Bildbeschreibung ein

Die entsprechende Implementierung befindet sich in der DegradeSlot-Klasse, der Kern-API:

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

Geben Sie weiterhin performCheckingdie Methode ein:

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.Leistungsschalter

Wir geben die tryPass-Methode von CircuitBreaker ein:

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

Die Beurteilung des Zeitfensters erfolgt in retryTimeoutArrived()der Methode:

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

OPEN auf HALF_OPEN umschalten in fromOpenToHalfOpen(context)der Methode:

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

Es gibt Änderungen von OPEN zu HALF_OPEN und von HALF_OPEN zu OPEN, aber es gibt noch ein paar Änderungen:

  • Von GESCHLOSSEN nach OFFEN
  • Von HALB_ÖFFNEN bis GESCHLOSSEN

2.10.2. Auslösen des Leistungsschalters

Nachdem die Anforderung alle Slots durchlaufen hat, muss die Exit-Methode ausgeführt werden, und in der Exit-Methode von DegradeSlot:

Fügen Sie hier eine Bildbeschreibung ein

Die onRequestComplete-Methode von CircuitBreaker wird aufgerufen. Und CircuitBreaker hat zwei Implementierungen:
Fügen Sie hier eine Bildbeschreibung ein

Nehmen wir als Beispiel die Sicherung mit abnormalem Verhältnis, um zu sehen, wie man Folgendes ExceptionCircuitBreakereingibt 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);
}

Schauen wir uns die Methode der Schwellenwertbeurteilung an:

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);
    }
}
```java
(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);
}

Schauen wir uns die Methode der Schwellenwertbeurteilung an:

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/yang2330648064/article/details/130444090
Recomendado
Clasificación