13. Consumer Method Call Procedures for Service Introduction (Cluster Fault Tolerance and Load Balancing)

Keywords: Dubbo Netty encoding Attribute

The previous section analyses the logic of provider directory refresh and connection between consumers and providers introduced by services, and analyses the following major packaging levels, which I divide into three layers

MockClusterInvoker
    invoker -> FailoverClusterInvoker(If the consumer sets it up group,So this will become MergeableClusterInvoker)
                    directory -> RegistryDirectory(With the function of dynamically updating the provider directory)
                                    urlInvokerMap -> This is a Map,key Provider url,The value is InvokeDelegate

        
InvokeDelegate
    invoker -> ProtocolFilterWrapper
                invoker -> ListenerInvokerWrapper
                            invoker -> DubboInvoker
                                        clients -> This is an array whose number of elements depends on how many connections are allowed when the configuration service is introduced. ReferenceCountExchangeClient
                                        
                                        
ReferenceCountExchangeClient
    client -> HeaderExchangeClient
                client -> NettyClient
                            channel -> NioSocketChannel
                            handler -> MultiMessageHandler
                                            handler -> HeartbeatHandler
                                                           handler -> AllChannelHandler
                                                                            handler -> DecodeHandler
                                                                                            handler -> HeaderExchangeChannel
                                                                                                            handler -> DubboProtocol#RequHandler (internal class)

MockCluster Invoker instance belongs to the outermost layer of invoker. This MockCluster Invoker is passed into ProxyFactory.getProxy(invoker) to form a consumer-side interface proxy, so when the consumer-side interface invokes a method, it will call MockCluster Invoker. It can be said that this class is the entry to prepare to invoke the provider service, MockCluster Invoker. The invoker directly referenced by sterInvoker is a failover caller (in the case of a group specified, it is a mergeable caller), and the list of providers held by Failover Cluster Invoker is a dynamic list that updates the list of service providers based on changes in the registry.

The InvokeDelegate layer represents a provider, and it sets up a series of filters on the way to invoke provider services.

The Reference Count Exchange Client layer represents the specific connection of each invoker. They are encapsulated in the client attribute of DubboInvoker and are an array. This layer mainly deals with the channel of netty, and then sends information to the provider. Once a message is sent to the channel of netty, com.alibaba.dub will be called. Bo. remoting. transport. netty4. NettyCodecAdapter. InternalEncoder encodes parameters.
Dubbo's own channel processor is invoked, but for sending requests, dubbo's channel processing does not do more logic. If it is called synchronously, it just encapsulates the request into DefaultFuture and waits for the return of the service provider. If the service provider returns the data, then dubbo's channel will be very large. Big use, it will call MultiMessageHandler - > Heartbeat Handler - > All Channel Handler - > DecodeHandler - > Header Exchange Channel - > DubboProtocol # Request Handler. See the series of articles on the processing of requestor response data for specific logic.

Let's look at this: What happens when an interface calls a method? As mentioned earlier, MockCluster Invoker is the entry to start invoking providers

public Result com.alibaba.dubbo.rpc.cluster.support.wrapper.MockClusterInvoker#invoke(Invocation invocation) throws RpcException {
    Result result = null;
    //Get the mock value
    String value = directory.getUrl().getMethodParameter(invocation.getMethodName(), Constants.MOCK_KEY, Boolean.FALSE.toString()).trim();
    if (value.length() == 0 || value.equalsIgnoreCase("false")) {
        //no mock
        //No need for mock
        result = this.invoker.invoke(invocation);
    } else if (value.startsWith("force")) {
        if (logger.isWarnEnabled()) {
            logger.info("force-mock: " + invocation.getMethodName() + " force-mock enabled , url : " + directory.getUrl());
        }
        //force:direct mock
        //Force a call to the mock service
        result = doMockInvoke(invocation, null);
    } else {
        //fail-mock
        try {
            result = this.invoker.invoke(invocation);
        } catch (RpcException e) {
            if (e.isBiz()) {
                throw e;
            } else {
                if (logger.isWarnEnabled()) {
                    logger.warn("fail-mock: " + invocation.getMethodName() + " fail-mock enabled , url : " + directory.getUrl(), e);
                }
                result = doMockInvoke(invocation, e);
            }
        }
    }
    return result;
}

The above code is mainly to check whether the call of mock service is needed. From the code point of view, it can be divided into two kinds, one is mandatory call, the other is called after the failure of calling service interface.

//Examples are Failover Cluster Invoker (failover)
public Result com.alibaba.dubbo.rpc.cluster.support.AbstractClusterInvoker#invoke(final Invocation invocation) throws RpcException {
    //Check if the current invoker has been destroyed
    checkWhetherDestroyed();
    LoadBalance loadbalance = null;

    // binding attachments into invocation.
    //Getting additional parameters from the rpc call context
    Map<String, String> contextAttachments = RpcContext.getContext().getAttachments();
    if (contextAttachments != null && contextAttachments.size() != 0) {
        ((RpcInvocation) invocation).addAttachments(contextAttachments);
    }
    //Getting available providers
    List<Invoker<T>> invokers = list(invocation);
    if (invokers != null && !invokers.isEmpty()) {
        //Get load balancing policy, default is random policy
        loadbalance = ExtensionLoader.getExtensionLoader(LoadBalance.class).getExtension(invokers.get(0).getUrl()
                .getMethodParameter(RpcUtils.getMethodName(invocation), Constants.LOADBALANCE_KEY, Constants.DEFAULT_LOADBALANCE));
    }
    //If it is an asynchronous call, the call id is generated by atomic operation
    RpcUtils.attachInvocationIdIfAsync(getUrl(), invocation);
    return doInvoke(invocation, invokers, loadbalance);
}

The most important thing about the above code is to filter providers. Now let's see how it filters.

//It is an example of Registry Directory
public List<Invoker<T>> com.alibaba.dubbo.rpc.cluster.directory.AbstractDirectory#list(Invocation invocation) throws RpcException {
    if (destroyed) {
        throw new RpcException("Directory already destroyed .url: " + getUrl());
    }
    //The actual method of screening providers perfectly inherits spring's coding style
    List<Invoker<T>> invokers = doList(invocation);
    //Getting routes from the last service subscription
    List<Router> localRouters = this.routers; // local reference
    if (localRouters != null && !localRouters.isEmpty()) {
        for (Router router : localRouters) {
            try {
                //Check whether it is a run-time filter routing (a wave is filtered when subscribing)
                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;
}

com.alibaba.dubbo.registry.integration.RegistryDirectory#doList

When I say everything in the comment, I put the method name of the next piece of code.

public List<Invoker<T>> com.alibaba.dubbo.registry.integration.RegistryDirectory#doList(Invocation invocation) {
    //Disallow access, throw errors
    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;
    //This collection is the result of filtering at the start of service introduction, methodName - > List < Invoker >
    Map<String, List<Invoker<T>>> localMethodInvokerMap = this.methodInvokerMap; // local reference
    if (localMethodInvokerMap != null && localMethodInvokerMap.size() > 0) {
        //Get the method name
        String methodName = RpcUtils.getMethodName(invocation);
        //Get the parameters we passed in when we called the interface
        Object[] args = RpcUtils.getArguments(invocation);
        if (args != null && args.length > 0 && args[0] != null
                && (args[0] instanceof String || args[0].getClass().isEnum())) {
            //If the first parameter is of enumeration type, the key in the localMethod InvokerMap collection is the method name +"."+enumeration value, and different providers can be invoked depending on the change of the first parameter value.
            invokers = localMethodInvokerMap.get(methodName + "." + args[0]); // The routing can be enumerated according to the first parameter
        }
        if (invokers == null) {
            //Get the provider by method name
            invokers = localMethodInvokerMap.get(methodName);
        }
        if (invokers == null) {
            //Get the provider through * which is routed at service introduction start-up
            invokers = localMethodInvokerMap.get(Constants.ANY_VALUE);
        }
        if (invokers == null) {
            //Get the first group
            Iterator<List<Invoker<T>>> iterator = localMethodInvokerMap.values().iterator();
            if (iterator.hasNext()) {
                invokers = iterator.next();
            }
        }
    }
    //Return
    return invokers == null ? new ArrayList<Invoker<T>>(0) : invokers;
}

After routing screening, the next is the home of load balancing.

public Result com.alibaba.dubbo.rpc.cluster.support.FailoverClusterInvoker#doInvoke(Invocation invocation, final List<Invoker<T>> invokers, LoadBalance loadbalance) throws RpcException {
    List<Invoker<T>> copyinvokers = invokers;
    //If copyinvokers are empty, throw an error
    checkInvokers(copyinvokers, invocation);
    //Retries parameter value is obtained from url, indicating the number of failed retries, default is 2, if the first call is counted as 3.
    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);
    //loop
    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 is greater than zero, it means that a retry failed.
        if (i > 0) {
            //Check that the current invoker is closed
            checkWhetherDestroyed();
            //Re-pass routing screening because providers are likely to drop out at any time
            copyinvokers = list(invocation);
            // check again
            //Check copyinvokers again for value
            checkInvokers(copyinvokers, invocation);
        }
        //Select one through load balancing
        Invoker<T> invoker = select(loadbalance, invocation, copyinvokers, invoked);
        invoked.add(invoker);
        //Record in rpc context
        RpcContext.getContext().setInvokers((List) invoked);
        try {
            //Calling invoker of an executable remote provider
            Result result = invoker.invoke(invocation);
            //Even if the retry is successful, a log reminder will be printed
            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 {
            //Record the address of the provider that has been invoked to print the log after successful retry and which machine calls failed!
            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);
}
                                                    |
                                                    V
//Select: Excluded providers, which usually have value when retried. These values were all failed in the last call. invokers: Candidate providers
protected Invoker<T> com.alibaba.dubbo.rpc.cluster.support.AbstractClusterInvoker#select(LoadBalance loadbalance, Invocation invocation, List<Invoker<T>> invokers, List<Invoker<T>> selected) throws RpcException {
    if (invokers == null || invokers.isEmpty())
        return null;
    //Get the method name
    String methodName = invocation == null ? "" : invocation.getMethodName();
    //Judging whether to use stickiness strategy, stickiness is to use the same machine as possible for subsequent requests after using a provider's services.
    boolean sticky = invokers.get(0).getUrl().getMethodParameter(methodName, Constants.CLUSTER_STICKY_KEY, Constants.DEFAULT_CLUSTER_STICKY);
    {
        //ignore overloaded method
        //Check that a provider was screened last time and that the provider is still providing services
        if (stickyInvoker != null && !invokers.contains(stickyInvoker)) {
            //If the provider is offline, set it to null
            stickyInvoker = null;
        }
        //ignore concurrency problem
        //If the service condition, then use the provider that was last filtered
        if (sticky && stickyInvoker != null && (selected == null || !selected.contains(stickyInvoker))) {
            if (availablecheck && stickyInvoker.isAvailable()) {
                return stickyInvoker;
            }
        }
    }
    //Where Load Balancing Strategies Are Really Used
    Invoker<T> invoker = doSelect(loadbalance, invocation, invokers, selected);
    //If stickiness is allowed, record the results of this screening
    if (sticky) {
        stickyInvoker = invoker;
    }
    return invoker;
}

Most of the explanations have been written in the comments, which are too much to sum up. Now let's continue to analyze how dubbo filters providers through a load strategy.

private Invoker<T> com.alibaba.dubbo.rpc.cluster.support.AbstractClusterInvoker#doSelect(LoadBalance loadbalance, Invocation invocation, List<Invoker<T>> invokers, List<Invoker<T>> selected) throws RpcException {
    if (invokers == null || invokers.isEmpty())
        return null;
    //If there is only one provider, return directly
    if (invokers.size() == 1)
        return invokers.get(0);
    if (loadbalance == null) {
        //If no load balancing policy is specified, get the default
        loadbalance = ExtensionLoader.getExtensionLoader(LoadBalance.class).getExtension(Constants.DEFAULT_LOADBALANCE);
    }
    Invoker<T> invoker = loadbalance.select(invokers, getUrl(), invocation);

    //If the `invoker` is in the  `selected` or invoker is unavailable && availablecheck is true, reselect.
    //If the provider is an unavailable provider, it needs to be re-filtered
    if ((selected != null && selected.contains(invoker))
            || (!invoker.isAvailable() && getUrl() != null && availablecheck)) {
        try {
            //Re-screening, if availablecheck is true, checks whether each provider is available
            Invoker<T> rinvoker = reselect(loadbalance, invocation, invokers, selected, availablecheck);
            if (rinvoker != null) {
                invoker = rinvoker;
            } else {
                //Check the index of current selected invoker, if it's not the last one, choose the one at index+1.
                //If retry fails to get the right provider, get the next or first invoker before re-filtering
                int index = invokers.indexOf(invoker);
                try {
                    //Avoid collision
                    invoker = index < invokers.size() - 1 ? invokers.get(index + 1) : invokers.get(0);
                } catch (Exception e) {
                    logger.warn(e.getMessage() + " may because invokers list dynamic change, ignore.", e);
                }
            }
        } catch (Throwable t) {
            logger.error("cluster reselect fail reason is :" + t.getMessage() + " if can not solve, you can set cluster.availablecheck=false in url", t);
        }
    }
    return invoker;
}

Next we choose a load strategy for analysis. Here we use random strategy analysis and other self-analysis.

protected <T> Invoker<T> com.alibaba.dubbo.rpc.cluster.loadbalance.RandomLoadBalance#doSelect(List<Invoker<T>> invokers, URL url, Invocation invocation) {
    //Get the number of providers
    int length = invokers.size(); // Number of invokers
    //Weight summation
    int totalWeight = 0; // The sum of weights
    boolean sameWeight = true; // Every invoker has the same weight?
    for (int i = 0; i < length; i++) {
        //Recycling to gain the weight of each provider
        int weight = getWeight(invokers.get(i), invocation);
        totalWeight += weight; // Sum
        //Check whether the weights of each provider are the same. If they are different, mark sameWeight as different, and unequal probability values will be triggered when weights are different.
        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.
        //Obtaining offsets from the sum of weights of 0-n providers
        int offset = random.nextInt(totalWeight);
        // Return a invoker based on the random value.
        for (int i = 0; i < length; i++) {
            //The offset subtracts the weight until it is less than zero. For different weights, their probability of being selected is different. For example, I have three providers, the first weights are 10, the second weights are 30, and the third weights are 60.
            //Their probability of being selected is 10%, 30%, 60%.
            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.
    //If the weights of each provider are the same, then it's simple and rude to randomize one of the n providers directly, and they have the same probability of being selected.
    return invokers.get(random.nextInt(length));
}

For the above code, it is necessary to explain that if we have three providers, the first provider has 10, the second provider has 60, and the third provider has 30, the probability of their being selected is 10%, 60%, 30%.

The sum of weights is 100. We randomly get a number between 0100. The probability that the number falls below 010 is 10%, the probability that it falls below 1070 is 60%, and the probability that it falls below 70100 is 30%.

The orange part represents the provider with a weight of 10, the blue part represents the provider with a weight of 60, the green part represents the provider with a weight of 30, and the total weight is 100. In this 100, I randomly take a value of 20 (offset in the code), so the 20 falls in the blue area when I use this 2. 0 minus the weight of the first provider is enough, only minus the weight of the blue part is not enough, so it chooses the provider whose weight is 60, and its probability of being selected is 60%.

After screening the invoker, we can begin to analyze the second level of decoration.

//... First floor

InvokeDelegate
    invoker -> ProtocolFilterWrapper
                invoker -> ListenerInvokerWrapper
                            invoker -> DubboInvoker
                                        clients -> This is an array whose number of elements depends on how many connections are allowed when the configuration service is introduced. ReferenceCountExchangeClient
                                        
//... The third level

Its entry Invoke Delegate, for this layer of Invoke Delegate did not do more logic, directly invoked later, ProtocolFilter Wrapper, Listener Invoker Wrapper no longer analysis here, specific operations can see the request or response data processing series of articles, here the main analysis of different things, then It's Dubbo Invoker.

protected Result com.alibaba.dubbo.rpc.protocol.dubbo.DubboInvoker#doInvoke(final Invocation invocation) throws Throwable {
    RpcInvocation inv = (RpcInvocation) invocation;
    final String methodName = RpcUtils.getMethodName(invocation);
    inv.setAttachment(Constants.PATH_KEY, getUrl().getPath());
    inv.setAttachment(Constants.VERSION_KEY, version);

    ExchangeClient currentClient;
    //If there is only one connection, use it directly
    if (clients.length == 1) {
        currentClient = clients[0];
    } else {
        //Take turns
        currentClient = clients[index.getAndIncrement() % clients.length];
    }
    try {
        //Check whether it is an asynchronous call
        boolean isAsync = RpcUtils.isAsync(getUrl(), invocation);
        //Whether it is a two-way communication, that is, when a message is sent to the other party, the other party needs to respond to it.
        boolean isOneway = RpcUtils.isOneway(getUrl(), invocation);
        //Waiting overtime
        int timeout = getUrl().getMethodParameter(methodName, Constants.TIMEOUT_KEY, Constants.DEFAULT_TIMEOUT);
        //If isOneway is true, it means one-way transmission without the response of the other party. It is usually used in the case that the other party is a consumer and can only go in and out.
        if (isOneway) {
            //Whether to wait for all data to be sent, if true, the thread will block timeout time and wait for all data to be sent.
            boolean isSent = getUrl().getMethodParameter(methodName, Constants.SENT_KEY, false);
            //send data
            currentClient.send(inv, isSent);
            //Clear the future without getting the result of the other party's response (the other party will not respond).
            RpcContext.getContext().setFuture(null);
            //Return an empty result set directly
            return new RpcResult();
            //Is it an asynchronous call?
        } else if (isAsync) {
            //The request interface wraps the inv parameter as a Request and stores the Future in com. alibaba. dubbo. remoting. exchange. support. DefaultFuture FUTURES
            //Blocking timeout, waiting for the provider to return the response
            ResponseFuture future = currentClient.request(inv, timeout);
            //Set RespseFuture to the rpc context for waiting for the response of the other party, and the user can get the future.
            RpcContext.getContext().setFuture(new FutureAdapter<Object>(future));
            return new RpcResult();
        } else {
            //Get interface response synchronously and clear the future s of rpc context
            RpcContext.getContext().setFuture(null);
            //Synchronized acquisition of interface response
            return (Result) currentClient.request(inv, timeout).get();
        }
    } catch (TimeoutException e) {
        throw new RpcException(RpcException.TIMEOUT_EXCEPTION, "Invoke remote method timeout. method: " + invocation.getMethodName() + ", provider: " + getUrl() + ", cause: " + e.getMessage(), e);
    } catch (RemotingException e) {
        throw new RpcException(RpcException.NETWORK_EXCEPTION, "Failed to invoke remote method: " + invocation.getMethodName() + ", provider: " + getUrl() + ", cause: " + e.getMessage(), e);
    }
}

This is followed by the encoding of messages. See the Request or Response Data Processing series for the encoding of messages, which will not be explained here.

Posted by adambedford on Mon, 16 Sep 2019 01:25:18 -0700