天天看點

Dubbo——動态配置

前言

RegistryDirectory 作為一個 NotifyListener 監聽器,RegistryDirectory 會同時監聽注冊中心的 providers、routers 和 configurators 三個目錄。通過 RegistryDirectory 處理 configurators 目錄的邏輯,我們了解到 configurators 目錄中動态添加的 URL 會覆寫 providers 目錄下注冊的 Provider URL,Dubbo 還會按照 configurators 目錄下的最新配置,重新建立 Invoker 對象(同時會銷毀原來的 Invoker 對象)。

在老版本的 Dubbo 中,可以通過服務治理控制台向注冊中心的 configurators 目錄寫入動态配置的 URL。在 Dubbo 2.7.x 版本中,動态配置資訊除了可以寫入注冊中心的 configurators 目錄之外,還可以寫入外部的配置中心,本文重點來看寫入注冊中心的動态配置。

首先,我們需要了解一下 configurators 目錄中 URL 都有哪些協定以及這些協定的含義,然後還要知道 Dubbo 是如何解析這些 URL 得到 Configurator 對象的,以及 Configurator 是如何與已有的 Provider URL 共同作用得到實作動态更新配置的效果。

基礎協定

首先,我們需要了解寫入注冊中心 configurators 中的動态配置有 override 和 absent 兩種協定。下面是一個 override 協定的示例:

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

那這個 URL 中各個部分的含義是怎樣的呢?下面我們就一個一個來分析下:

  • override:表示采用覆寫方式。Dubbo 支援 override 和 absent 兩種協定,我們也可以通過 SPI 的方式進行擴充。
  • 0.0.0.0:表示對所有 IP 生效。如果隻想覆寫某個特定 IP 的 Provider 配置,可以使用該 Provider 的具體 IP。
  • org.apache.dubbo.demo.DemoService:表示隻對指定服務生效。
  • category=configurators:表示該 URL 為動态配置類型。
  • dynamic=false:表示該 URL 為持久資料,即使注冊該 URL 的節點退出,該 URL 依舊會儲存在注冊中心。
  • enabled=true:表示該 URL 的覆寫規則已生效。
  • application=dubbo-demo-api-consumer:表示隻對指定應用生效。如果不指定,則預設表示對所有應用都生效。
  • timeout=1000:表示将滿足以上條件 Provider URL 中的 timeout 參數值覆寫為 1000。如果想覆寫其他配置,可以直接以參數的形式添加到 override URL 之上。

在 Dubbo 的官網中,還提供了一些簡單示例,我們這裡也簡單解讀一下。

  • 禁用某個 Provider,通常用于臨時剔除某個 Provider 節點:
override://10.20.153.10/com.foo.BarService?category=configurators&dynamic=false&disabled=true
           
  • 調整某個 Provider 的權重為 200:
override://10.20.153.10/com.foo.BarService?category=configurators&dynamic=false&weight=200
           
  • 調整負載均衡政策為 LeastActiveLoadBalance(負載均衡的内容會在下一課時詳細介紹):
override://10.20.153.10/com.foo.BarService?category=configurators&dynamic=false&loadbalance=leastactive
           
  • 服務降級,通常用于臨時屏蔽某個出錯的非關鍵服務(mock 機制的具體實作我們會在後面的課時詳細介紹):
override://0.0.0.0/com.foo.BarService?category=configurators&dynamic=false&application=foo&mock=force:return+null
           

Configurator

當我們在注冊中心的 configurators 目錄中添加 override(或 absent)協定的 URL 時,Registry 會收到注冊中心的通知,回調注冊在其上的 NotifyListener,其中就包括 RegistryDirectory。RegistryDirectory.notify() 處理 providers、configurators 和 routers 目錄變更的流程,其中 configurators 目錄下 URL 會被解析成 Configurator 對象。

Configurator 接口抽象了一條配置資訊,同時提供了将配置 URL 解析成 Configurator 對象的工具方法。Configurator 接口具體定義如下:

public interface Configurator extends Comparable<Configurator> {
	// 擷取該Configurator對象對應的配置URL,例如前文介紹的override協定URL
    URL getUrl();

	// configure()方法接收的參數是原始URL,傳回經過Configurator修改後的URL
    URL configure(URL url);

	// toConfigurators()工具方法可以将多個配置URL對象解析成相應的Configurator對象
    static Optional<List<Configurator>> toConfigurators(List<URL> urls) {
        if (CollectionUtils.isEmpty(urls)) {
            return Optional.empty();
        }
		// 建立ConfiguratorFactory擴充卡
        ConfiguratorFactory configuratorFactory = ExtensionLoader.getExtensionLoader(ConfiguratorFactory.class)
                .getAdaptiveExtension();
		// 記錄解析的結果
        List<Configurator> configurators = new ArrayList<>(urls.size());
        for (URL url : urls) {
			// 遇到empty協定,直接清空configurators集合,結束解析,傳回空集合
            if (EMPTY_PROTOCOL.equals(url.getProtocol())) {
                configurators.clear();
                break;
            }
            Map<String, String> override = new HashMap<>(url.getParameters());
            //The anyhost parameter of override may be added automatically, it can't change the judgement of changing url
            override.remove(ANYHOST_KEY);
			// 如果該配置URL沒有攜帶任何參數,則跳過該URL
            if (CollectionUtils.isEmptyMap(override)) {
                continue;
            }
			// 通過ConfiguratorFactory擴充卡選擇合适ConfiguratorFactory擴充,并建立Configurator對象
            configurators.add(configuratorFactory.getConfigurator(url));
        }
		// 排序
        Collections.sort(configurators);
        return Optional.of(configurators);
    }


	// 排序首先按照ip進行排序,所有ip的優先級都高于0.0.0.0,當ip相同時,會按照priority參數值進行排序
    @Override
    default int compareTo(Configurator o) {
        if (o == null) {
            return -1;
        }

        int ipCompare = getUrl().getHost().compareTo(o.getUrl().getHost());
        // host is the same, sort by priority
        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;
        }
    }
}
           

ConfiguratorFactory 接口是一個擴充接口,Dubbo 提供了兩個實作類,如下圖所示:

Dubbo——動态配置

其中,OverrideConfiguratorFactory 對應的擴充名為 override,建立的 Configurator 實作是 OverrideConfigurator;AbsentConfiguratorFactory 對應的擴充名是 absent,建立的 Configurator 實作類是 AbsentConfigurator。

Configurator 接口的繼承關系如下圖所示:

Dubbo——動态配置

其中,AbstractConfigurator 中維護了一個 configuratorUrl 字段,記錄了完整的配置 URL。AbstractConfigurator 是一個模闆類,其核心實作是 configure() 方法,具體實作如下:

public abstract class AbstractConfigurator implements Configurator {

    private final URL configuratorUrl;
	
    @Override
    public URL configure(URL url) {
        // 這裡會根據配置URL的enabled參數以及host決定該URL是否可用,
		// 同時還會根據原始URL是否為空以及原始URL的host是否為空,決定目前是否執行後續覆寫邏輯
        if (!configuratorUrl.getParameter(ENABLED_KEY, true) || configuratorUrl.getHost() == null || url == null || url.getHost() == null) {
            return url;
        }
        /*
         * This if branch is created since 2.7.0.
         */
		// 針對2.7.0之後版本,這裡添加了一個configVersion參數作為區分
        String apiVersion = configuratorUrl.getParameter(CONFIG_VERSION_KEY);
		// 對2.7.0之後版本的配置處理
        if (StringUtils.isNotEmpty(apiVersion)) {
            String currentSide = url.getParameter(SIDE_KEY);
            String configuratorSide = configuratorUrl.getParameter(SIDE_KEY);
			// 根據配置URL中的side參數以及原始URL中的side參數值進行比對
            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);
            }
        }
        /*
         * This else branch is deprecated and is left only to keep compatibility with versions before 2.7.0
         */
        else {
			// 2.7.0版本之前對配置的處理
            url = configureDeprecated(url);
        }
        return url;
    }
}	
           

這裡我們需要關注下configureDeprecated() 方法對曆史版本的相容,其實這也是對注冊中心 configurators 目錄下配置 URL 的處理,具體實作如下:

public abstract class AbstractConfigurator implements Configurator {

    private final URL configuratorUrl;
	
    @Deprecated
    private URL configureDeprecated(URL url) {
        // 如果配置URL中的端口不為空,則是針對Provider的,需要判斷原始URL的端口,
		// 兩者端口相同,才能執行configureIfMatch()方法中的配置方法
        if (configuratorUrl.getPort() != 0) {
            if (url.getPort() == configuratorUrl.getPort()) {
                return configureIfMatch(url.getHost(), url);
            }
        } else {
        	// 如果沒有指定端口,則該配置URL要麼是針對Consumer的,要麼是針對任意URL的(即host為0.0.0.0)
        	// 如果原始URL屬于Consumer,則使用Consumer的host進行比對
            if (url.getParameter(SIDE_KEY, PROVIDER).equals(CONSUMER)) {
                // NetUtils.getLocalHost is the ip address consumer registered to registry.
                return configureIfMatch(NetUtils.getLocalHost(), url);
            } else if (url.getParameter(SIDE_KEY, CONSUMER).equals(PROVIDER)) {
                // 如果是Provider URL,則用0.0.0.0來配置
                return configureIfMatch(ANYHOST_VALUE, url);
            }
        }
        return url;
    }
}
           

configureIfMatch() 方法會排除比對 URL 中不可動态修改的參數,并調用 Configurator 子類的 doConfigurator() 方法重寫原始 URL,具體實作如下:

public abstract class AbstractConfigurator implements Configurator {

    private final URL configuratorUrl;
	
    private URL configureIfMatch(String host, URL url) {
		// 比對host
        if (ANYHOST_VALUE.equals(configuratorUrl.getHost()) || host.equals(configuratorUrl.getHost())) {
            // TODO, to support wildcards
            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)) {// 比對application
					// 排除不能動态修改的屬性,其中包括category、check、dynamic、enabled還有以~開頭的屬性	
                    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);
							// 如果配置URL與原URL中以~開頭的參數值不相同,則不使用該配置URL重寫原URL
                            if (value != null && !ANY_VALUE.equals(value)
                                    && !value.equals(url.getParameter(key.startsWith("~") ? key.substring(1) : key))) {
                                return url;
                            }
                        }
                    }
					// 移除配置URL不支援動态配置的參數之後,調用Configurator子類的doConfigure方法重新生成URL
                    return doConfigure(url, configuratorUrl.removeParameters(conditionKeys));
                }
            }
        }
        return url;
    }
}	
           

再反過來仔細審視一下 AbstractConfigurator.configure() 方法中針對 2.7.0 版本之後動态配置的處理,其中會根據 side 參數明确判斷配置 URL 和原始 URL 屬于 Consumer 端還是 Provider 端,判斷邏輯也更加清晰。比對之後的具體替換過程同樣是調用 configureIfMatch() 方法實作的,這裡不再重複。

public class OverrideConfigurator extends AbstractConfigurator {

    public OverrideConfigurator(URL url) {
        super(url);
    }

    @Override
    public URL doConfigure(URL currentUrl, URL configUrl) {
		// 直接調用addParameters()方法,進行覆寫
        return currentUrl.addParameters(configUrl.getParameters());
    }

}
           
public class AbsentConfigurator extends AbstractConfigurator {

    public AbsentConfigurator(URL url) {
        super(url);
    }

    @Override
    public URL doConfigure(URL currentUrl, URL configUrl) {
		// 直接調用addParametersIfAbsent()方法嘗試添加參數
        return currentUrl.addParametersIfAbsent(configUrl.getParameters());
    }

}