Cluster layer for in-depth analysis

Posted by sunshine66 on Thu, 16 Dec 2021 11:21:22 +0100

Relationship of each node:

The Invoker here is an abstraction of the callable Service of the Provider. The Invoker encapsulates the Provider address and Service interface information;

Directory represents multiple invokers and can be regarded as a List. However, unlike List, its value may change dynamically, such as pushing changes by the registry;
The Cluster disguises multiple invokers in the Directory as one Invoker, which is transparent to the upper layer. The camouflage process includes fault-tolerant logic. After the call fails, try another one again;
The Router is responsible for selecting subsets from multiple invokers according to routing rules, such as read-write separation, application isolation, etc;
LoadBalance is responsible for selecting a specific one from multiple invokers for this call. The selection process includes the load balancing algorithm. After the call fails, it needs to be re selected;

The Cluster obtains an available Invoker through directory, routing and load balancing and gives it to the upper layer for calling. The interface is as follows:

@SPI(FailoverCluster.NAME)
public interface Cluster {

    /**
     * Merge the directory invokers to a virtual invoker.
     *
     * @param <T>
     * @param directory
     * @return cluster invoker
     * @throws RpcException
     */
    @Adaptive
    <T> Invoker<T> join(Directory<T> directory) throws RpcException;

}

Cluster is a cluster fault-tolerant interface. Invoker s obtained after routing and load balancing are handled by fault-tolerant mechanisms. dubbo provides a variety of fault-tolerant mechanisms, including:

Failover Cluster: fail to switch automatically. In case of failure, retry other servers [1]. It is usually used for read operations, but retries cause longer delays. You can set the number of retries (excluding the first time) by retries = "2".

Failfast Cluster: fast failure, only one call is initiated, and an error is reported immediately after failure. It is usually used for non idempotent write operations, such as adding new records.

Failsafe Cluster: fail safe. When an exception occurs, it is ignored directly. It is usually used for operations such as writing audit logs.

Failback Cluster: automatic recovery in case of failure, background recording of failed requests, and regular retransmission. Typically used for message notification operations.

Forking Cluster: multiple servers are called in parallel. As long as one is successful, it will be returned. It is usually used for read operations with high real-time requirements, but it needs to waste more service resources. You can set the maximum number of parallels by forks = "2".

Broadcast Cluster: broadcast calls all providers one by one. If any one reports an error, it will report an error [2]. It is usually used to notify all providers to update local resource information such as cache or log.

FailoverCluster is used by default. When it fails, it will retry other servers by default, which is twice by default; Of course, other fault-tolerant mechanisms can also be extended; Take a look at the default FailoverCluster fault tolerance mechanism. The specific source code is in FailoverClusterInvoker:

public Result doInvoke(Invocation invocation, final List<Invoker<T>> invokers, LoadBalance loadbalance) throws RpcException {
        List<Invoker<T>> copyinvokers = invokers;
        checkInvokers(copyinvokers, invocation);
        int len = getUrl().getMethodParameter(invocation.getMethodName(), Constants.RETRIES_KEY, Constants.DEFAULT_RETRIES) + 1;
        if (len <= 0) {
            len = 1;
        }
        // retry loop.
        RpcException le = null; // last exception.
        List<Invoker<T>> invoked = new ArrayList<Invoker<T>>(copyinvokers.size()); // invoked invokers.
        Set<String> providers = new HashSet<String>(len);
        for (int i = 0; i < len; i++) {
            //Reselect before retry to avoid a change of candidate `invokers`.
            //NOTE: if `invokers` changed, then `invoked` also lose accuracy.
            if (i > 0) {
                checkWhetherDestroyed();
                copyinvokers = list(invocation);
                // check again
                checkInvokers(copyinvokers, invocation);
            }
            Invoker<T> invoker = select(loadbalance, invocation, copyinvokers, invoked);
            invoked.add(invoker);
            RpcContext.getContext().setInvokers((List) invoked);
            try {
                Result result = invoker.invoke(invocation);
                if (le != null && logger.isWarnEnabled()) {
                    logger.warn("Although retry the method " + invocation.getMethodName()
                            + " in the service " + getInterface().getName()
                            + " was successful by the provider " + invoker.getUrl().getAddress()
                            + ", but there have been failed providers " + providers
                            + " (" + providers.size() + "/" + copyinvokers.size()
                            + ") from the registry " + directory.getUrl().getAddress()
                            + " on the consumer " + NetUtils.getLocalHost()
                            + " using the dubbo version " + Version.getVersion() + ". Last error is: "
                            + le.getMessage(), le);
                }
                return result;
            } catch (RpcException e) {
                if (e.isBiz()) { // biz exception.
                    throw e;
                }
                le = e;
            } catch (Throwable e) {
                le = new RpcException(e.getMessage(), e);
            } finally {
                providers.add(invoker.getUrl().getAddress());
            }
        }
        throw new RpcException(le != null ? le.getCode() : 0, "Failed to invoke the method "
                + invocation.getMethodName() + " in the service " + getInterface().getName()
                + ". Tried " + len + " times of the providers " + providers
                + " (" + providers.size() + "/" + copyinvokers.size()
                + ") from the registry " + directory.getUrl().getAddress()
                + " on the consumer " + NetUtils.getLocalHost() + " using the dubbo version "
                + Version.getVersion() + ". Last error is: "
                + (le != null ? le.getMessage() : ""), le != null && le.getCause() != null ? le.getCause() : le);
    }

Invocation refers to the relevant parameters transmitted from the client to the server, including (method name, method parameters, parameter value and attachment information), invokers refers to the list of servers after routing, and loadbalance refers to the specified load balancing policy; First, check whether the invokers are empty. If they are empty, throw an exception directly, and then get the number of retries, which is 2 by default. The next step is to call the specified number of times in a loop. If it is not the first call (indicating that the first call fails), the server list will be reloaded, and then get the unique Invoker through the load balancing policy. Finally, send the invocation to the server through the Invoker, Return Result;

The specific doInvoke method is called in the abstract class AbstractClusterInvoker:

public Result invoke(final Invocation invocation) throws RpcException {
        checkWhetherDestroyed();
        LoadBalance loadbalance = null;
        List<Invoker<T>> invokers = list(invocation);
        if (invokers != null && !invokers.isEmpty()) {
            loadbalance = ExtensionLoader.getExtensionLoader(LoadBalance.class).getExtension(invokers.get(0).getUrl()
                    .getMethodParameter(RpcUtils.getMethodName(invocation), Constants.LOADBALANCE_KEY, Constants.DEFAULT_LOADBALANCE));
        }
        RpcUtils.attachInvocationIdIfAsync(getUrl(), invocation);
        return doInvoke(invocation, invokers, loadbalance);
    }
	
	 protected List<Invoker<T>> list(Invocation invocation) throws RpcException {
        List<Invoker<T>> invokers = directory.list(invocation);
        return invokers;
    }

First, the Invoker list is obtained through Directory, and routing processing is also done in Directory, and then the load balancing strategy is obtained. Finally, the specific fault tolerance strategy is invoked. Let's take a look at directory;

Directory Interface

The interface is defined as follows:

public interface Directory<T> extends Node {

    /**
     * get service type.
     *
     * @return service type.
     */
    Class<T> getInterface();

    /**
     * list invokers.
     *
     * @return invokers
     */
    List<Invoker<T>> list(Invocation invocation) throws RpcException;

}

The function of directory service is to obtain the service list of the specified interface. There are two specific implementations: StaticDirectory and RegistryDirectory, both of which inherit from AbstractDirectory; From the name, we can roughly know that StaticDirectory is a fixed directory service, which means that the Invoker list will not change dynamically; RegistryDirectory is a dynamic directory service, which dynamically updates the service list through the registry; List is implemented in an abstract class:

public List<Invoker<T>> list(Invocation invocation) throws RpcException {
        if (destroyed) {
            throw new RpcException("Directory already destroyed .url: " + getUrl());
        }
        List<Invoker<T>> invokers = doList(invocation);
        List<Router> localRouters = this.routers; // local reference
        if (localRouters != null && !localRouters.isEmpty()) {
            for (Router router : localRouters) {
                try {
                    if (router.getUrl() == null || router.getUrl().getParameter(Constants.RUNTIME_KEY, false)) {
                        invokers = router.route(invokers, getConsumerUrl(), invocation);
                    }
                } catch (Throwable t) {
                    logger.error("Failed to execute router: " + getUrl() + ", cause: " + t.getMessage(), t);
                }
            }
        }
        return invokers;
    }

First, check whether the directory is destroyed, then call doList, define it in the implementation class, and finally call the routing function. Next, let's focus on the doList method in StaticDirectory and RegistryDirectory.

1.RegistryDirectory

It is a dynamic directory service. All visible RegistryDirectory also inherits the NotifyListener interface. It is a notification interface. When the service list is updated in the registry, it will notify RegistryDirectory at the same time. The notification logic is as follows:

public synchronized void notify(List<URL> urls) {
        List<URL> invokerUrls = new ArrayList<URL>();
        List<URL> routerUrls = new ArrayList<URL>();
        List<URL> configuratorUrls = new ArrayList<URL>();
        for (URL url : urls) {
            String protocol = url.getProtocol();
            String category = url.getParameter(Constants.CATEGORY_KEY, Constants.DEFAULT_CATEGORY);
            if (Constants.ROUTERS_CATEGORY.equals(category)
                    || Constants.ROUTE_PROTOCOL.equals(protocol)) {
                routerUrls.add(url);
            } else if (Constants.CONFIGURATORS_CATEGORY.equals(category)
                    || Constants.OVERRIDE_PROTOCOL.equals(protocol)) {
                configuratorUrls.add(url);
            } else if (Constants.PROVIDERS_CATEGORY.equals(category)) {
                invokerUrls.add(url);
            } else {
                logger.warn("Unsupported category " + category + " in notified url: " + url + " from registry " + getUrl().getAddress() + " to consumer " + NetUtils.getLocalHost());
            }
        }
        // configurators
        if (configuratorUrls != null && !configuratorUrls.isEmpty()) {
            this.configurators = toConfigurators(configuratorUrls);
        }
        // routers
        if (routerUrls != null && !routerUrls.isEmpty()) {
            List<Router> routers = toRouters(routerUrls);
            if (routers != null) { // null - do nothing
                setRouters(routers);
            }
        }
        List<Configurator> localConfigurators = this.configurators; // local reference
        // merge override parameters
        this.overrideDirectoryUrl = directoryUrl;
        if (localConfigurators != null && !localConfigurators.isEmpty()) {
            for (Configurator configurator : localConfigurators) {
                this.overrideDirectoryUrl = configurator.configure(overrideDirectoryUrl);
            }
        }
        // providers
        refreshInvoker(invokerUrls);
    }

This notification interface will accept three types of URLs: router, configurator and provider;

Routing rules: determine the target server of a dubbo service call, which is divided into conditional routing rules and script routing rules, and supports extensibility. The operation of writing routing rules to the registry is usually completed by the page of the monitoring center or governance center;

Configuration rule: write dynamic configuration override rule [1] to the registry. This function is usually completed by the page of the monitoring center or governance center;

provider: list of dynamically provided services

The routing rules and configuration rules are actually used to update and filter the provider service list. The refreshInvoker method is to refresh the local invoker list according to three url categories. Let's take a look at the doList interface implemented by RegistryDirectory:

public List<Invoker<T>> doList(Invocation invocation) {
        if (forbidden) {
            // 1. No service provider 2. Service providers are disabled
            throw new RpcException(RpcException.FORBIDDEN_EXCEPTION,
                "No provider available from registry " + getUrl().getAddress() + " for service " + getConsumerUrl().getServiceKey() + " on consumer " +  NetUtils.getLocalHost()
                        + " use dubbo version " + Version.getVersion() + ", please check status of providers(disabled, not registered or in blacklist).");
        }
        List<Invoker<T>> invokers = null;
        Map<String, List<Invoker<T>>> localMethodInvokerMap = this.methodInvokerMap; // local reference
        if (localMethodInvokerMap != null && localMethodInvokerMap.size() > 0) {
            String methodName = RpcUtils.getMethodName(invocation);
            Object[] args = RpcUtils.getArguments(invocation);
            if (args != null && args.length > 0 && args[0] != null
                    && (args[0] instanceof String || args[0].getClass().isEnum())) {
                invokers = localMethodInvokerMap.get(methodName + "." + args[0]); // The routing can be enumerated according to the first parameter
            }
            if (invokers == null) {
                invokers = localMethodInvokerMap.get(methodName);
            }
            if (invokers == null) {
                invokers = localMethodInvokerMap.get(Constants.ANY_VALUE);
            }
            if (invokers == null) {
                Iterator<List<Invoker<T>>> iterator = localMethodInvokerMap.values().iterator();
                if (iterator.hasNext()) {
                    invokers = iterator.next();
                }
            }
        }
        return invokers == null ? new ArrayList<Invoker<T>>(0) : invokers;
    }

After refreshInvoker processing, the service list has methodInvokerMap, and a method corresponds to the service list Map > >;
Obtain the corresponding service list through the method specified in the Invocation. If the specific method does not have a corresponding service list, obtain the service list corresponding to "*"; After processing, route processing is performed in the parent class. The routing rules are also obtained through the notification interface. The routing rules are introduced in the next chapter;

2.StaticDirectory

This is a static directory service. The service list already exists at the time of initialization and will not change; Static directory is rarely used. It is mainly used for the reference of services to multiple registries;

protected List<Invoker<T>> doList(Invocation invocation) throws RpcException {

        return invokers;
    }

Because it is static, all doList methods are also very simple. You can directly return the service list in memory;

Router interface

Routing rules determine the target server of a dubbo service call, which are divided into conditional routing rules and script routing rules, and support extensibility. The interfaces are as follows:

public interface Router extends Comparable<Router> {

    /**
     * get the router url.
     *
     * @return url
     */
    URL getUrl();

    /**
     * route.
     *
     * @param invokers
     * @param url        refer url
     * @param invocation
     * @return routed invokers
     * @throws RpcException
     */
    <T> List<Invoker<T>> route(List<Invoker<T>> invokers, URL url, Invocation invocation) throws RpcException;

}

The route method provided in the interface filters out a subset of invokers through certain rules; Three implementation classes are provided: ScriptRouter, ConditionRouter and MockInvokersSelector

ScriptRouter: script routing rules support all scripts of the JDK script engine, such as javascript, jruby, groovy, etc. set the script type through the type=javascript parameter, which defaults to JavaScript;

ConditionRouter: routing rules based on conditional expressions, such as host = 10.20 153.10 => host = 10.20. 153.11;=> The previous is the consumer matching condition, all parameters are compared with the consumer's URL, = > the later is the filter condition of the provider's address list, and all parameters are compared with the provider's URL;

MockInvokersSelector: whether it is configured to use mock. This router ensures that only callers with protocol mock appear in the final caller list, and all other callers will be excluded;

Let's focus on the source code of ScriptRouter

public ScriptRouter(URL url) {
        this.url = url;
        String type = url.getParameter(Constants.TYPE_KEY);
        this.priority = url.getParameter(Constants.PRIORITY_KEY, 0);
        String rule = url.getParameterAndDecoded(Constants.RULE_KEY);
        if (type == null || type.length() == 0) {
            type = Constants.DEFAULT_SCRIPT_TYPE_KEY;
        }
        if (rule == null || rule.length() == 0) {
            throw new IllegalStateException(new IllegalStateException("route rule can not be empty. rule:" + rule));
        }
        ScriptEngine engine = engines.get(type);
        if (engine == null) {
            engine = new ScriptEngineManager().getEngineByName(type);
            if (engine == null) {
                throw new IllegalStateException(new IllegalStateException("Unsupported route rule type: " + type + ", rule: " + rule));
            }
            engines.put(type, engine);
        }
        this.engine = engine;
        this.rule = rule;
    }

The constructor initializes the script engine and script code respectively. The default script engine is javascript; Look at a specific url: Here I recommend an architecture learning exchange skirt. Exchange learning skirt No.: 821169538, which will share some videos recorded by senior architects

"script://0.0.0.0/com.foo.BarService?category=routers&dynamic=false&rule=" + URL.encode("(function route(invokers) { ... } (invokers))")

The script protocol represents a script protocol, followed by a javascript script, and the passed in parameter is invokers;

(function route(invokers) {
    var result = new java.util.ArrayList(invokers.size());
    for (i = 0; i < invokers.size(); i ++) {
        if ("10.20.153.10".equals(invokers.get(i).getUrl().getHost())) {
            result.add(invokers.get(i));
        }
    }
    return result;
} (invokers)); // Indicates the immediate execution method

The above script filters out that the host is 10.20 153.10. How to execute this script? In the route method:

public <T> List<Invoker<T>> route(List<Invoker<T>> invokers, URL url, Invocation invocation) throws RpcException {
        try {
            List<Invoker<T>> invokersCopy = new ArrayList<Invoker<T>>(invokers);
            Compilable compilable = (Compilable) engine;
            Bindings bindings = engine.createBindings();
            bindings.put("invokers", invokersCopy);
            bindings.put("invocation", invocation);
            bindings.put("context", RpcContext.getContext());
            CompiledScript function = compilable.compile(rule);
            Object obj = function.eval(bindings);
            if (obj instanceof Invoker[]) {
                invokersCopy = Arrays.asList((Invoker<T>[]) obj);
            } else if (obj instanceof Object[]) {
                invokersCopy = new ArrayList<Invoker<T>>();
                for (Object inv : (Object[]) obj) {
                    invokersCopy.add((Invoker<T>) inv);
                }
            } else {
                invokersCopy = (List<Invoker<T>>) obj;
            }
            return invokersCopy;
        } catch (ScriptException e) {
            //fail then ignore rule .invokers.
            logger.error("route error , rule has been ignored. rule: " + rule + ", method:" + invocation.getMethodName() + ", url: " + RpcContext.getContext().getUrl(), e);
            return invokers;
        }
    }

First, compile the script through the script engine, then execute the script, and pass in the Bindings parameter, so that the invokers can be obtained in the script and then filtered; Finally, let's look at the load balancing strategy

LoadBalance interface

During cluster load balancing, Dubbo provides a variety of balancing strategies. random call is the default, and the load balancing strategy can be extended by itself; The interface classes are as follows:

@SPI(RandomLoadBalance.NAME)
public interface LoadBalance {

    /**
     * select one invoker in list.
     *
     * @param invokers   invokers.
     * @param url        refer url
     * @param invocation invocation.
     * @return selected invoker.
     */
    @Adaptive("loadbalance")
    <T> Invoker<T> select(List<Invoker<T>> invokers, URL url, Invocation invocation) throws RpcException;

}

SPI defines the default policy RandomLoadBalance and provides a select method to select an invoker from the service list through the policy; dubbo provides a variety of policies by default:

Random LoadBalance: random. Set the random probability according to the weight. The probability of collision on a section is high, but the larger the adjustment amount is, the more uniform the distribution is, and the weight is relatively uniform after using the probability, which is conducive to dynamically adjusting the provider's weight;

RoundRobin LoadBalance: polling, which sets the polling ratio according to the weight after the Convention; There is a problem that slow providers accumulate requests. For example, the second machine is very slow, but it doesn't hang up. When the request is transferred to the second machine, it gets stuck,

Over time, all requests were stuck on the second stage;

Leadactive loadbalance: the minimum number of active calls, the random number of the same active number, and the active number refers to the count difference before and after the call; Make the slow provider receive fewer requests, because the slower the provider, the greater the count difference before and after the call;

Consistent Hash loadbalance: consistent Hash. Requests with the same parameters are always sent to the same provider; When a provider hangs up, the requests originally sent to the provider are shared among other providers based on the virtual node, which will not cause drastic changes;

Let's focus on the default RandomLoadBalance source code

protected <T> Invoker<T> doSelect(List<Invoker<T>> invokers, URL url, Invocation invocation) {
        int length = invokers.size(); // Number of invokers
        int totalWeight = 0; // The sum of weights
        boolean sameWeight = true; // Every invoker has the same weight?
        for (int i = 0; i < length; i++) {
            int weight = getWeight(invokers.get(i), invocation);
            totalWeight += weight; // Sum
            if (sameWeight && i > 0
                    && weight != getWeight(invokers.get(i - 1), invocation)) {
                sameWeight = false;
            }
        }
        if (totalWeight > 0 && !sameWeight) {
            // If (not every invoker has the same weight & at least one invoker's weight>0), select randomly based on totalWeight.
            int offset = random.nextInt(totalWeight);
            // Return a invoker based on the random value.
            for (int i = 0; i < length; i++) {
                offset -= getWeight(invokers.get(i), invocation);
                if (offset < 0) {
                    return invokers.get(i);
                }
            }
        }
        // If all invokers have the same weight value or totalWeight=0, return evenly.
        return invokers.get(random.nextInt(length));
    }

First, calculate the total weight and check whether each service has the same weight; If the total weight is greater than 0 and the weights of services are different, it is selected randomly through the weight, otherwise it is selected randomly directly through the Random function;

There are some likes and collections that can help you. Pay attention to them

If you need more tutorials, just scan the code on wechat

                                                                                 

                                                                                         👆👆👆

Don't forget to scan the code to get the information [HD Java learning roadmap]

And [full set of learning videos and supporting materials]
 

Topics: Java Distribution rpc