Dubbo——動態(tài)配置

前言

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 提供了兩個實現類妇智,如下圖所示:


ConfiguratorFactory 繼承關系圖

其中,OverrideConfiguratorFactory 對應的擴展名為 override氏身,創(chuàng)建的 Configurator 實現是 OverrideConfigurator巍棱;AbsentConfiguratorFactory 對應的擴展名是 absent,創(chuàng)建的 Configurator 實現類是 AbsentConfigurator蛋欣。

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


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 之后的配置格式以及核心處理邏輯督勺。

?著作權歸作者所有,轉載或內容合作請聯系作者
  • 序言:七十年代末渠羞,一起剝皮案震驚了整個濱河市,隨后出現的幾起案子智哀,更是在濱河造成了極大的恐慌堵未,老刑警劉巖,帶你破解...
    沈念sama閱讀 212,816評論 6 492
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件盏触,死亡現場離奇詭異,居然都是意外死亡,警方通過查閱死者的電腦和手機赞辩,發(fā)現死者居然都...
    沈念sama閱讀 90,729評論 3 385
  • 文/潘曉璐 我一進店門雌芽,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人辨嗽,你說我怎么就攤上這事世落。” “怎么了糟需?”我有些...
    開封第一講書人閱讀 158,300評論 0 348
  • 文/不壞的土叔 我叫張陵屉佳,是天一觀的道長。 經常有香客問我洲押,道長武花,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 56,780評論 1 285
  • 正文 為了忘掉前任杈帐,我火速辦了婚禮体箕,結果婚禮上,老公的妹妹穿的比我還像新娘挑童。我一直安慰自己累铅,他們只是感情好,可當我...
    茶點故事閱讀 65,890評論 6 385
  • 文/花漫 我一把揭開白布站叼。 她就那樣靜靜地躺著娃兽,像睡著了一般。 火紅的嫁衣襯著肌膚如雪尽楔。 梳的紋絲不亂的頭發(fā)上投储,一...
    開封第一講書人閱讀 50,084評論 1 291
  • 那天,我揣著相機與錄音翔试,去河邊找鬼轻要。 笑死,一個胖子當著我的面吹牛垦缅,可吹牛的內容都是我干的冲泥。 我是一名探鬼主播,決...
    沈念sama閱讀 39,151評論 3 410
  • 文/蒼蘭香墨 我猛地睜開眼壁涎,長吁一口氣:“原來是場噩夢啊……” “哼凡恍!你這毒婦竟也來了?” 一聲冷哼從身側響起怔球,我...
    開封第一講書人閱讀 37,912評論 0 268
  • 序言:老撾萬榮一對情侶失蹤嚼酝,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后竟坛,有當地人在樹林里發(fā)現了一具尸體闽巩,經...
    沈念sama閱讀 44,355評論 1 303
  • 正文 獨居荒郊野嶺守林人離奇死亡钧舌,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內容為張勛視角 年9月15日...
    茶點故事閱讀 36,666評論 2 327
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發(fā)現自己被綠了涎跨。 大學時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片洼冻。...
    茶點故事閱讀 38,809評論 1 341
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖隅很,靈堂內的尸體忽然破棺而出撞牢,到底是詐尸還是另有隱情,我是刑警寧澤叔营,帶...
    沈念sama閱讀 34,504評論 4 334
  • 正文 年R本政府宣布屋彪,位于F島的核電站,受9級特大地震影響绒尊,放射性物質發(fā)生泄漏畜挥。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點故事閱讀 40,150評論 3 317
  • 文/蒙蒙 一垒酬、第九天 我趴在偏房一處隱蔽的房頂上張望砰嘁。 院中可真熱鬧,春花似錦勘究、人聲如沸矮湘。這莊子的主人今日做“春日...
    開封第一講書人閱讀 30,882評論 0 21
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽缅阳。三九已至,卻和暖如春景描,著一層夾襖步出監(jiān)牢的瞬間十办,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 32,121評論 1 267
  • 我被黑心中介騙來泰國打工超棺, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留向族,地道東北人。 一個月前我還...
    沈念sama閱讀 46,628評論 2 362
  • 正文 我出身青樓棠绘,卻偏偏與公主長得像件相,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子氧苍,可洞房花燭夜當晚...
    茶點故事閱讀 43,724評論 2 351

推薦閱讀更多精彩內容

  • 開篇 覆蓋規(guī)則是Dubbo設計的在無需重啟應用的情況下夜矗,動態(tài)調整RPC調用行為的一種能力。 在Dubbo2.6及更...
    晴天哥_王志閱讀 2,476評論 2 1
  • 前言 本文繼續(xù)分析dubbo的cluster層让虐,此層封裝多個提供者的路由及負載均衡紊撕,并橋接注冊中心,以Invoke...
    Java大生閱讀 987評論 0 0
  • 先看官網兩張圖【引用來自官網】:image.png 官網說明: 1.首先 ReferenceConfig 類的 i...
    致慮閱讀 1,024評論 0 2
  • 要了解服務導出做了什么赡突,需要了解導出的目的是什么对扶?dubbo是一款面向接口代理的高性能RPC調用区赵,說白了就是提供遠...
    loveFXX閱讀 976評論 0 0
  • dubbo隨筆 一.產生背景: 2001年左右 阿里互聯網的發(fā)展,網站應用的規(guī)模不斷擴大辩稽,常規(guī)的垂直應用架構已無法...
    柳淼_1034閱讀 243評論 0 0