33 Routing Mechanism How Requests Actually Flow That's Figured Out Below

33 Routing Mechanism How Requests Actually Flow That’s Figured Out Below #

In the previous lesson, we introduced the basic functionality of the Router interface and the implementation of loading multiple Routers using RouterChain. Then we explained the logic of handling conditional routing rules in the class ConditionRouter and the logic of handling script routing rules in the class ScriptRouter. In this lesson, we will continue with the content from the previous lesson and introduce the remaining three implementation classes of the Router interface.

FileRouterFactory #

FileRouterFactory is a decorator for ScriptRouterFactory. Its extension is file. FileRouterFactory adds the ability to read files on top of ScriptRouterFactory. We can save the routing rules used by ScriptRouter to a file, and then specify the file path in the URL. FileRouterFactory parses the path to the script file and reads the file content. It then calls ScriptRouterFactory to create the corresponding ScriptRouter object.

Let’s take a look at the specific implementation of the getRouter() method in FileRouterFactory. It completes the conversion from the file protocol URL to the script protocol URL. The following is an example of the conversion: first, the file:// protocol is converted to the script:// protocol. Then, the type parameter and the rule parameter are added. The value of the type parameter is determined based on the file extension, and in this example, it is js. The value of the rule parameter is the file content.

2.png

Let’s analyze the specific implementation of the getRouter() method with the following example:

public Router getRouter(URL url) {

    // Use script protocol by default

    String protocol = url.getParameter(ROUTER_KEY, ScriptRouterFactory.NAME); 

    String type = null; 

    String path = url.getPath(); 

    if (path != null) { // Get the language type of the script file

        int i = path.lastIndexOf('.');

        if (i > 0) {

            type = path.substring(i + 1);

        }

    }

    // Read the content of the script file

    String rule = IOUtils.read(new FileReader(new File(url.getAbsolutePath())));

    boolean runtime = url.getParameter(RUNTIME_KEY, false);

    // Create a URL with script protocol

    URL script = URLBuilder.from(url)

            .setProtocol(protocol)

            .addParameter(TYPE_KEY, type)

            .addParameter(RUNTIME_KEY, runtime)

            .addParameterAndEncoded(RULE_KEY, rule)

            .build();

    // Get the Router implementation corresponding to the script

    return routerFactory.getRouter(script);

}

TagRouterFactory & TagRouter #

TagRouterFactory is an extension implementation of the RouterFactory interface. Its extension is tag. However, it is important to note that TagRouterFactory is different from the ConditionRouterFactory and ScriptRouterFactory introduced in the previous lesson. It indirectly implements the RouterFactory interface by inheriting the abstract class CacheableRouterFactory.

The abstract class CacheableRouterFactory maintains a ConcurrentMap collection (routerMap field) to cache Routers, in which the Key is ServiceKey. In the getRouter() method of CacheableRouterFactory, it first queries the routerMap collection based on the URL’s ServiceKey. If the query fails, it calls the createRouter() abstract method to create the corresponding Router object. In the TagRouterFactory.createRouter() method, the TagRouter object is created.

Tag-based testing environment isolation solution #

Through TagRouter, we can group one or multiple Providers into the same group and constrain the traffic to flow only within the specified group. This makes it easy to achieve traffic isolation, supporting scenarios such as gray release.

Currently, Dubbo provides two ways, dynamic and static, to tag Providers. The dynamic way is to dynamically send tags through the service governance platform, while the static way is to tag in static configurations such as XML. Consumers can add the request.tag attribute in RpcContext’s attachment, and the value saved in the attachment will be continuously passed during a complete remote call. We only need to set it at the beginning of the call to achieve continuous tag passing.

After understanding the basic concepts and functions of Tag, let’s briefly introduce an example of using Tag.

In actual development and testing, a complete request may involve a large number of Providers, which are maintained by different teams. These teams deal with different requirements every day and make modifications in the Providers they are responsible for. If all teams use the same testing environment, the testing environment will become unstable. As shown in the figure below, 4 Providers are managed by different teams, and the versions with bugs are deployed for Provider 2 and Provider 4 in the testing environment. This will cause the entire testing environment to fail to process requests, and it is very difficult to troubleshoot bugs in such an unstable testing environment because it may eventually be found to be someone else’s bug.

3.png

Provider nodes in different states

To solve the above problem, we can independently set up a separate testing environment for each requirement using the Tag approach. However, this solution will require a large number of machines, and the initial setup cost and subsequent maintenance cost will be very high.

The following is an architectural diagram of environment isolation implemented through the Tag approach. For Requirement 1, all requests to Provider 2 will be routed to Providers with the Requirement 1 tag, and other Providers will use Providers in a stable testing environment. For Requirement 2, all requests to Provider 4 will go to Provider 4 with the Requirement 2 tag, and other Providers will use Providers in a stable testing environment.

4.png Tag-based Testing Environment Isolation Solution

In some special scenarios, there may be a downgrade scenario for Tags. For example, if there is no corresponding Provider for a Tag, a downgrade will be performed according to certain rules. If the Provider cluster does not have a Provider node corresponding to the requested Tag, the request Tag will be downgraded to an empty Provider by default. If we want to throw an exception when a matching Tag Provider node is not found, we need to set request.tag.force = true.

If the request.tag in the request is not set, only the Provider with an empty Tag will be matched. In other words, even if there are available services in the cluster, they cannot be called if the Tag does not match. In summary, requests with Tags can be downgraded to Providers without Tags, but requests without Tags can never access Providers with Tags.

TagRouter #

Next, let’s take a look at the specific implementation of TagRouter. TagRouter holds a reference to a TagRouterRule object, and TagRouterRule maintains a collection of Tags. Each Tag object maintains the name of the Tag and the collection of network addresses bound to the Tag, as shown in the following diagram:

5.png

TagRouter, TagRouterRule, Tag, and address mapping diagram

In addition, TagRouterRule also maintains two collections in addressToTagnames and tagnameToAddresses (both of type Map`>), which respectively record the mapping from Tag names to addresses and from addresses to Tag names. In the init() method of TagRouterRule, these two collections are initialized based on the tags collection.

After understanding the basic construction of TagRouterRule, let’s continue to look at how TagRouter constructs TagRouterRule. In addition to implementing the Router interface, TagRouter also implements the ConfigurationListener interface, as shown in the following diagram:

6.png

TagRouter inheritance diagram

ConfigurationListener is used to listen for changes in the configuration, including changes to the TagRouterRule configuration. When we dynamically update the TagRouterRule configuration, the process() method of the ConfigurationListener interface is triggered. The implementation of the process() method in TagRouter is as follows:

public synchronized void process(ConfigChangedEvent event) {

    // DELETED event will directly clear tagRouterRule

    if (event.getChangeType().equals(ConfigChangeType.DELETED)) {

        this.tagRouterRule = null;

    } else { // other events will parse the latest routing rules and record them in the tagRouterRule field

        this.tagRouterRule = TagRuleParser.parse(event.getContent());

    }

}

We can see that if the operation is to delete the configuration, the tagRouterRule is set to null directly. If it is to modify or add the configuration, the passed configuration is parsed by TagRuleParser to obtain the corresponding TagRouterRule object. TagRuleParser can parse the yaml-formatted TagRouterRule configuration. Here is an example configuration:

force: false

runtime: true

enabled: false

priority: 1

key: demo-provider

tags:

  - name: tag1

    addresses: null

  - name: tag2

    addresses: ["30.5.120.37:20880"]

  - name: tag3

    addresses: []

The TagRouterRule structure obtained after parsing by TagRuleParser is shown in the following image:

1.png

TagRouterRule structure diagram

In addition to the several collection fields shown in the above image, TagRouterRule also inherits some control fields from the AbstractRouterRule abstract class. The ConditionRouterRule introduced later also inherits AbstractRouterRule.

9.png

Inheritance diagram of AbstractRouterRule

The specific meanings of the core fields in AbstractRouterRule can be summarized as follows:

  • key (string type), scope (string type): key specifies which service or application the rule applies to. When scope is “service”, key consists of [{group}:]{service}[:{version}]; when scope is “application”, key is the name of the application.
  • rawRule (string type): records the original string configuration before the routing rule is parsed.
  • runtime (boolean type): indicates whether the routing rule is executed on each invocation. If set to false, it will be executed in advance and cached when the Provider list changes, and the routing result will be directly obtained from the cache during invocation.
  • force (boolean type): whether to force execution when the routing result is empty. If not forced, the routing rule with an empty result will be automatically invalidated. The default value of this field is false.
    • valid (boolean type): Used to identify whether the configuration used to generate the current RouterRule object is valid.
    • enabled (boolean type): Identifies whether the current routing rule is effective.
    • priority (int type): Used to represent the priority of the current RouterRule.
    • dynamic (boolean type): Indicates whether the routing rule is persistent. When the registry exits, whether the routing rule still exists.

As we can see, the core fields in AbstractRouterRule correspond one-to-one with the example configuration mentioned earlier.

We know that the ultimate goal of the Router is to filter Invoker objects that meet certain conditions. Now let’s take a look at how TagRouter uses TagRouterRule for Invoker filtering. The general steps are as follows:

  1. If invokers is empty, return an empty collection directly.
  2. Check if the associated TagRouterRule object is available. If not available, it will directly call the filterUsingStaticTag() method for filtering and return the filtered result. In the filterUsingStaticTag() method, it compares the tag value carried in the request with the tag parameter value in the Provider URL.
  3. Get the tag information for this invocation, which will attempt to get it from Invocation and URL parameters.
  4. If the tag information is specified in this request, it will first obtain the address set associated with the tag. 1. If the address set is not empty, it matches the Invoker set that meets the conditions based on the addresses in the set. If there are Invokers that meet the conditions, it directly returns the filtered Invoker set. If not, it determines whether to return an empty Invoker set based on the force configuration. 2. If the address set is empty, it compares the tag value carried in the request with the tag parameter value in the Provider URL, and matches the Invoker set that meets the conditions. If there are Invokers that meet the conditions, it directly returns the filtered Invoker set. If not, it determines whether to return an empty Invoker set based on the force configuration. 3. If the force configuration is false and the Invoker set that meets the conditions is empty, it returns the list of Providers that do not include any tags.
  5. If the tag information is not carried in this request, it first obtains the address set associated with all tags in the TagRouterRule. If the address set is not empty, it filters out the Invokers that are not in the address set and adds them to the result set. Finally, it compares the tag value in the Provider URL with the tag names in the TagRouterRule to get the final Invoker set.

The implementation of the above process is in the TagRouter.route() method, as shown below:

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

    ... // If invokers is empty, return an empty collection directly (omitted)

    final TagRouterRule tagRouterRuleCopy = tagRouterRule;

    if (tagRouterRuleCopy == null || !tagRouterRuleCopy.isValid() || !tagRouterRuleCopy.isEnabled()) {

        return filterUsingStaticTag(invokers, url, invocation);

    }

    // Check if the associated TagRouterRule object is available. If not available, it will directly call the filterUsingStaticTag() method for filtering

    List<Invoker<T>> result = invokers;

    // Get the tag information for this invocation, attempt to get it from Invocation and URL

    String tag = StringUtils.isEmpty(invocation.getAttachment(TAG_KEY)) ? url.getParameter(TAG_KEY) :
            invocation.getAttachment(TAG_KEY);

    if (StringUtils.isNotEmpty(tag)) { // This request specifies a special tag

        // Get the address set associated with the tag

        List<String> addresses = tagRouterRuleCopy.getTagnameToAddresses().get(tag);

        if (CollectionUtils.isNotEmpty(addresses)) {

            // Match the Invokers that meet the conditions based on the above address set

            result = filterInvoker(invokers, invoker -> addressMatches(invoker.getUrl(), addresses));

            // If there are Invokers that meet the conditions, directly return the filtered Invoker set
            // If there are no Invokers that meet the conditions, decide whether to return an empty Invoker set based on the force configuration

            if (CollectionUtils.isNotEmpty(result) || tagRouterRuleCopy.isForce()) {

                return result;

            }

        } else {

            // If the address set is empty, compare the tag value carried in the request with the tag parameter value in the Provider URL

            result = filterInvoker(invokers, invoker -> tag.equals(invoker.getUrl().getParameter(TAG_KEY)));

        }

        if (CollectionUtils.isNotEmpty(result) || isForceUseTag(invocation)) {

            return result; // If there are Invokers that meet the conditions or the force configuration is true

        } else { // If the force configuration is false and the Invoker set that meets the conditions is empty, return the list of Providers that do not include any tags.

            List<Invoker<T>> tmp = filterInvoker(invokers, invoker -> addressNotMatches(invoker.getUrl(),
                    tagRouterRuleCopy.getAddresses()));

            return filterInvoker(tmp, invoker -> StringUtils.isEmpty(invoker.getUrl().getParameter(TAG_KEY)));

} } else{ // If the request does not carry tag information, it will first obtain the address set associated with all tags in the TagRouterRule rule. List addresses = tagRouterRuleCopy.getAddresses();

if (CollectionUtils.isNotEmpty(addresses)){
    // If the address set is not empty, filter out the Invoker that is not in the address set and add it to the result set.
    result = filterInvoker(invokers, invoker -> addressNotMatches(invoker.getUrl(), addresses));

    if (CollectionUtils.isEmpty(result)){
        return result;
    }
}

// If there are no Invokers that meet the conditions or the address set is empty, compare the tag carried in the request with the tag parameter value in the Provider URL to obtain the final Invoker set.

return filterInvoker(result, invoker -> {
    String localTag = invoker.getUrl().getParameter(TAG_KEY);
    return StringUtils.isEmpty(localTag) || !tagRouterRuleCopy.getTagNames().contains(localTag);
});

} }

ServiceRouter & AppRouter #

In addition to TagRouterFactory, which inherits CacheableRouterFactory, ServiceRouterFactory also inherits CacheableRouterFactory and has caching capabilities. The specific inheritance relationship is shown in the following figure:

8.png

CacheableRouterFactory Inheritance Relationship Diagram

The Router implemented by ServiceRouterFactory is ServiceRouter, and AppRouter is similar to ServiceRouter. Both inherit the abstract class ListenableRouter (although ListenableRouter is an abstract class, there are no abstract methods for subclasses to implement), and the inheritance relationship is shown in the following figure:

7.png

ListenableRouter Inheritance Relationship Diagram

ListenableRouter adds the ability to dynamically configure based on ConditionRouter. The process() method of ListenableRouter is similar to the process() method of TagRouter. For the DELETE event of ConfigChangedEvent, the references of ConditionRouterRule and ConditionRouter maintained in ListenableRouter are directly cleared. For the ADDED and UPDATED events, ConditionRuleParser is used to parse the event content and obtain the corresponding ConditionRouterRule object and ConditionRouter collection. The ConditionRuleParser here also parses the configuration of ConditionRouterRule in yaml file format. ConditionRouterRule maintains a conditions collection (List type), which records multiple Condition routing rules, corresponding to generating multiple ConditionRouter objects.

The entire process of parsing ConditionRouterRule is similar to the process of parsing TagRouterRule as mentioned above.

In the route() method of ListenableRouter, all ConditionRouter Invoker sets that meet all routing conditions are filtered out by traversing the ConditionRouters. The specific implementation is as follows:

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

    if (CollectionUtils.isEmpty(invokers) || conditionRouters.size() == 0) {

        return invokers; // Check boundary conditions and return the invokers collection directly

    } 

    for (Router router : conditionRouters) { // Filter routing rules

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

    }

    return invokers;

}

ServiceRouter and AppRouter simply inherit the abstract class ListenableRouter and do not override any methods of ListenableRouter. The two differ only in the following two points.

  • One is the difference in the priority field values. ServiceRouter is 140, and AppRouter is 150, which means ServiceRouter is executed before AppRouter.
  • The other is the difference in obtaining the Key of the ConditionRouterRule configuration. ServiceRouter uses a RuleKey consisting of {interface}:[version]:[group], which obtains a ConditionRouterRule corresponding to a service. AppRouter uses the application parameter value in the URL as the RuleKey, which obtains a ConditionRouterRule corresponding to a service instance.

Summary #

In this lesson, we continued with the previous lesson and introduced the remaining implementations of the Router interface.

We first introduced the file-based FileRouter implementation, which depends on the ScriptRouter introduced in the previous lesson. Next, we explained the tag-based test environment isolation solution and how to implement it based on TagRouter. We also analyzed the core implementation of TagRouter in depth. Finally, we introduced the abstract class ListenableRouter and the two implementations of ServerRouter and AppRouter. They add the ability to dynamically change routing rules based on conditional routing. They also distinguish between service-level and service instance-level configurations.