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;
    }

整个流程结束.

四、总结

整个源码的分析可以看出,整个流程链路比较的清爽、功能划分比较清楚,什么时候该干什么,学习了一波。

Logo

权威|前沿|技术|干货|国内首个API全生命周期开发者社区

更多推荐