前言
RegistryDirectory 作為一個 NotifyListener 監(jiān)聽器,RegistryDirectory 會同時監(jiān)聽注冊中心的 providers联予、routers 和 configurators 三個目錄羔沙。通過 RegistryDirectory 處理 configurators 目錄的邏輯,我們了解到 configurators 目錄中動態(tài)添加的 URL 會覆蓋 providers 目錄下注冊的 Provider URL葵腹,Dubbo 還會按照 configurators 目錄下的最新配置玩般,重新創(chuàng)建 Invoker 對象(同時會銷毀原來的 Invoker 對象)银觅。
在老版本的 Dubbo 中,可以通過服務治理控制臺向注冊中心的 configurators 目錄寫入動態(tài)配置的 URL坏为。在 Dubbo 2.7.x 版本中究驴,動態(tài)配置信息除了可以寫入注冊中心的 configurators 目錄之外,還可以寫入外部的配置中心匀伏,本文重點來看寫入注冊中心的動態(tài)配置洒忧。
首先,我們需要了解一下 configurators 目錄中 URL 都有哪些協議以及這些協議的含義够颠,然后還要知道 Dubbo 是如何解析這些 URL 得到 Configurator 對象的熙侍,以及 Configurator 是如何與已有的 Provider URL 共同作用得到實現動態(tài)更新配置的效果。
基礎協議
首先履磨,我們需要了解寫入注冊中心 configurators 中的動態(tài)配置有 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 為動態(tài)配置類型飞蹂。
dynamic=false:表示該 URL 為持久數據,即使注冊該 URL 的節(jié)點退出杨刨,該 URL 依舊會保存在注冊中心晤柄。
enabled=true:表示該 URL 的覆蓋規(guī)則已生效。
application=dubbo-demo-api-consumer:表示只對指定應用生效妖胀。如果不指定,則默認表示對所有應用都生效惠勒。
timeout=1000:表示將滿足以上條件 Provider URL 中的 timeout 參數值覆蓋為 1000赚抡。如果想覆蓋其他配置,可以直接以參數的形式添加到 override URL 之上纠屋。
在 Dubbo 的官網中涂臣,還提供了一些簡單示例,我們這里也簡單解讀一下。
- 禁用某個 Provider赁遗,通常用于臨時剔除某個 Provider 節(jié)點:
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();
}
// 創(chuàng)建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擴展,并創(chuàng)建Configurator對象
configurators.add(configuratorFactory.getConfigurator(url));
}
// 排序
Collections.sort(configurators);
return Optional.of(configurators);
}
// 排序首先按照ip進行排序燃辖,所有ip的優(yōu)先級都高于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 提供了兩個實現類妇智,如下圖所示:
其中,OverrideConfiguratorFactory 對應的擴展名為 override氏身,創(chuàng)建的 Configurator 實現是 OverrideConfigurator巍棱;AbsentConfiguratorFactory 對應的擴展名是 absent,創(chuàng)建的 Configurator 實現類是 AbsentConfigurator蛋欣。
Configurator 接口的繼承關系如下圖所示:
其中航徙,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是否為空窝稿,決定當前是否執(zhí)行后續(xù)覆蓋邏輯
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參數作為區(qū)分
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的端口踪少,
// 兩者端口相同塘安,才能執(zhí)行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 中不可動態(tài)修改的參數萝究,并調用 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
// 排除不能動態(tài)修改的屬性,其中包括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不支持動態(tài)配置的參數之后险领,調用Configurator子類的doConfigure方法重新生成URL
return doConfigure(url, configuratorUrl.removeParameters(conditionKeys));
}
}
}
return url;
}
}
再反過來仔細審視一下 AbstractConfigurator.configure() 方法中針對 2.7.0 版本之后動態(tài)配置的處理,其中會根據 side 參數明確判斷配置 URL 和原始 URL 屬于 Consumer 端還是 Provider 端秒紧,判斷邏輯也更加清晰绢陌。匹配之后的具體替換過程同樣是調用 configureIfMatch() 方法實現的,這里不再重復熔恢。
Configurator 的兩個子類實現非常簡單脐湾。在 OverrideConfigurator 的 doConfigure() 方法中,會直接用配置 URL 中剩余的全部參數叙淌,覆蓋原始 URL 中的相應參數秤掌,具體實現如下:
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());
}
}
在 AbsentConfigurator 的 doConfigure() 方法中鹰霍,會嘗試用配置 URL 中的參數添加到原始 URL 中闻鉴,如果原始 URL 中已經有了該參數是不會被覆蓋的,具體實現如下:
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());
}
}
到這里茂洒,Dubbo 2.7.0 版本之前的動態(tài)配置核心實現就介紹完了孟岛,其中也簡單涉及了 Dubbo 2.7.0 版本之后一些邏輯,只不過沒有全面介紹 Dubbo 2.7.0 之后的配置格式以及核心處理邏輯督勺。