Dubbo-Dubbo 動態配置中心

Dubbo 動態配置中心

一、參考文檔

http://dubbo.apache.org/zh-cn/docs/user/configuration/config-center.html

三大中心指的:註冊中心,元數據中心,配置中心。
在 2.7 之前的版本,Dubbo 只配備了註冊中心,主流使用的註冊中心爲 zookeeper。新增加了元數據中心和配置中心,自然是爲了解決對應的痛點,下面我們來詳細闡釋三大中心改造的原因。

元數據改造

元數據是什麼?元數據定義爲描述數據的數據,在服務治理中,例如服務接口名,重試次數,版本號等等都可以理解爲元數據。在 2.7 之前,元數據一股腦丟在了註冊中心之中,這造成了一系列的問題:
推送量大 -> 存儲數據量大 -> 網絡傳輸量大 -> 延遲嚴重
生產者端註冊 30+ 參數,有接近一半是不需要作爲註冊中心進行傳遞;消費者端註冊 25+ 參數,只有個別需要傳遞給註冊中心。有了以上的理論分析,Dubbo 2.7 進行了大刀闊斧的改動,只將真正屬於服務治理的數據發佈到註冊中心之中,大大降低了註冊中心的負荷。
同時,將全量的元數據發佈到另外的組件中:元數據中心。元數據中心目前支持 redis(推薦),zookeeper。這也爲 Dubbo 2.7 全新的 Dubbo Admin 做了準備,關於新版的 Dubbo Admin,我將會後續準備一篇獨立的文章進行介紹。
示例:使用 zookeeper 作爲元數據中心
<dubbo:metadata-report address=“zookeeper://127.0.0.1:2181”/>

Dubbo 2.6 元數據

dubbo://30.5.120.185:20880/com.alibaba.dubbo.demo.DemoService?
anyhost=true&
application=demo-provider&
interface=com.alibaba.dubbo.demo.DemoService&
methods=sayHello&
bean.name=com.alibaba.dubbo.demo.DemoService&
dubbo=2.0.2&
executes=4500&
generic=false&
owner=kirito&
pid=84228&
retries=7&
side=provider&
timestamp=1552965771067
從本地的 zookeeper 中取出一條服務數據,通過解碼之後,可以看出,的確有很多參數是不必要。

Dubbo 2.7 元數據

在 2.7 中,如果不進行額外的配置,zookeeper 中的數據格式仍然會和 Dubbo 2.6 保持一致,這主要是爲了保證兼容性,讓 Dubbo 2.6 的客戶端可以調用 Dubbo 2.7 的服務端。如果整體遷移到 2.7,則可以爲註冊中心開啓簡化配置的參數:
<dubbo:registry address=“zookeeper://127.0.0.1:2181” simplified=“true”/>
Dubbo 將會只上傳那些必要的服務治理數據,一個簡化過後的數據如下所示:
dubbo://30.5.120.185:20880/org.apache.dubbo.demo.api.DemoService?
application=demo-provider&
dubbo=2.0.2&
release=2.7.0&
timestamp=1552975501873
對於那些非必要的服務信息,仍然全量存儲在元數據中心之中:

元數據中心的數據可以被用於服務測試,服務 MOCK 等功能。目前註冊中心配置中 simplified 的默認值爲 false,因爲考慮到了遷移的兼容問題,在後續迭代中,默認值將會改爲 true。

配置中心支持

衡量配置中心的必要性往往從三個角度出發:

  1. 分佈式配置統一管理
  2. 動態變更推送
  3. 安全性

Spring Cloud Config, Apollo, Nacos 等分佈式配置中心組件都對上述功能有不同程度的支持。在 2.7 之前的版本中,在 zookeeper 中設置了部分節點:configurators,routers,用於管理部分配置和路由信息,它們可以理解爲 Dubbo 配置中心的雛形。在 2.7 中,Dubbo 正式支持了配置中心,目前支持的幾種註冊中心 Zookeeper,Apollo,Nacos(2.7.1-release 支持)。
在 Dubbo 中,配置中心主要承擔了兩個作用

  • 外部化配置。啓動配置的集中式存儲
  • 服務治理。服務治理規則的存儲與通知

示例:使用 Zookeeper 作爲配置中心
<dubbo:config-center address=“zookeeper://127.0.0.1:2181”/>
引入配置中心後,需要注意配置項的覆蓋問題。

二、動態配置中心

何爲配置中心?配置中心即就是我們平常經常見的比如註冊中心的地址,服務的版本,服務的分組,反正在dubbo中能夠看到的一些信息都可以作爲配置中心中去存儲。官方說的這兩點比較的明確。就是一些外部化的參數配置,其實這個在其他的配置中心組件中非常常見,比如diamond 從remote獲取配置信息,然後覆蓋本地的信息。
image.png

1、直接下載官方demo工程

git clone   https://github.com/apache/dubbo-samples.git

dubbo-samples-zookeeper

spring/dubbo-provider.properties
將本地的配置修改爲配置中心的地址

dubbo.application.name=zookeeper-demo-provider
dubbo.config-center.address=zookeeper://127.0.0.1:2181

#dubbo.registry.address=zookeeper://${zookeeper.address:localhost}:2181
#dubbo.protocol.name=dubbo
#dubbo.protocol.port=20880
#dubbo.application.qosEnable=true
#dubbo.application.qosPort=33333
dubbo.application.qosAcceptForeignIp=false


2、配置管理

Dubbo admin 配置中
image.png
將一些基礎配置信息配置進去

dubbo.registry.address=zookeeper://127.0.0.1:2181 
dubbo.metadata-report.address=zookeeper://127.0.0.1:2181
dubbo.protocol.name=dubbo
dubbo.protocol.port=20880
dubbo.application.qosEnable=true
dubbo.application.qosPort=33333
dubbo.application.qosAcceptForeignIp=false

3、查看 zookeeper

image.png

4、啓動demo服務即可。

發現demo 發現日誌監聽配置的變化

12/07/19 10:35:23:023 CST] main-EventThread  INFO state.ConnectionStateManager: State change: CONNECTED
[12/07/19 10:35:23:023 CST] ZookeeperDynamicConfiguration-thread-1  INFO curator.CuratorZookeeperClient:  [DUBBO] listen the zookeeper changed. The changed data:ChildData{path='/dubbo/config', stat=427,427,1562936763476,1562936763476,0,1,0,0,0,1,428
, data=[]}, dubbo version: 2.7.2, current host: 192.168.199.112
[12/07/19 10:35:23:023 CST] ZookeeperDynamicConfiguration-thread-1  INFO curator.CuratorZookeeperClient:  [DUBBO] listen the zookeeper changed. The changed data:ChildData{path='/dubbo/config/dubbo', stat=428,428,1562936763478,1562936763478,0,1,0,0,0,1,429
, data=[]}, dubbo version: 2.7.2, current host: 192.168.199.112
[12/07/19 10:35:23:023 CST] ZookeeperDynamicConfiguration-thread-1  INFO curator.CuratorZookeeperClient:  [DUBBO] listen the zookeeper changed. The changed data:ChildData{path='/dubbo/config/dubbo/dubbo.properties', stat=429,465,1562936763479,1562940541881,4,0,0,0,267,0,429
, data=[100, 117, 98, 98, 111, 46, 114, 101, 103, 105, 115, 116, 114, 121, 46, 97, 100, 100, 114, 101, 115, 115, 61, 122, 111, 111, 107, 101, 101, 112, 101, 114, 58, 47, 47, 49, 50, 55, 46, 48, 46, 48, 46, 49, 58, 50, 49, 56, 49, 32, 10, 100, 117, 98, 98, 111, 46, 109, 101, 116, 97, 100, 97, 116, 97, 45, 114, 101, 112, 111, 114, 116, 46, 97, 100, 100, 114, 101, 115, 115, 61, 122, 111, 111, 107, 101, 101, 112, 101, 114, 58, 47, 47, 49, 50, 55, 46, 48, 46, 48, 46, 49, 58, 50, 49, 56, 49, 10, 100, 117, 98, 98, 111, 46, 112, 114, 111, 116, 111, 99, 111, 108, 46, 110, 97, 109, 101, 61, 100, 117, 98, 98, 111, 10, 100, 117, 98, 98, 111, 46, 112, 114, 111, 116, 111, 99, 111, 108, 46, 112, 111, 114, 116, 61, 50, 48, 56, 56, 48, 10, 100, 117, 98, 98, 111, 46, 97, 112, 112, 108, 105, 99, 97, 116, 105, 111, 110, 46, 113, 111, 115, 69, 110, 97, 98, 108, 101, 61, 116, 114, 117, 101, 10, 100, 117, 98, 98, 111, 46, 97, 112, 112, 108, 105, 99, 97, 116, 105, 111, 110, 46, 113, 111, 115, 80, 111, 114, 116, 61, 51, 51, 51, 51, 51, 10, 100, 117, 98, 98, 111, 46, 97, 112, 112, 108, 105, 99, 97, 116, 105, 111, 110, 46, 113, 111, 115, 65, 99, 99, 101, 112, 116, 70, 111, 114, 101, 105, 103, 110, 73, 112, 61, 102, 97, 108, 115, 101]}, dubbo version: 2.7.2, current host: 192.168.199.112
[12/07/19 10:35:23:023 CST] ZookeeperDynamicConfiguration-thread-1  INFO curator.CuratorZookeeperClient:  [DUBBO] listen the zookeeper changed. The changed data:null, dubbo version: 2.7.2, current host: 192.168.199.112

全局的額陪孩子和官方文檔相同處理邏輯。
image.png

5、實現原理

在這裏插入圖片描述

三、源碼解析

1、服務導出

監聽到spring 啓動完畢,然後服務進行導出
org.apache.dubbo.config.spring.ServiceBean#onApplicationEvent
org.apache.dubbo.config.spring.ServiceBean#export

 public void onApplicationEvent(ContextRefreshedEvent event) {
        if (!isExported() && !isUnexported()) {
            if (logger.isInfoEnabled()) {
                logger.info("The service ready on spring started. service: " + getInterface());
            }
            export();
        }
    }

org.apache.dubbo.config.ServiceConfig#export
這裏是同步的進行導出服務,先檢查配置項信息。

public synchronized void export() {
        checkAndUpdateSubConfigs();

        if (!shouldExport()) {
            return;
        }

        if (shouldDelay()) {
            delayExportExecutor.schedule(this::doExport, getDelay(), TimeUnit.MILLISECONDS);
        } else {
            doExport();
        }
    }

2、獲取配置流程

org.apache.dubbo.config.ServiceConfig#checkAndUpdateSubConfigs

  1. 獲取本地的全局配置,比如應用、註冊中心、協議、配置中心地址等等
  2. 從配置中心獲取信息
 public void checkAndUpdateSubConfigs() {
        // Use default configs defined explicitly on global configs
        completeCompoundConfigs();
        // Config Center should always being started first.
        startConfigCenter();
        checkDefault();
        checkProtocol();
        checkApplication();
        // if protocol is not injvm checkRegistry
        if (!isOnlyInJvm()) {
            checkRegistry();
        }
        this.refresh();
        checkMetadataReport();

        if (StringUtils.isEmpty(interfaceName)) {
            throw new IllegalStateException("<dubbo:service interface=\"\" /> interface not allow null!");
        }

        if (ref instanceof GenericService) {
            interfaceClass = GenericService.class;
            if (StringUtils.isEmpty(generic)) {
                generic = Boolean.TRUE.toString();
            }
        } else {
            try {
                interfaceClass = Class.forName(interfaceName, true, Thread.currentThread()
                        .getContextClassLoader());
            } catch (ClassNotFoundException e) {
                throw new IllegalStateException(e.getMessage(), e);
            }
            checkInterfaceAndMethods(interfaceClass, methods);
            checkRef();
            generic = Boolean.FALSE.toString();
        }
        if (local != null) {
            if ("true".equals(local)) {
                local = interfaceName + "Local";
            }
            Class<?> localClass;
            try {
                localClass = ClassUtils.forNameWithThreadContextClassLoader(local);
            } catch (ClassNotFoundException e) {
                throw new IllegalStateException(e.getMessage(), e);
            }
            if (!interfaceClass.isAssignableFrom(localClass)) {
                throw new IllegalStateException("The local implementation class " + localClass.getName() + " not implement interface " + interfaceName);
            }
        }
        if (stub != null) {
            if ("true".equals(stub)) {
                stub = interfaceName + "Stub";
            }
            Class<?> stubClass;
            try {
                stubClass = ClassUtils.forNameWithThreadContextClassLoader(stub);
            } catch (ClassNotFoundException e) {
                throw new IllegalStateException(e.getMessage(), e);
            }
            if (!interfaceClass.isAssignableFrom(stubClass)) {
                throw new IllegalStateException("The stub implementation class " + stubClass.getName() + " not implement interface " + interfaceName);
            }
        }
        checkStubAndLocal(interfaceClass);
        checkMock(interfaceClass);
    }

3、獲取全局配置信息

org.apache.dubbo.config.ServiceConfig#completeCompoundConfigs

private void completeCompoundConfigs() {
        if (provider != null) {
            if (application == null) {
                setApplication(provider.getApplication());
            }
            if (module == null) {
                setModule(provider.getModule());
            }
            if (registries == null) {
                setRegistries(provider.getRegistries());
            }
            if (monitor == null) {
                setMonitor(provider.getMonitor());
            }
            if (protocols == null) {
                setProtocols(provider.getProtocols());
            }
            if (configCenter == null) {
                setConfigCenter(provider.getConfigCenter());
            }
        }
        if (module != null) {
            if (registries == null) {
                setRegistries(module.getRegistries());
            }
            if (monitor == null) {
                setMonitor(module.getMonitor());
            }
        }
        if (application != null) {
            if (registries == null) {
                setRegistries(application.getRegistries());
            }
            if (monitor == null) {
                setMonitor(application.getMonitor());
            }
        }

3、獲取配置中心

org.apache.dubbo.config.AbstractInterfaceConfig#startConfigCenter
環境信息準備是重點。

 void startConfigCenter() {
       ## 配置中心配置是否存在
        if (configCenter == null) {
            ConfigManager.getInstance().getConfigCenter().ifPresent(cc -> this.configCenter = cc);
        }

        if (this.configCenter != null) {
            // TODO there may have duplicate refresh
            this.configCenter.refresh();
            ## 準備獲環境信息
            prepareEnvironment();
        }
        ConfigManager.getInstance().refreshAll();
    }

4、環境信息準備

org.apache.dubbo.config.AbstractInterfaceConfig#prepareEnvironment
image.png

動態獲取配置中心的實現,然後獲取到全局配置內容,獲取應用配置內容,然後刷新到本地的環境變量中去。

private void prepareEnvironment() {
        if (configCenter.isValid()) {
            if (!configCenter.checkOrUpdateInited()) {
                return;
            }
            ## 獲取配置中心實現類
            DynamicConfiguration dynamicConfiguration = getDynamicConfiguration(configCenter.toUrl());
            
            ## 獲取常量
            String configContent = dynamicConfiguration.getConfigs(configCenter.getConfigFile(), configCenter.getGroup());

            String appGroup = application != null ? application.getName() : null;
            String appConfigContent = null;
            if (StringUtils.isNotEmpty(appGroup)) {
                appConfigContent = dynamicConfiguration.getConfigs
                        (StringUtils.isNotEmpty(configCenter.getAppConfigFile()) ? configCenter.getAppConfigFile() : configCenter.getConfigFile(),
                         appGroup
                        );
            }
            try {
                Environment.getInstance().setConfigCenterFirst(configCenter.isHighestPriority());
                Environment.getInstance().updateExternalConfigurationMap(parseProperties(configContent));
                Environment.getInstance().updateAppExternalConfigurationMap(parseProperties(appConfigContent));
            } catch (IOException e) {
                throw new IllegalStateException("Failed to parse configurations from Config Center.", e);
            }
        }
    }

5、SPI 動態獲取配置工廠

org.apache.dubbo.config.AbstractInterfaceConfig#getDynamicConfiguration


    private DynamicConfiguration getDynamicConfiguration(URL url) {
        DynamicConfigurationFactory factories = ExtensionLoader
                .getExtensionLoader(DynamicConfigurationFactory.class)
                .getExtension(url.getProtocol());
        DynamicConfiguration configuration = factories.getDynamicConfiguration(url);
        Environment.getInstance().setDynamicConfiguration(configuration);
        return configuration;
    }

image.png

6、ZookeeperDynamicConfigurationFactory 工廠的實現

org.apache.dubbo.configcenter.support.zookeeper.ZookeeperDynamicConfigurationFactory
給予了註冊的地址
image.png

public class ZookeeperDynamicConfigurationFactory extends AbstractDynamicConfigurationFactory {

    private ZookeeperTransporter zookeeperTransporter;

    public void setZookeeperTransporter(ZookeeperTransporter zookeeperTransporter) {
        this.zookeeperTransporter = zookeeperTransporter;
    }


    @Override
    protected DynamicConfiguration createDynamicConfiguration(URL url) {
        return new ZookeeperDynamicConfiguration(url, zookeeperTransporter);
    }
}

7、 ZookeeperDynamicConfiguration連接

org.apache.dubbo.configcenter.support.zookeeper.ZookeeperDynamicConfiguration#ZookeeperDynamicConfiguration
這裏監聽的路徑啓動

ZookeeperDynamicConfiguration(URL url, ZookeeperTransporter zookeeperTransporter) {
        this.url = url;
        rootPath = "/" + url.getParameter(CONFIG_NAMESPACE_KEY, DEFAULT_GROUP) + "/config";

        initializedLatch = new CountDownLatch(1);
        this.cacheListener = new CacheListener(rootPath, initializedLatch);
        this.executor = Executors.newFixedThreadPool(1, new NamedThreadFactory(this.getClass().getSimpleName(), true));

        zkClient = zookeeperTransporter.connect(url);
        zkClient.addDataListener(rootPath, cacheListener, executor);
        try {
            // Wait for connection
            this.initializedLatch.await();
        } catch (InterruptedException e) {
            logger.warn("Failed to build local cache for config center (zookeeper)." + url);
        }
    }

8、環境信息準備–>獲取到配置信息

org.apache.dubbo.config.AbstractInterfaceConfig#prepareEnvironment
配置文件、分組都是URL中的參數

 private void prepareEnvironment() {
        if (configCenter.isValid()) {
            if (!configCenter.checkOrUpdateInited()) {
                return;
            }
            DynamicConfiguration dynamicConfiguration = getDynamicConfiguration(configCenter.toUrl());
            String configContent = dynamicConfiguration.getConfigs(configCenter.getConfigFile(), configCenter.getGroup());

            String appGroup = application != null ? application.getName() : null;
            String appConfigContent = null;
            if (StringUtils.isNotEmpty(appGroup)) {
                appConfigContent = dynamicConfiguration.getConfigs
                        (StringUtils.isNotEmpty(configCenter.getAppConfigFile()) ? configCenter.getAppConfigFile() : configCenter.getConfigFile(),
                         appGroup
                        );
            }
            try {
                Environment.getInstance().setConfigCenterFirst(configCenter.isHighestPriority());
                Environment.getInstance().updateExternalConfigurationMap(parseProperties(configContent));
                Environment.getInstance().updateAppExternalConfigurationMap(parseProperties(appConfigContent));
            } catch (IOException e) {
                throw new IllegalStateException("Failed to parse configurations from Config Center.", e);
            }
        }
    }

9 讀取配置文件

org.apache.dubbo.common.config.ConfigurationUtils#parseProperties

public static Map<String, String> parseProperties(String content) throws IOException {
        Map<String, String> map = new HashMap<>();
        if (StringUtils.isEmpty(content)) {
            logger.warn("You specified the config centre, but there's not even one single config item in it.");
        } else {
            Properties properties = new Properties();
            properties.load(new StringReader(content));
            properties.stringPropertyNames().forEach(
                    k -> map.put(k, properties.getProperty(k))
            );
        }
        return map;
    }

整個流程結束.

四、總結

整個源碼的分析可以看出,整個流程鏈路比較的清爽、功能劃分比較清楚,什麼時候該幹什麼,學習了一波。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章