34 Add Meal Initial Probe Into Dubbo Dynamic Configuration

34 Add Meal Initial Probe Into Dubbo Dynamic Configuration #

In Lesson 31, we discussed in detail about the RegistryDirectory. As a NotifyListener listener, the RegistryDirectory listens to the providers, routers, and configurators directories in the registry. Through the RegistryDirectory, we learned that URLs dynamically added in the configurators directory will override the Provider URLs registered in the providers directory. Dubbo will also create a new Invoker object based on the latest configuration in the configurators directory (while destroying the old Invoker object).

In older versions of Dubbo, we could write dynamic configuration URLs to the configurators directory in the registry using the service governance console. In Dubbo 2.7.x, dynamic configuration information can be written not only to the configurators directory in the registry, but also to an external configuration center. We will discuss this in detail in future lessons. In this lesson, we will focus on writing dynamic configuration to the registry.

First, let’s understand the protocols used in the URLs written to the configurators directory, and the meanings of these protocols. We also need to know how Dubbo parses these URLs to get Configurator objects, and how these Configurator objects work together with existing Provider URLs to achieve dynamic configuration updates.

Basic Protocols #

Firstly, we need to understand that there are two protocols for dynamic configuration in the configurators directory: override and absent. Here is an example of the override protocol:

override://0.0.0.0/org.apache.dubbo.demo.DemoService?category=configurators&dynamic=false&enabled=true&application=dubbo-demo-api-consumer&timeout=1000

Let’s analyze the meaning of each part in this URL:

  • override indicates that it uses the override approach. Dubbo supports the override and absent protocols, and we can also extend them using SPI.
  • 0.0.0.0 indicates that it is effective for all IPs. If you want to override the configuration of a specific IP Provider, you can use the specific IP of that Provider.
  • org.apache.dubbo.demo.DemoService indicates that it is only effective for the specified service.
  • category=configurators indicates that this URL is of the dynamic configuration type.
  • dynamic=false indicates that this URL is persistent data. Even if the node that registered this URL exits, the URL will still be saved in the registry.
  • enabled=true indicates that the override rule of this URL is effective.
  • application=dubbo-demo-api-consumer indicates that it is only effective for the specified application. If not specified, it implies it is effective for all applications.
  • timeout=1000 indicates that the timeout parameter value in the Provider URL that meets the above conditions will be overridden with 1000. If you want to override other configurations, you can directly add them as parameters to the override URL.

The Dubbo official website also provides some simple examples, which we will briefly explain here:

  • Disable a specific Provider, usually used to temporarily exclude a specific Provider node:
override://10.20.153.10/com.foo.BarService?category=configurators&dynamic=false&disabled=true
  • Adjust the weight of a specific Provider to 200:
override://10.20.153.10/com.foo.BarService?category=configurators&dynamic=false&weight=200
  • Adjust the load balancing strategy to LeastActiveLoadBalance (we will discuss load balancing in detail in the next lesson):
override://10.20.153.10/com.foo.BarService?category=configurators&dynamic=false&loadbalance=leastactive
  • Service downgrade, usually used to temporarily mask an erroneous non-critical service (we will discuss the specific implementation of the mock mechanism in future lessons):
override://0.0.0.0/com.foo.BarService?category=configurators&dynamic=false&application=foo&mock=force:return+null

Configurator #

When we add URLs with the override (or absent) protocol to the configurators directory in the registry, the Registry will receive a notification from the registry, and it will call back the NotifyListener that is registered on it, which includes the RegistryDirectory. In Lesson 31, we analyzed in detail the process of RegistryDirectory.notify() handling the changes in the providers, configurators, and routers directories. The URLs in the configurators directory are parsed into Configurator objects.

The Configurator interface abstracts a configuration and provides utility methods to parse a configuration URL into a Configurator object. Here is the definition of the Configurator interface:

public interface Configurator extends Comparable<Configurator> {

    // Get the configuration URL corresponding to this `Configurator`. For example, the `override` protocol URL mentioned earlier.

    URL getUrl();

    // The `configure()` method takes the original URL as a parameter and returns the URL modified by the `Configurator`.

    URL configure(URL url);

    // The `toConfigurators()` method can parse multiple configuration URL objects into corresponding `Configurator` objects.

    static Optional<List<Configurator>> toConfigurators(List<URL> urls) {

        // Create a `ConfiguratorFactory` adapter.

        ConfiguratorFactory configuratorFactory = ExtensionLoader.getExtensionLoader(ConfiguratorFactory.class)
                .getAdaptiveExtension();

        List<Configurator> configurators = new ArrayList<>(urls.size()); // Record the parsing result

        for (URL url : urls) {
// If the empty protocol is encountered, clear the configurators collection, 
// end the parsing, and return an empty collection
if (EMPTY_PROTOCOL.equals(url.getProtocol())) {
    configurators.clear();
    break;
}

Map<String, String> override = new HashMap<>(url.getParameters());
override.remove(ANYHOST_KEY);

// If the configuration URL does not carry any parameters, skip it
if (override.size() == 0) {
    configurators.clear();
    continue;
}

// Select the appropriate ConfiguratorFactory extension
// through the ConfiguratorFactory adapter, and create a Configurator object
configurators.add(configuratorFactory.getConfigurator(url));
}

// Sort the configurators
Collections.sort(configurators);

return Optional.of(configurators);

}

// The sorting is first done by IP, where all IPs have higher priority than 0.0.0.0,
// and then by the priority parameter value when the IPs are the same
default int compareTo(Configurator o) {
    if (o == null) {
        return -1;
    }
    int ipCompare = getUrl().getHost().compareTo(o.getUrl().getHost());
    if (ipCompare == 0) {
        int i = getUrl().getParameter(PRIORITY_KEY, 0);
        int j = o.getUrl().getParameter(PRIORITY_KEY, 0);
        return Integer.compare(i, j);
    } else {
        return ipCompare;
    }
}

The ConfiguratorFactory interface is an extension interface. Dubbo provides two implementation classes as shown in the diagram below:

Lark20201120-160501.png

ConfiguratorFactory inheritance diagram

Among them, the OverrideConfiguratorFactory corresponds to the extension name “override”, and creates an implementation of Configurator called OverrideConfigurator. The AbsentConfiguratorFactory corresponds to the extension name “absent”, and creates an implementation of Configurator called AbsentConfigurator.

The inheritance diagram of the Configurator interface is shown in the diagram below:

Lark20201120-160505.png

Configurator inheritance diagram

Among them, the AbstractConfigurator maintains a configuratorUrl field, which records the complete configuration URL. AbstractConfigurator is a template class, and its core implementation is the configure() method, which is implemented as follows:

public URL configure(URL url) {
    // Determine whether the URL is enabled based on the enabled parameter of the configuration URL and the host
    // Also, determine whether to continue with the subsequent override logic based on whether the original URL is empty or if the host of the original URL is empty
    if (!configuratorUrl.getParameter(ENABLED_KEY, true) || configuratorUrl.getHost() == null || url == null || url.getHost() == null) {
        return url;
    }

    // For versions after 2.7.0, a configVersion parameter is added for differentiation
    String apiVersion = configuratorUrl.getParameter(CONFIG_VERSION_KEY);
}
if (StringUtils.isNotEmpty(apiVersion)) { // Handling configuration for versions after 2.7.0

    String currentSide = url.getParameter(SIDE_KEY);

    String configuratorSide = configuratorUrl.getParameter(SIDE_KEY);

    // Match based on the "side" parameter in the configurator URL
    // and the "side" parameter value in the original URL

    if (currentSide.equals(configuratorSide) && CONSUMER.equals(configuratorSide) && 0 == configuratorUrl.getPort()) {

        url = configureIfMatch(NetUtils.getLocalHost(), url);

    } else if (currentSide.equals(configuratorSide) && PROVIDER.equals(configuratorSide) && url.getPort() == configuratorUrl.getPort()) {

        url = configureIfMatch(url.getHost(), url);

    }

} else { // Handling configuration for versions before 2.7.0

    url = configureDeprecated(url);

}

return url;

Here, we need to pay attention to the configureDeprecated() method for compatibility with previous versions. This is also the handling of configuration URLs under the configurators directory in the registry. The implementation is as follows:

private URL configureDeprecated(URL url) {

    // If the port in the configuration URL is not empty, it is for Provider,
    // and the port of the original URL needs to be checked. The two ports must match
    // in order to execute the configuration method in the configureIfMatch() method.

    if (configuratorUrl.getPort() != 0) {

        if (url.getPort() == configuratorUrl.getPort()) {

            return configureIfMatch(url.getHost(), url);

        }

    } else {

        // If no port is specified, the configuration URL is either for Consumer or for any URL (i.e., host is 0.0.0.0)

        // If the original URL belongs to Consumer, use the Consumer's host for matching

        if (url.getParameter(SIDE_KEY, PROVIDER).equals(CONSUMER)) {

            return configureIfMatch(NetUtils.getLocalHost(), url);

        } else if (url.getParameter(SIDE_KEY, CONSUMER).equals(PROVIDER)) {

            // If it is a Provider URL, use 0.0.0.0 for configuration

            return configureIfMatch(ANYHOST_VALUE, url);

        }

    }

    return url;

}

The configureIfMatch() method will exclude the parameters that cannot be dynamically modified in the matching URL, and call the doConfigurator() method of the Configurator subclass to override the original URL. The implementation is as follows:

private URL configureIfMatch(String host, URL url) {

    if (ANYHOST_VALUE.equals(configuratorUrl.getHost()) || host.equals(configuratorUrl.getHost())) { // Match the host

        String providers = configuratorUrl.getParameter(OVERRIDE_PROVIDERS_KEY);

        if (StringUtils.isEmpty(providers) || providers.contains(url.getAddress()) || providers.contains(ANYHOST_VALUE)) {

            String configApplication = configuratorUrl.getParameter(APPLICATION_KEY,

                    configuratorUrl.getUsername());

            String currentApplication = url.getParameter(APPLICATION_KEY, url.getUsername());

            if (configApplication == null || ANY_VALUE.equals(configApplication)

                    || configApplication.equals(currentApplication)) { // Match the application

                // Exclude parameters that cannot be dynamically modified, including category, check, dynamic, enabled,
                // as well as parameters starting with ~

                Set<String> conditionKeys = new HashSet<String>();

                conditionKeys.add(CATEGORY_KEY);

                conditionKeys.add(Constants.CHECK_KEY);

                conditionKeys.add(DYNAMIC_KEY);

                conditionKeys.add(ENABLED_KEY);

                conditionKeys.add(GROUP_KEY);

                conditionKeys.add(VERSION_KEY);

                conditionKeys.add(APPLICATION_KEY);

                conditionKeys.add(SIDE_KEY);

                conditionKeys.add(CONFIG_VERSION_KEY);

                conditionKeys.add(COMPATIBLE_CONFIG_KEY);

                conditionKeys.add(INTERFACES);

                for (Map.Entry<String, String> entry : configuratorUrl.getParameters().entrySet()) {

                    String key = entry.getKey();

                    String value = entry.getValue();

                    if (key.startsWith("~") || APPLICATION_KEY.equals(key) || SIDE_KEY.equals(key)) {

                        conditionKeys.add(key);

                        // If the configuration URL and the value of the parameter in the original URL
                        // that starts with ~ are not the same, the configuration URL is not used to override the original URL.

                        if (value != null && !ANY_VALUE.equals(value)

                                && !value.equals(url.getParameter(key.startsWith("~") ? key.substring(1) : key))) {

                            return url;

                        }

                    }

                }

                // After removing parameters that the configuration URL does not support dynamic configuration,
                // call the doConfigure method of the Configurator subclass to regenerate a URL

                return doConfigure(url, configuratorUrl.removeParameters(conditionKeys));

            }

        }

    }

    return url;

}

Let’s take a closer look at the handling of dynamic configuration after Dubbo 2.7.0 in the configure() method of AbstractConfigurator. It explicitly determines whether the configurator URL and the original URL belong to the Consumer side or the Provider side based on the side parameter. The specific replacement process after the match is also implemented by calling the configureIfMatch() method, which will not be repeated here.

The implementation of the two Configurator subclasses is very simple. In the doConfigure() method of OverrideConfigurator, all remaining parameters in the configuration URL are used to override the corresponding parameters in the original URL. The implementation is as follows:

public URL doConfigure(URL currentUrl, URL configUrl) {

    // Directly call addParameters() to perform the override

    return currentUrl.addParameters(configUrl.getParameters());

}

In the doConfigure() method of AbsentConfigurator, an attempt is made to add the parameters from the configuration URL to the original URL. If the original URL already has the same parameter, it will not be overridden. The implementation is as follows:

public URL doConfigure(URL currentUrl, URL configUrl) {

    // Directly call addParametersIfAbsent() to attempt to add parameters

    return currentUrl.addParametersIfAbsent(configUrl.getParameters());

}

So far, we have completed the introduction to the core implementation of dynamic configuration before Dubbo 2.7.0. Although we also briefly mentioned some logic after Dubbo 2.7.0, we did not fully introduce the configuration format and core processing logic in Dubbo after version 2.7.0. Don’t worry, we will continue to analyze these contents in depth in the “Configuration Center” section.

Summary #

In this lesson, we mainly introduced the implementation of configuration related features in Dubbo. We first analyzed the format and meanings of the override protocol URL and absent protocol URL involved in the configurators directory through examples. Then, we explained in detail how Dubbo parses configurator URLs to obtain Configurator objects, and how Configurators override various parameters of Provider URLs.