Sentinel source code entry method analysis

Write in front

In the previous chapter, we have analyzed how Sentinel makes SentinelResource annotation effective. One question is reserved. Each method annotated by SentinelResource will first call the following code in the surround notification

entry = SphU.entry(resourceName, resourceType, entryType, pjp.getArgs())

, this article continues to analyze the entry method. Before analyzing this method, we must first understand these concepts

Resource

resource is the most important concept in sentinel. Sentinel protects specific business code or other rear services through resources.

Sentinel shields the complex logic. Users only need to define a resource for the protected code or service, then define rules, and leave the rest to sentinel. Moreover, resources and rules are decoupled, and rules can even be modified dynamically at run time. After defining resources, you can protect your own services by embedding points in the program. There are two ways to embed points

  1. Try catch mode (through SphU.entry(...)), execute exception handling (or fallback) when BlockException is caught

  2. If else mode (through SphO.entry(...)), execute exception handling (or fallback) when false is returned

The above two methods define resources in the form of hard coding, and then embed resources. The intrusion into business code is too large. Since version 0.1.1, sentinel has added annotation support. Resources can be defined through annotations. The specific annotation is SentinelResource. In addition to defining resources through annotations, you can also specify blockHandler and fallback methods.

Rule

The rules set around the real-time state of resources can include flow control rules, fuse degradation rules and system protection rules. All rules can be adjusted dynamically and in real time.

Context

Context is the context environment for Resource operation. Each Resource operation (entry / exit for Resource) must belong to a context. If the context is not specified in the program, a default context with the name of "sentinel_default_context" will be created. There may be multiple Resource operations in a context life cycle. When the last Resource in the context life cycle exits, the context will be cleaned up, which also indicates the end of the whole context life cycle. The main properties of context are as follows:

    /**
     * Context name.
     * context Name, default name "sentinel_default_context"
     */
    private final String name;

    /**
     * The entrance node of current invocation tree.
     * context Entry node. Each context must have an entry node
     */
    private DefaultNode entranceNode;

    /**
     * Current processing entry.
     * context There may be multiple entries in the current Context lifecycle, so curEntry will change
     */
    private Entry curEntry;

    /**
     * The origin of this context (usually indicate different invokers, e.g. service consumer name or origin IP).
     */
    private String origin = "";

    private final boolean async;

If you want to call sphu Entry () or spho Before entry(), customize a context through contextutil Enter () method. When the entry executes the exit method, if the parent node of the entry is null, it indicates that it is the outermost entry in the current context. At this time, the context in ThreadLocal will be cleared.

Entry

I saw the appearance of entry in the Context just now. Now let's talk about entry. Each execution of sphu Entry () or spho Entry () will return an entry, which represents a resource operation, and the current invocation information will be saved internally. Multiple resource operations in a Context life cycle correspond to multiple entries. These entries form a parent/child structure and are saved in the entry instance. The entry class CtEntry structure is as follows:

CtEntry(ResourceWrapper resourceWrapper, ProcessorSlot<Object> chain, Context context) {
        super(resourceWrapper);
        this.chain = chain;
        this.context = context;

        setUpEntryFor(context);
    }
public Entry(ResourceWrapper resourceWrapper) {
        this.resourceWrapper = resourceWrapper;
        this.createTimestamp = TimeUtil.currentTimeMillis();
    }

private void setUpEntryFor(Context context) {
        // The entry should not be associated to NullContext.
        if (context instanceof NullContext) {
            return;
        }
        this.parent = context.getCurEntry();
        if (parent != null) {
            ((CtEntry) parent).child = this;
        }
        context.setCurEntry(this);
    }

That is, the returned entry will be the child node of the current entry

DefaultNode

Continue to look at the structure of Entry

public abstract class Entry implements AutoCloseable {

    private static final Object[] OBJECTS0 = new Object[0];

    private final long createTimestamp;
    private long completeTimestamp;

    private Node curNode;
    /**
     * {@link Node} of the specific origin, Usually the origin is the Service Consumer.
     */
    private Node originNode;

    private Throwable error;
    private BlockException blockError;

    protected final ResourceWrapper resourceWrapper;
    }

Node appears in the example code. What is this

  1. Node implements the class DefaultNode by default, which also has a subclass EntranceNode;
    context has an entranceNode attribute and Entry has a curNode attribute.

  2. EntranceNode: the creation of this class is completed when initializing the context (ContextUtil.trueEnter method). Note that this class is for the context dimension, that is, a context has and has only one EntranceNode.

  3. DefaultNode: this class is created in NodeSelectorSlot Entry is completed when there is no context The DefaultNode corresponding to name will be created (New DefaultNode (resourcewrapper, null, corresponding to resource) and saved to the local cache (private volatile map < string, DefaultNode > map in NodeSelectorSlot); Get context The DefaultNode corresponding to name will be set to the curentry of the current context Curnode attribute, that is, in NodeSelectorSlot, there is a context with only one DefaultNode.

Why does a context have only one DefaultNode? Where is our resouece?

In fact, there is only one DefaultNode in a context here, which is within the scope of NodeSelectorSlot. NodeSelectorSlot is a link in ProcessorSlotChain, and the acquisition of ProcessorSlotChain is based on the Resource dimension.

To sum up, for the same Resource, multiple contexts correspond to multiple defaultnodes; For different resources (whether it is the same context or not), it corresponds to multiple different defaultnodes.

The DefaultNode structure is as follows:

    /**
     * The resource associated with the node.
     */
    private ResourceWrapper id;

    /**
     * The list of all child nodes.
     */
    private volatile Set<Node> childList = new HashSet<>();

    /**
     * Associated cluster node.
     */
    private ClusterNode clusterNode;

A Resouce has only one clusterNode, and multiple defaultnodes correspond to one clusterNode. If defaultNode If clusterNode is null, it is in clusterbuilderslot The entry will be initialized.

StatisticNode
The StatisticNode stores the real-time statistical data of resources (based on the sliding time window mechanism). Through these statistical data, sentinel can carry out a series of operations such as current limiting and degradation, The StatisticNode properties are as follows:

    /**
     * Holds statistics of the recent {@code INTERVAL} seconds. The {@code INTERVAL} is divided into time spans
     * by given {@code sampleCount}.
     * Second level sliding time window (time window unit: 500ms)
     */
    private transient volatile Metric rollingCounterInSecond = new ArrayMetric(SampleCountProperty.SAMPLE_COUNT,
        IntervalProperty.INTERVAL);

    /**
     * Holds statistics of the recent 60 seconds. The windowLengthInMs is deliberately set to 1000 milliseconds,
     * meaning each bucket per second, in this way we can get accurate statistics of each second.
     * Sliding time window of minute level (time window unit: 1s)
     */
    private transient Metric rollingCounterInMinute = new ArrayMetric(60, 60 * 1000, false);

    /**
     * The counter for thread count.
     */
    private LongAdder curThreadNum = new LongAdder();

    /**
     * The last timestamp when metrics were fetched.
     */
    private long lastFetchTime = -1;

A separate article will be written for the detailed introduction

Slot

Slot is another very important concept in sentinel. The workflow of sentinel is developed around the slot chain composed of slots. It should be noted that each slot has its own responsibilities. They perform their respective duties, cooperate well, and achieve the final purpose of current limiting and degradation through a certain arrangement sequence. By default, the order between slots is fixed, because some slots need to rely on the results calculated by other slots to work.

However, this does not mean that we can only follow the definition of the framework. Sentinel uses SlotChainBuilder as the SPI interface to make Slot Chain have the ability to expand. By implementing the SlotsChainBuilder interface, we can add custom slots and customize the order of each slot, so that we can add custom functions to sentinel.

So far, after introducing the core concepts, let's continue with the source code

Source code

Entry entry method

Direct follow-up code

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

Called rnv shp

  1. Name – the unique name of the protected resource
  2. resourceType – classification of resources (e.g. Web or RPC)
  3. trafficType – traffic type (inbound, outbound, or internal). This is used to mark whether the system can be blocked when it is unstable. Only inbound traffic can be blocked by SystemRule
  4. batchCount - traffic
  5. Args – args is used for parameter flow control or custom slot
public Entry entryWithType(String name, int resourceType, EntryType entryType, int count, boolean prioritized,
                               Object[] args) throws BlockException {
        StringResourceWrapper resource = new StringResourceWrapper(name, entryType, resourceType);
        return entryWithPriority(resource, count, prioritized, args);
    }

Finally, the entryWithType method is called

private Entry entryWithPriority(ResourceWrapper resourceWrapper, int count, boolean prioritized, Object... args)
        throws BlockException {
        Context context = ContextUtil.getContext();
        if (context instanceof NullContext) {
            // The {@link NullContext} indicates that the amount of context has exceeded the threshold,
            // so here init the entry only. No rule checking will be done.
            return new CtEntry(resourceWrapper, null, context);
        }

        if (context == null) {
            // Using default context.
            context = InternalContextUtil.internalEnter(Constants.CONTEXT_DEFAULT_NAME);
        }

        // Global switch is close, no rule checking will do.
        if (!Constants.ON) {
            return new CtEntry(resourceWrapper, null, context);
        }

        ProcessorSlot<Object> chain = lookProcessChain(resourceWrapper);

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

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

This method can be said to cover the core logic of the whole Sentinel

  1. Get the context. NullContext indicates that the number of contexts has exceeded the threshold, so only the entries are initialized here. No rule checks will be performed.
  2. If the context is null, the default context will be used. As can be seen from the following code, the called trueRnter method, in which the resource parameter is' ', will continue to be analyzed below
private final static class InternalContextUtil extends ContextUtil {
        static Context internalEnter(String name) {
            return trueEnter(name, "");
        }

        static Context internalEnter(String name, String origin) {
            return trueEnter(name, origin);
        }
    }
  1. If the global switch is turned off, no rule checks are performed. return new CtEntry(resourceWrapper, null, context);
  2. Check all process chains by calling the lookProcessChain method. The slot chain is related to resources, and the most critical logic of Sentinel is also in each slot. This method is analyzed separately below and is more important
  3. If the process chain obtained in the previous step is null, it means that the amount of resources (slot chain) exceeds constants MAX_ SLOT_ CHAIN_ Size, so rule checking will not be performed, and return new CtEntry(resourceWrapper, null, context) will be returned directly;
  4. Build a complete CtEntry with three parameters and call the slot chain obtained in step 4

trueEnter

protected static Context trueEnter(String name, String origin) {
        Context context = contextHolder.get();
        if (context == null) {
            Map<String, DefaultNode> localCacheNameMap = contextNameNodeMap;
            DefaultNode node = localCacheNameMap.get(name);
            if (node == null) {
                if (localCacheNameMap.size() > Constants.MAX_CONTEXT_NAME_SIZE) {
                    setNullContext();
                    return NULL_CONTEXT;
                } else {
                    LOCK.lock();
                    try {
                        node = contextNameNodeMap.get(name);
                        if (node == null) {
                            if (contextNameNodeMap.size() > Constants.MAX_CONTEXT_NAME_SIZE) {
                                setNullContext();
                                return NULL_CONTEXT;
                            } else {
                                node = new EntranceNode(new StringResourceWrapper(name, EntryType.IN), null);
                                // Add entrance node.
                                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 = new Context(node, name);
            context.setOrigin(origin);
            contextHolder.set(context);
        }

        return context;
    }
  1. First try to get from ThreadLocal, and then return directly
  2. If the first step is not obtained, try to obtain the entry node corresponding to the context name from the cache
  3. Judge whether the number of entry nodes in the cache is greater than 2000public final static int MAX_CONTEXT_NAME_SIZE = 2000; If it is greater than 2000, a null is returned_ CONTEXT
  4. The above checks ensure thread safety by generating an entry node according to the context name, during which pun retrieval will be carried out
  5. Add to the global root node and cache. Note that each ContextName corresponds to an entry node, entranceNode
  6. Initialize the context object according to ContextName and entranceNode, and set the context object to the current thread

lookProcessChain

You can see that the slot chain is resource latitude

ProcessorSlot<Object> lookProcessChain(ResourceWrapper resourceWrapper) {
        ProcessorSlotChain chain = chainMap.get(resourceWrapper);
        if (chain == null) {
            synchronized (LOCK) {
                chain = chainMap.get(resourceWrapper);
                if (chain == null) {
                    // Entry size limit.
                    if (chainMap.size() >= Constants.MAX_SLOT_CHAIN_SIZE) {
                        return null;
                    }

                    chain = SlotChainProvider.newSlotChain();
                    Map<ResourceWrapper, ProcessorSlotChain> newMap = new HashMap<ResourceWrapper, ProcessorSlotChain>(
                        chainMap.size() + 1);
                    newMap.putAll(chainMap);
                    newMap.put(resourceWrapper, chain);
                    chainMap = newMap;
                }
            }
        }
        return chain;
    }
  1. Get from cache
  2. If there is no cache, the pun is retrieved and the number of slot chains is checked here
  3. Initialize slot chain

SlotChainProvider.newSlotChain()

public static ProcessorSlotChain newSlotChain() {
        if (slotChainBuilder != null) {
            return slotChainBuilder.build();
        }

        // Resolve the slot chain builder SPI.
        slotChainBuilder = SpiLoader.loadFirstInstanceOrDefault(SlotChainBuilder.class, DefaultSlotChainBuilder.class);

        if (slotChainBuilder == null) {
            // Should not go through here.
            RecordLog.warn("[SlotChainProvider] Wrong state when resolving slot chain builder, using default");
            slotChainBuilder = new DefaultSlotChainBuilder();
        } else {
            RecordLog.info("[SlotChainProvider] Global slot chain builder resolved: "
                + slotChainBuilder.getClass().getCanonicalName());
        }
        return slotChainBuilder.build();
    }

If it has been initialized, directly return if (slotchainbuilder! = null) {return slotchainbuilder. Build();}
The main method is to initialize through SPI mechanism

SpiLoader.loadFirstInstanceOrDefault(SlotChainBuilder.class, DefaultSlotChainBuilder.class);

View this method

public static <T> T loadFirstInstanceOrDefault(Class<T> clazz, Class<? extends T> defaultClass) {
        AssertUtil.notNull(clazz, "SPI class cannot be null");
        AssertUtil.notNull(defaultClass, "default SPI class cannot be null");
        try {
            String key = clazz.getName();
            // Not thread-safe, as it's expected to be resolved in a thread-safe context.
            ServiceLoader<T> serviceLoader = SERVICE_LOADER_MAP.get(key);
            if (serviceLoader == null) {
                serviceLoader = ServiceLoaderUtil.getServiceLoader(clazz);
                SERVICE_LOADER_MAP.put(key, serviceLoader);
            }
			//Load the first specific SPI instance found (excluding the default SPI class provided)
            for (T instance : serviceLoader) {
                if (instance.getClass() != defaultClass) {
                    return instance;
                }
            }
            //If no other SPI implementation is found, a default SPI instance is created
            return defaultClass.newInstance();
        } catch (Throwable t) {
            RecordLog.error("[SpiLoader] ERROR: loadFirstInstanceOrDefault failed", t);
            t.printStackTrace();
            return null;
        }
    }

Load the first specific SPI instance found (excluding the default SPI class provided). If no other SPI implementation is found, a default SPI instance is created.

Finally, take a look at the default SPI instance

 public ProcessorSlotChain build() {
        ProcessorSlotChain chain = new DefaultProcessorSlotChain();

        // Note: the instances of ProcessorSlot should be different, since they are not stateless.

        List<ProcessorSlot> sortedSlotList = SpiLoader.loadPrototypeInstanceListSorted(ProcessorSlot.class);
        for (ProcessorSlot slot : sortedSlotList) {
            if (!(slot instanceof AbstractLinkedProcessorSlot)) {
                RecordLog.warn("The ProcessorSlot(" + slot.getClass().getCanonicalName() + ") is not an instance of AbstractLinkedProcessorSlot, can't be added into ProcessorSlotChain");
                continue;
            }

            chain.addLast((AbstractLinkedProcessorSlot<?>) slot);
        }

        return chain;
    }

The method is simple: load the sorting of the provided SPI interface and the list of prototype SPI instances.
Note: each call returns a different instance, the prototype instance, rather than a singleton instance.

summary

  1. Gets the context object. If the context object has not been initialized, it will be initialized with the default name.
  2. Judge global switch
  3. The slot chain is generated according to the given resources. The slot chain is related to resources, and the most critical logic of Sentinel is also in each slot.
  4. Call slot chain

So far, we have analyzed the entry method and know the core process of Sentinel. In the next article, let's see what the generated slot chain does

Keywords: Microservices source code sentinel

Added by Drakla on Sun, 13 Feb 2022 15:42:45 +0200