32 Routing Mechanism How Requests Actually Flow That's Figured Out Above

32 Routing Mechanism How Requests Actually Flow That’s Figured Out Above #

As the second lesson of analyzing the dubbo-cluster module, in this lesson we will introduce another core concept involved in the dubbo-cluster module - Router.

The main function of the Router is to filter out the Invoker collection that meets the conditions based on the routing rules configured by the user and the information carried in the request, for subsequent load balancing logic to use. In the previous lesson on the implementation of RegistryDirectory, we have already seen the existence of the Router chain through RouterChain. However, we did not analyze it in detail. Let’s take an in-depth look at Router.

RouterChain, RouterFactory, and Router #

Let’s start by looking at the core fields of RouterChain.

  • invokers (of type List`>): Invoker collection that the current RouterChain object needs to filter. As we can see, it is set using the RouterChain.setInvokers() method in the StaticDirectory.
  • builtinRouters (of type List`): Collection of built-in Routers activated by the current RouterChain.
  • routers (of type List`): Collection of Router used in the current RouterChain, which includes not only all Router objects in the builtinRouters collection, but also Router objects added through the addRouters() method.

In the constructor of RouterChain, it searches for the value of the router parameter in the URL parameter, and based on that value, gets the activated RouterFactory, and then loads these activated RouterFactory objects through the Dubbo SPI mechanism. The activated built-in Router instance is created by the RouterFactory. The specific implementation is as follows:

private RouterChain(URL url) {

    // Load the activated RouterFactory using ExtensionLoader

    List<RouterFactory> extensionFactories = ExtensionLoader.getExtensionLoader(RouterFactory.class)

            .getActivateExtension(url, "router");

    // Iterate all RouterFactory and call their getRouter() method to create corresponding Router objects

    List<Router> routers = extensionFactories.stream()

            .map(factory -> factory.getRouter(url))

            .collect(Collectors.toList());

    initWithRouters(routers); // Initialize the `builtinRouters` and `routers` fields

}

public void initWithRouters(List<Router> builtinRouters) {

    this.builtinRouters = builtinRouters;

    this.routers = new ArrayList<>(builtinRouters);

    this.sort(); // This sorts the `routers` collection

}

After initializing the built-in Router, new Router instances can be added to the routers field through the addRouter() method in Directory implementation. The specific implementation is as follows:

public void addRouters(List<Router> routers) {

    List<Router> newRouters = new ArrayList<>();

    newRouters.addAll(builtinRouters); // Add the builtinRouters collection

    newRouters.addAll(routers); // Add the passed Router collection

    CollectionUtils.sort(newRouters); // Re-sort

    this.routers = newRouters;

}

The RouterChain.route() method traverses the routers field and calls the route() method of each Router object to filter the invokers collection. The specific implementation is as follows:

public List<Invoker<T>> route(URL url, Invocation invocation) {

    List<Invoker<T>> finalInvokers = invokers;

    for (Router router : routers) { // Iterate all Router objects

        finalInvokers = router.route(finalInvokers, url, invocation);

    }

    return finalInvokers;

}

After understanding the overall logic of RouterChain, we know that the actual routing is performed by the Router objects in the routers collection. Next, let’s take a look at the RouterFactory, which is an extension interface. The specific definition is as follows:

@SPI

public interface RouterFactory {

    @Adaptive("protocol") // The dynamically generated adapter selects the extension implementation based on the `protocol` parameter

    Router getRouter(URL url);

}

There are many implementation classes of the RouterFactory interface, as shown in the following diagram:

Drawing 0.png

Inheritance diagram of RouterFactory

Now, let’s delve into each implementation class of RouterFactory and its corresponding Router implementation object. Router determines the target service of a Dubbo invocation, and each implementation class of the Router interface represents a routing rule. When a Consumer accesses a Provider, Dubbo filters out the appropriate Provider list based on the routing rules, and then performs further filtering through the load balancing algorithm. The inheritance relationship of the Router interface is as shown in the following diagram:

Drawing 1.png

Inheritance diagram of Router

Next, let’s start by introducing the specific implementations of RouterFactory and Router.

ConditionRouterFactory & ConditionRouter #

First, let’s look at the ConditionRouterFactory implementation. Its extension name is “condition”, and its getRouter() method creates a ConditionRouter object, as shown below:

public Router getRouter(URL url) {

    return new ConditionRouter(url);

}

ConditionRouter is an implementation class of routing based on conditional expressions. Here is an example of a routing rule based on a conditional expression:

host = 192.168.0.100 => host = 192.168.0.150

In the above rule, the conditions before => are for matching the Consumer. All the parameters in this condition will be compared with the Consumer’s URL. When the Consumer meets the matching conditions, the filtering rules after => will be applied to the Consumer’s invocation.

The filtering conditions after => are for the Provider address list. All the parameters in this condition will be compared with the Provider’s URL, and the Consumer will only get the filtered address list.

If the matching conditions for the Consumer are empty, it means that the filtering conditions after => apply to all Consumers. For example: => host != 192.168.0.150 means that no Consumer can request the Provider node 192.168.0.150.

If the filtering conditions for the Provider are empty, it means that no Provider can be accessed. For example: host = 192.168.0.100 => means that the Consumer with IP 192.168.0.100 cannot access any Provider node.

There are several core fields in ConditionRouter:

  • url (of type URL): The URL of the routing rule. Specific routing rules can be obtained from the rule parameter.
  • ROUTE_PATTERN (of type Pattern): A regular expression used to split the routing rule.
  • priority (of type int): The priority of the routing rule, used for sorting. The larger the value of this field, the higher the priority. The default value is 0.
  • force (of type boolean): Whether to enforce execution when the routing result is empty. If not enforced, routing rules with empty results will be automatically invalidated. If enforced, empty routing results will be returned directly.
  • whenCondition (of type Map): The collection of conditions matched by the Consumer. By parsing the => part before the rule expression, the content of this collection can be obtained.
  • thenCondition (of type Map): The collection of conditions matched by the Provider. By parsing the => part after the rule expression, the content of this collection can be obtained.

In the construction method of ConditionRouter, the priority, force, and enable fields are initialized based on the corresponding parameters carried in the URL. Then, the routing rule is obtained from the rule parameter of the URL and is parsed in the init() method. The specific parsing logic is implemented as follows:

public void init(String rule) {
    
    // Remove the "consumer." and "provider." strings from the routing rule
    rule = rule.replace("consumer.", "").replace("provider.", "");
    
    // Split the routing rule into whenRule and thenRule using the "=>" string
    int i = rule.indexOf("=>");
    String whenRule = i < 0 ? null : rule.substring(0, i).trim();
    String thenRule = i < 0 ? rule.trim() : rule.substring(i + 2).trim();
    
     // Parse whenRule and thenRule to obtain the whenCondition and thenCondition collections of conditions
    Map<String, MatchPair> when = StringUtils.isBlank(whenRule) || "true".equals(whenRule) ? new HashMap<String, MatchPair>() : parseRule(whenRule);
    Map<String, MatchPair> then = StringUtils.isBlank(thenRule) || "false".equals(thenRule) ? null : parseRule(thenRule);
    
    this.whenCondition = when;
    this.thenCondition = then;
}

In the whenCondition and thenCondition collections, the Key is the parameter name specified in the condition expression (e.g., host in the expression host = 192.168.0.150). ConditionRouter supports three types of parameters:

  • Service invocation information, such as method, argument, etc.
  • URL fields, such as protocol, host, port, etc.
  • All parameters on the URL, such as application, etc.

The Value is a MatchPair object, which includes two sets: matches and mismatches. When using MatchPair for filtering, the following four rules are followed:

  1. When the mismatches set is empty, each matching condition in the matches set will be checked one by one. If any of them matches successfully, true will be returned. The matching logic and the matching logic for conditions in the mismatches set are implemented in the UrlUtils.isMatchGlobPattern() method, which performs the following operations: If the matching condition starts with the “$” symbol, the corresponding parameter value will be obtained from the URL for matching. When encountering the “” wildcard, the case of the “” wildcard being at the beginning, middle, or end of the matching condition will be handled.
  2. When the matches set is empty, each matching condition in the mismatches set will be checked one by one. If any of them matches successfully, false will be returned.
  3. When both the matches set and mismatches set are not empty, the matching conditions in the mismatches set will be checked first. If any of them matches successfully, false will be returned. If all the conditions in the mismatches set fail to match, the matching conditions in the matches set will be checked, and if any of them matches successfully, true will be returned.
  4. If none of the above three steps match successfully, false will be returned directly.

The above process is specifically implemented in the isMatch() method of the MatchPair, which is relatively simple and will not be shown here.

After understanding the matching flow of each MatchPair, let’s see how the parseRule() method parses a complete condition expression and generates the corresponding MatchPair. The specific implementation is as follows:

private static Map<String, MatchPair> parseRule(String rule) throws ParseException {
    
    Map<String, MatchPair> condition = new HashMap<String, MatchPair>();
    MatchPair pair = null;
    Set<String> values = null;
    
    // First, match the entire condition expression according to the regular expression specified by ROUTE_PATTERN
    final Matcher matcher = ROUTE_PATTERN.matcher(rule);
    
    while (matcher.find()) { // Traverse the matching results
        // Each matching result has two parts (groups). The first part is the separator and the second part is the content
        String separator = matcher.group(1);
        String content = matcher.group(2);
        
        if (StringUtils.isEmpty(separator)) { // (1) No separator, content is the parameter name
            pair = new MatchPair();
            // Initialize the MatchPair object and record it with the corresponding Key (i.e., content) in the condition collection
            condition.put(content, pair);
        }
        else if ("&".equals(separator)) { // (4)
            // The "&" separator indicates multiple expressions, so multiple MatchPair objects will be created
            if (condition.get(content) == null) {
                pair = new MatchPair();
                condition.put(content, pair);
            } else {
                pair = condition.get(content);
            }
        }
        else if ("=".equals(separator)) { // (2)
            pair.setMatches(values);
            values = null;
        }
        else if ("!=".equals(separator) || "~=".equals(separator)) { // (3)
            values = new HashSet<String>();
            values.add(content);
            pair.setMismatches(values);
            values = null;
        }
    }
    
    return condition;
}

// = and != The two separators indicate the boundary line of KV.if (pair == null) {throw new ParseException("…");}values = pair.matches;values.add(content);} else if ("!=".equals(separator)) { // —(5)if (pair == null) {throw new ParseException("…");}values = pair.mismatches;values.add(content);} else if (",".equals(separator)) { // —(3)// Comma separator indicates multiple Value valuesif (values == null || values.isEmpty()) {throw new ParseException("…");}values.add(content);} else {throw new ParseException("…");}}return condition;}

After introducing the implementation of the parseRule() method, we can further understand the working principle of the parseRule() method through the parsing process of the following conditional expression example:

host = 2.2.2.2,1.1.1.1,3.3.3.3 & method !=get => host = 1.2.3.4

After grouping by the ROUTE_PATTERN regular expression, we get the following groups: 2.png

Rule group diagram

Let’s start with the processing of the Consumer matching rules before =>.

  1. In group 1, the separator is an empty string and the content is the “host” string. At this time, the program will enter the branch shown in the example code above (“1”), create a MatchPair object, and record it as the Key in the condition set with “host”.
  2. In group 2, the separator is the “=” empty string, and the content is the “2.2.2.2” string. When processing this group, the program will enter the branch in the parseRule() method “2” and add the “2.2.2.2” string to the matches collection of the MatchPair.
  3. In group 3, the separator is the “,” string, and the content is the “3.3.3.3” string. When processing this group, the program will enter the branch in the parseRule() method “3” and continue adding the “3.3.3.3” string to the matches collection of the MatchPair.
  4. In group 4, the separator is the “&” string, and the content is the “method” string. When processing this group, the program will enter the branch in the parseRule() method “4” and create a new MatchPair object, and record it as the Key in the condition set with “method”.
  5. In group 5, the separator is the “!=” string, and the content is the “get” string. When processing this group, the program will enter the branch in the parseRule() method “5”, and add the “get” string to the mismatches collection of the MatchPair created in step 4.

Finally, we get the whenCondition set as shown in the following figure: 3.png

whenCondition set diagram

Similarly, the thenCondition set obtained by parsing the rules after “=>” is shown in the following figure: 1.png

thenCondition set diagram

After understanding the parsing process of the ConditionRouter and the matching principles inside the MatchPair, the last thing to introduce in the ConditionRouter is its route() method.

The ConditionRouter.route() method first tries the whenCondition set created earlier to determine whether the Consumer initiating the call meets the Consumer filtering conditions before “=>”. If it does not meet the conditions, it will directly return the entire invokers set. If it meets the conditions, it will filter the invokers set based on the thenCondition set to get the Invoker set that meets the Provider filtering conditions, and then return it to the upper layer caller. The core implementation of the ConditionRouter.route() method is as follows:

public <T> List<Invoker<T>> route(List<Invoker<T>> invokers, URL url, Invocation invocation)
        throws RpcException {
    ... // Check if the current ConditionRouter object is enabled based on the enable field
    ... // If the current invokers set is empty, return directly
    
    if (!matchWhen(url, invocation)) { // Match whether the Consumer initiating the request meets the filtering conditions before "=>"
        return invokers;
    }
    
    List<Invoker<T>> result = new ArrayList<Invoker<T>>();
    
    if (thenCondition == null) { // Check if there are Provider filtering conditions after "=>". If not, return an empty set directly, indicating that there are no available Providers
        return result;
    }
    
    for (Invoker<T> invoker : invokers) { // Check whether the Invoker meets the filtering conditions after "=>"
        if (matchThen(invoker.getUrl(), url)) {
            result.add(invoker); // Record Invokers that meet the conditions
        }
    }
    
    if (!result.isEmpty()) {
        return result;
    }
    } else if (force) { // When no Invoker meets the condition, decide whether to return an empty collection or all Invokers based on the force parameter

        return result;

    }

    return invokers;

}

ScriptRouterFactory & ScriptRouter #

The extension name of ScriptRouterFactory is script, and its getRouter() method will create a ScriptRouter object and return it.

ScriptRouter supports all scripts of JDK script engine such as JavaScript, JRuby, Groovy, etc., by setting the script type with the type=javascript parameter. JavaScript is the default script type. Let’s define a route() function for host filtering:

function route(invokers, invocation, context){

    var result = new java.util.ArrayList(invokers.size()); 

	var targetHost = new java.util.ArrayList();

	targetHost.add("10.134.108.2"); 

	for (var i = 0; i < invokers.length; i++) {  // Traverse the Invoker collection

        // Check if the host of the Invoker meets the condition

		if(targetHost.contains(invokers[i].getUrl().getHost())){

			result.add(invokers[i]);

		}

	}

	return result;

}

route(invokers, invocation, context)  // Execute the `route()` function immediately

We can encode the above code and add it as the value of the rule parameter in the URL. When this URL is passed to the constructor of ScriptRouter, it can be parsed by ScriptRouter.

The core fields of ScriptRouter are as follows:

  • url (URL type): The URL of the routing rule, and the specific routing rule can be obtained from the rule parameter.
  • priority (int type): The priority of the routing rule used for sorting. The larger the field value, the higher the priority. The default value is 0.
  • ENGINES (ConcurrentHashMap type): This is a static collection where the Key is the name of the script language and the Value is the corresponding ScriptEngine object. ScriptEngine objects are reused according to the type of the script language.
  • engine (ScriptEngine type): The ScriptEngine object used by the current ScriptRouter.
  • rule (String type): The specific script content used by the current ScriptRouter.
  • function (CompiledScript type): The function compiled based on the specific script content rule.

In the constructor of ScriptRouter, the url field and priority field (used for sorting) are initialized first. Then, the engine, rule, and function three core fields are initialized based on the type parameter in the URL. The implementation is as follows:

public ScriptRouter(URL url) {

    this.url = url;

    this.priority = url.getParameter(PRIORITY_KEY, SCRIPT_ROUTER_DEFAULT_PRIORITY);

    // Get the corresponding ScriptEngine object from the ENGINES collection based on the `type` parameter value in the URL

    engine = getEngine(url);

    // Get the `rule` parameter value from the URL, which is the specific script content

    rule = getRule(url);

    Compilable compilable = (Compilable) engine;

    // Compile the script in the `rule` field to get the `function` field

    function = compilable.compile(rule);

}

Next, let’s take a look at the implementation of the route() method in ScriptRouter. First, the necessary parameters for calling the function function, which is the Bindings object, are created. Then, the function function is called to get the filtered Invoker collection, and finally, the getRoutedInvokers() method is used to organize the Invoker collection and obtain the final return value.

public <T> List<Invoker<T>> route(List<Invoker<T>> invokers, URL url, Invocation invocation) throws RpcException {

    // Create a `Bindings` object as the parameter for the `function` function

    Bindings bindings = createBindings(invokers, invocation);

    if (function == null) {

        return invokers;

    }

    // Call the `function` function and organize the obtained Invoker collection in the `getRoutedInvokers()` method

    return getRoutedInvokers(function.eval(bindings));

}

private <T> Bindings createBindings(List<Invoker<T>> invokers, Invocation invocation) {

    Bindings bindings = engine.createBindings();

    // Combined with the example script in JavaScript above, we can see that the `bindings` object provides the `invokers`, `Invocation`, and `context` parameters for the `route()` function in the script.

    bindings.put("invokers", new ArrayList<>(invokers));

    bindings.put("invocation", invocation);

    bindings.put("context", RpcContext.getContext());

    return bindings;

}

Summary #

In this lesson, we focused on the Router interface. First, we introduced the core implementation of RouterChain and the construction process. Then, we explained the functionality of the RouterFactory interface and the Router interface’s core methods. Next, we analyzed in-depth the implementation of the condition routing feature in ConditionRouter and the scripting routing feature in ScriptRouter.