Dubbo笔记 ㉔ :元数据中心

一、前言

本系列为个人Dubbo学习笔记,内容基于《深度剖析Apache Dubbo 核心技术内幕》, 过程参考官方源码分析文章,仅用于个人笔记记录。本文分析基于Dubbo2.7.0版本,由于个人理解的局限性,若文中不免出现错误,感谢指正。

系列文章地址:Dubbo源码分析:全集整理


关于元数据中心的介绍,建议详参: https://www.freesion.com/article/3146497596/


三大中心指的:注册中心,元数据中心,配置中心。

在 2.7 之前的版本,Dubbo 只配备了注册中心,主流使用的注册中心为 zookeeper。新增加了元数据中心和配置中心,自然是为了解决对应的痛点,下面我们来详细阐释三大中心改造的原因。

元数据是什么?元数据定义为描述数据的数据,在服务治理中,例如服务接口名,重试次数,版本号等等都可以理解为元数据。在 2.7 之前,元数据一股脑丢在了注册中心之中,这造成了一系列的问题:

推送量大 -> 存储数据量大 -> 网络传输量大 -> 延迟严重

生产者端注册 30+ 参数,有接近一半是不需要作为注册中心进行传递;消费者端注册 25+ 参数,只有个别需要传递给注册中心。有了以上的理论分析,Dubbo 2.7 进行了大刀阔斧的改动,只将真正属于服务治理的数据发布到注册中心之中,大大降低了注册中心的负荷。

同时,将全量的元数据发布到另外的组件中:元数据中心。元数据中心目前支持 redis(推荐),zookeeper。


简单来说,在之前的 Dubbo 版本中,当服务配置变更后,会向消费者和提供者推送变更后的服务信息,而这里的推送是全量推送。但推送的数据中有大量数据无需推送,而这些数据的推送会造成服务变更后的推送量很大,进而可能造成网络延迟等问题。

二、流程概述

我们以下面的 Main 方法形式来看一下 元数据中心做了什么。

    public static void main(String[] args) throws IOException {
    	// 构建一个常规的 ServiceConfig
        ServiceConfig<SimpleDemoService> serviceConfig = DubboUtil.serviceConfig("dubbo-provider", SimpleDemoService.class, new MainSimpleDemoServiceImpl());
        serviceConfig.getRegistry().setSimplified(true);
		// 配置元数据中心
        MetadataReportConfig metadataReportConfig = new MetadataReportConfig();
        metadataReportConfig.setAddress("zookeeper://127.0.0.1:2181");
        serviceConfig.setMetadataReportConfig(metadataReportConfig);

		// 配置配置中心
        ConfigCenterConfig configCenterConfig = new ConfigCenterConfig();
        configCenterConfig.setAddress("zookeeper://127.0.0.1:2181");
        serviceConfig.setConfigCenter(configCenterConfig);
		// 服务暴露
        serviceConfig.export();
        System.out.println("service is start");
        // 阻塞服务,防止程序执行直接关闭
        System.in.read();
    }

1. 提供者

对于提供者,在其服务暴露过程中,有下面几个场景会涉及到元数据中心。

  1. 服务刚开始暴露时会在 ServiceConfig#checkAndUpdateSubConfigs 方法中对配置信息进行检查和更新, 其中调用 AbstractInterfaceConfig#checkMetadataReport 来检查 元数据中心的情况。如下图:
    在这里插入图片描述

    AbstractInterfaceConfig#checkMetadataReport 的实现如下:

        protected void checkMetadataReport() {
            // 如果元数据中心没有配置,则配置一个默认的
            if (metadataReportConfig == null) {
                setMetadataReportConfig(new MetadataReportConfig());
            }
            // 从配置中心获取动态配置刷新本地
            metadataReportConfig.refresh();
            // 校验 元数据中心的 地址是否为空
            if (!metadataReportConfig.isValid()) {
                logger.warn("There's no valid metadata config found, if you are using the simplified mode of registry url, " +
                        "please make sure you have a metadata address configured properly.");
            }
        }
    
  2. 当服务开始进行暴露后会在 ServiceConfig#doExportUrlsFor1Protocol 方法中将元数据写入到元数据中心去。这里的内容我们下面具体分析,如下图:
    在这里插入图片描述

  3. 在 RegistryProtocol 暴露服务时,会通过 RegistryProtocol#getRegisteredProviderUrl 来服务服务 URL,此时RegistryProtocol#getRegisteredProviderUrl 会判断是否开启了简化参数,如果开启了简化参数,则返回一个简化后的提供者URL供提供者注册。如下图:
    在这里插入图片描述
    RegistryProtocol#getRegisteredProviderUrl 实现如下:

        private URL getRegisteredProviderUrl(final URL providerUrl, final URL registryUrl) {
            // 开启简化URL
            if (!registryUrl.getParameter(SIMPLIFIED_KEY, false)) {
                return providerUrl.removeParameters(getFilteredKeys(providerUrl)).removeParameters(
                        MONITOR_KEY, BIND_IP_KEY, BIND_PORT_KEY, QOS_ENABLE, QOS_PORT, ACCEPT_FOREIGN_IP, VALIDATION_KEY,
                        INTERFACES);
            } else {
                String[] paramsToRegistry = getParamsToRegistry(DEFAULT_REGISTER_PROVIDER_KEYS,
                        registryUrl.getParameter(EXTRA_KEYS_KEY, new String[0]));
                return URL.valueOf(providerUrl, paramsToRegistry, providerUrl.getParameter(METHODS_KEY, (String[]) null));
            }
    
        }
    

2. 消费者

消费者涉及元数据中心的场景与提供者类似。

  1. 消费者在获取提供者代理类时会通过 ReferenceConfig#checkAndUpdateSubConfigs 方法中对配置信息进行检查和更新。其中会调用 AbstractInterfaceConfig#checkMetadataReport 检查元数据中心的配置。如下图:在这里插入图片描述
    AbstractInterfaceConfig#checkMetadataReport 的代码实现和提供者相同,这里不再赘述。

  2. 在通过 ReferenceConfig#createProxy 创建提供者代理类的时候会将元数据写入到元数据中心去,这部分内容下面会详细分析。如下图:在这里插入图片描述

  3. 在 RegistryProtocol 引用提供者服务时,如果消费者配置了注册到注册中心(register 为 true),会通过 RegistryProtocol#getRegisteredConsumerUrl 来获取消费者的 URL,此时RegistryProtocol#getRegisteredConsumerUrl 会判断是否开启了简化参数,如果开启了简化参数,则返回一个简化后的消费者URL供消费者注册。如下图:
    在这里插入图片描述
    而 RegistryProtocol#getRegisteredConsumerUrl 实现如下:

        private URL getRegisteredConsumerUrl(final URL consumerUrl, URL registryUrl) {
        	// 如果开启了简化,则返回简化后 URl
            if (!registryUrl.getParameter(SIMPLIFIED_KEY, false)) {
                return consumerUrl.addParameters(CATEGORY_KEY, CONSUMERS_CATEGORY,
                        CHECK_KEY, String.valueOf(false));
            } else {
                return URL.valueOf(consumerUrl, DEFAULT_REGISTER_CONSUMER_KEYS, null).addParameters(
                        CATEGORY_KEY, CONSUMERS_CATEGORY, CHECK_KEY, String.valueOf(false));
            }
        }
    

二、元数据中心服务发布

上面对提供者和消费者流程中的元数据中心进行了概述。我们可以了解到:

  • 提供者通过 MetadataReportService#publishProvider 发布到元数据中心
  • 消费者t通过 MetadataReportService#publishConsumer 发布到元数据中心

这里我们来看看,服务是怎么发布到元数据中心的。

1. 提供者的发布

提供者 在 ServiceConfig#doExportUrlsFor1Protocol 方法中通过如下代码将提供者的信息写入到元数据中心 :

        MetadataReportService metadataReportService = null;
        // 获取元数据中心服务
        if ((metadataReportService = getMetadataReportService()) != null) {
        	// 发布当前提供者信息
            metadataReportService.publishProvider(url);
        }

		// AbstractInterfaceConfig#getMetadataReportService
	    protected MetadataReportService getMetadataReportService() {
	
	        if (metadataReportConfig == null || !metadataReportConfig.isValid()) {
	            return null;
	        }
	        // 将配置转化为 Url 再转化为 MetadataReportService 
	        return MetadataReportService.instance(this::loadMetadataReporterURL);
	    }

其中 metadataReportService.publishProvider(url); 其实现为 MetadataReportService#publishProvider,如下:

    public void publishProvider(URL providerUrl) throws RpcException {
        //first add into the list
        // 移除部分不需要的参数
        providerUrl = providerUrl.removeParameters(Constants.PID_KEY, Constants.TIMESTAMP_KEY, Constants.BIND_IP_KEY, Constants.BIND_PORT_KEY, Constants.TIMESTAMP_KEY);

        try {
        	// 获取接口名
            String interfaceName = providerUrl.getParameter(Constants.INTERFACE_KEY);
            if (StringUtils.isNotEmpty(interfaceName)) {
            	// 通过反射获取接口类
                Class interfaceClass = Class.forName(interfaceName);
                // 1. 获取完整服务定义。这里会将 接口所拥有的方法以及方法的参数解析出来。
                FullServiceDefinition fullServiceDefinition = ServiceDefinitionBuilder.buildFullDefinition(interfaceClass, providerUrl.getParameters());
                // 2. 保存接口元数据
                metadataReport.storeProviderMetadata(new MetadataIdentifier(providerUrl.getServiceInterface(),
                        providerUrl.getParameter(Constants.VERSION_KEY), providerUrl.getParameter(Constants.GROUP_KEY),
                        Constants.PROVIDER_SIDE,providerUrl.getParameter(Constants.APPLICATION_KEY)), fullServiceDefinition);
                return;
            }
            logger.error("publishProvider interfaceName is empty . providerUrl: " + providerUrl.toFullString());
        } catch (ClassNotFoundException e) {
            //ignore error
            logger.error("publishProvider getServiceDescriptor error. providerUrl: " + providerUrl.toFullString(), e);
        }
    }

这里逻辑比较简单,我们这里只需要注意下面两点:

  1. ServiceDefinitionBuilder.buildFullDefinition 生成了完整的服务数据。这里会解析 服务接口,并获取所有的方法和参数保存到元数据中。借由此,dubbo-admin 可以完成方法测试,如下图:
    在这里插入图片描述

  2. metadataReport.storeProviderMetadata : 将服务信息保存到元数据中心上。这里需要注意 metadataReport 是从 MetadataReportFactory 中获取,而 MetadataReportFactory 接口是 SPI 接口。而 MetadataReportFactory 有 ZookeeperMetadataReportFactory 和 RedisMetadataReportFactory 两个实现,默认为 redis。
    在这里插入图片描述


2. 消费者的发布

消费者的发布流程和提供者基本相同,在 ReferenceConfig#createProxy 创建代理对象时会将消费者注册到元数据中心。如下

        MetadataReportService metadataReportService = null;
        	// 获取元数据服务
        if ((metadataReportService = getMetadataReportService()) != null) {
            URL consumerURL = new URL(Constants.CONSUMER_PROTOCOL, map.remove(Constants.REGISTER_IP_KEY), 0, map.get(Constants.INTERFACE_KEY), map);
            // 发布当前消费者
            metadataReportService.publishConsumer(consumerURL);
        }

MetadataReportService#publishConsumer 实现如下:

    public void publishConsumer(URL consumerURL) throws RpcException {
    	// 消费者移除不需要的属性
        consumerURL = consumerURL.removeParameters(Constants.PID_KEY, Constants.TIMESTAMP_KEY, Constants.BIND_IP_KEY, Constants.BIND_PORT_KEY, Constants.TIMESTAMP_KEY);
        // 交由 metadataReport 来发布
        metadataReport.storeConsumerMetadata(new MetadataIdentifier(consumerURL.getServiceInterface(),
                consumerURL.getParameter(Constants.VERSION_KEY), consumerURL.getParameter(Constants.GROUP_KEY),Constants.CONSUMER_SIDE,
                consumerURL.getParameter(Constants.APPLICATION_KEY)), consumerURL.getParameters());
    }

这里我们发现,提供者和消费者都将服务发布委托给了 MetadataReport 来完成。而 MetadataReport 来源于 MetadataReportFactory 。上面我们提到 MetadataReportFactory 有两个实现类 ZookeeperMetadataReportFactory 和 RedisMetadataReportFactory。自然 MetadataReport也存在对应的 RedisMetadataReport 和 ZookeeperMetadataReport。

下面我们来看看 MetadataReport 的实现 。

3. MetadataReport

MetadataReport 结构如下:
在这里插入图片描述

这里可以看到 RedisMetadataReport 和 ZookeeperMetadataReport 都 “师承” 于 AbstractMetadataReport ,因此下面我们看一下 AbstractMetadataReport 的构造函数

3.1 AbstractMetadataReport 的构造

AbstractMetadataReport 的构造函数中完成了两件事:

  1. 在本地创建元数据存储文件;
  2. 创建元数据更新的定时器按照一定的频率将元数据更新到元数据中心
    public AbstractMetadataReport(URL reportServerURL) {
        setUrl(reportServerURL);
        // Start file save timer
        // 创建本地文件用来存储元数据
        // 如果在MetadataReportConfig.parameters中配置了file参数,则使用它作为文件名
        String filename = reportServerURL.getParameter(Constants.FILE_KEY, System.getProperty("user.home") + "/.dubbo/dubbo-metadata-" + reportServerURL.getParameter(Constants.APPLICATION_KEY) + "-" + reportServerURL.getAddress() + ".cache");
        File file = null;
        if (ConfigUtils.isNotEmpty(filename)) {
            file = new File(filename);
            if (!file.exists() && file.getParentFile() != null && !file.getParentFile().exists()) {
                if (!file.getParentFile().mkdirs()) {
                    throw new IllegalArgumentException("Invalid service store file " + file + ", cause: Failed to create directory " + file.getParentFile() + "!");
                }
            }
            // if this file exist, firstly delete it.
            if (!INIT.getAndSet(true) && file.exists()) {
            	// 第一次启动的时候将文件删除
                file.delete();
            }
        }
        this.file = file;
        loadProperties();
    
        syncReport = reportServerURL.getParameter(Constants.SYNC_REPORT_KEY, false);
        
        // 当访问元数据中心失败时,MetadataReportRetry设置了两个与重试相关的参数 :  1、重试次数;2、多长时间重试一次
        metadataReportRetry = new MetadataReportRetry(reportServerURL.getParameter(Constants.RETRY_TIMES_KEY, Constants.DEFAULT_METADATA_REPORT_RETRY_TIMES),
                reportServerURL.getParameter(Constants.RETRY_PERIOD_KEY, Constants.DEFAULT_METADATA_REPORT_RETRY_PERIOD));
        // cycle report the data switch
        // 参数cycle.report表示是否按照一定的频率将元数据更新到元数据中心,默认是true
        if (reportServerURL.getParameter(Constants.CYCLE_REPORT_KEY, Constants.DEFAULT_METADATA_REPORT_CYCLE_REPORT)) {
        	// 使用定时器按照一定的频率将元数据更新到元数据中心
            ScheduledExecutorService scheduler = Executors.newSingleThreadScheduledExecutor(new NamedThreadFactory("DubboMetadataReportTimer", true));
            scheduler.scheduleAtFixedRate(new Runnable() {
                @Override
                public void run() {
                	// 推送数据到元数据中心
                    publishAll();
                }
            }, calculateStartTime(), ONE_DAY_IN_MIll, TimeUnit.MILLISECONDS);
        }
    }

我们这里看一下 publishAll 的实现,如下:

    void publishAll() {
        logger.info("start to publish all metadata.");
        this.doHandleMetadataCollection(allMetadataReports);
    }
    
    private boolean doHandleMetadataCollection(Map<MetadataIdentifier, Object> metadataMap) {
        if (metadataMap.isEmpty()) {
            return true;
        }
        // 推送元数据到元数据中心
        Iterator<Map.Entry<MetadataIdentifier, Object>> iterable = metadataMap.entrySet().iterator();
        while (iterable.hasNext()) {
            Map.Entry<MetadataIdentifier, Object> item = iterable.next();
            // 区分提供者和消费者。
            if (Constants.PROVIDER_SIDE.equals(item.getKey().getSide())) {
                this.storeProviderMetadata(item.getKey(), (FullServiceDefinition) item.getValue());
            } else if (Constants.CONSUMER_SIDE.equals(item.getKey().getSide())) {
                this.storeConsumerMetadata(item.getKey(), (Map) item.getValue());
            }

        }
        return false;
    }

3.2 AbstractMetadataReport#storeProviderMetadata

AbstractMetadataReport#storeProviderMetadata 用来存储 提供者的元数据信息,其实现如下 :

	// AbstractMetadataReport#storeProviderMetadata
    public void storeProviderMetadata(MetadataIdentifier providerMetadataIdentifier, FullServiceDefinition serviceDefinition) {
    	// 如果同步发布,则直接发布
        if (syncReport) {
            storeProviderMetadataTask(providerMetadataIdentifier, serviceDefinition);
        } else { // 否则为异步发布,则交由线程池执行
            reportCacheExecutor.execute(new Runnable() {
                @Override
                public void run() {
                    storeProviderMetadataTask(providerMetadataIdentifier, serviceDefinition);
                }
            });
        }
    }

    private void storeProviderMetadataTask(MetadataIdentifier providerMetadataIdentifier, FullServiceDefinition serviceDefinition) {
        try {
			// 保存发布到元数据中心的服务
            allMetadataReports.put(providerMetadataIdentifier, serviceDefinition);
            // 从失败列表移除
            failedReports.remove(providerMetadataIdentifier);
            Gson gson = new Gson();
            String data = gson.toJson(serviceDefinition);
            // 保存到元数据中心
            doStoreProviderMetadata(providerMetadataIdentifier, data);
            // 本地保存属性
            saveProperties(providerMetadataIdentifier, data, true, !syncReport);
        } catch (Exception e) {
            // retry again. If failed again, throw exception.
            // 如果失败,放入失败集合中重试
            failedReports.put(providerMetadataIdentifier, serviceDefinition);
            metadataReportRetry.startRetryTask();
            logger.error("Failed to put provider metadata " + providerMetadataIdentifier + " in  " + serviceDefinition + ", cause: " + e.getMessage(), e);
        }
    }

上面的 doStoreProviderMetadata(providerMetadataIdentifier, data); 交由子类来实现, 我们这里以zk作为元数据中心,看一下其实现 ZookeeperMetadataReport#doStoreProviderMetadata :

    @Override
    protected void doStoreProviderMetadata(MetadataIdentifier providerMetadataIdentifier, String serviceDefinitions) {
        storeMetadata(providerMetadataIdentifier, serviceDefinitions);
    }
    
    private void storeMetadata(MetadataIdentifier metadataIdentifier, String v) {
        zkClient.create(getNodePath(metadataIdentifier), v, false);
    }
    
    String getNodePath(MetadataIdentifier metadataIdentifier) {
        return toRootDir() + metadataIdentifier.getUniqueKey(MetadataIdentifier.KeyTypeEnum.PATH) + Constants.PATH_SEPARATOR + METADATA_NODE_NAME;
    }

可以看到,ZookeeperMetadataReport#doStoreProviderMetadata 保存元数据的方式是在 zk上创建节点,如下结构:
在这里插入图片描述
节点数据结构如下 :

{
    "parameters": {
        "side": "provider",
        "application": "dubbo-provider",
        "release": "2.7.0",
        "methods": "sayHello,sayHello2",
        "dubbo": "2.0.2",
        "interface": "com.kingfish.service.impl.SimpleDemoService",
        "version": "1.0.0",
        "generic": "false",
        "revision": "1.0.0",
        "group": "dubbo",
        "anyhost": "true"
    },
    "canonicalName": "com.kingfish.service.impl.SimpleDemoService",
    "codeSource": "file:/E:/HKingFish/dubbo-demo/common-dubbo-sdk/target/classes/",
    "methods": [
        {
            "name": "sayHello2",
            "parameterTypes": [
                "java.lang.String"
            ],
            "returnType": "java.util.List<java.lang.String>"
        },
        {
            "name": "sayHello",
            "parameterTypes": [
                "java.lang.String"
            ],
            "returnType": "java.lang.String"
        }
    ],
    "types": [
        {
            "type": "char"
        },
        {
            "type": "int"
        },
        {
            "type": "java.lang.String",
            "properties": {
                "value": {
                    "type": "char[]"
                },
                "hash": {
                    "type": "int"
                }
            }
        }
    ]
}

3.3 AbstractMetadataReport#storeConsumerMetadata

AbstractMetadataReport#storeConsumerMetadataAbstractMetadataReport#storeProviderMetadata 的实现基本相同,作用则是将 消费者的元数据保存到元数据中心。

其实现如下:

    public void storeConsumerMetadata(MetadataIdentifier consumerMetadataIdentifier, Map<String, String> serviceParameterMap) {
    	// 同步发布
        if (syncReport) {
            storeConsumerMetadataTask(consumerMetadataIdentifier, serviceParameterMap);
        } else {
        	// 异步发布
            reportCacheExecutor.execute(new Runnable() {
                @Override
                public void run() {
                    storeConsumerMetadataTask(consumerMetadataIdentifier, serviceParameterMap);
                }
            });
        }
    }

    public void storeConsumerMetadataTask(MetadataIdentifier consumerMetadataIdentifier, Map<String, String> serviceParameterMap) {
        try {
        	// 将消费者的元数据信息保存起来
            allMetadataReports.put(consumerMetadataIdentifier, serviceParameterMap);
            // 移除失败记录
            failedReports.remove(consumerMetadataIdentifier);

            Gson gson = new Gson();
            String data = gson.toJson(serviceParameterMap);
            // 元数据写入到元数据中心
            doStoreConsumerMetadata(consumerMetadataIdentifier, data);
            saveProperties(consumerMetadataIdentifier, data, true, !syncReport);
        } catch (Exception e) {
            // retry again. If failed again, throw exception.
            failedReports.put(consumerMetadataIdentifier, serviceParameterMap);
            metadataReportRetry.startRetryTask();
            logger.error("Failed to put consumer metadata " + consumerMetadataIdentifier + ";  " + serviceParameterMap + ", cause: " + e.getMessage(), e);
        }
    }

我们以 zk 作为元数据中心,所以这里看一下 ZookeeperMetadataReport#doStoreConsumerMetadata 的实现如下:

    @Override
    protected void doStoreConsumerMetadata(MetadataIdentifier consumerMetadataIdentifier, String value) {
        storeMetadata(consumerMetadataIdentifier, value);
    }
	// zk 上创建节点。
    private void storeMetadata(MetadataIdentifier metadataIdentifier, String v) {
        zkClient.create(getNodePath(metadataIdentifier), v, false);
    }

    String getNodePath(MetadataIdentifier metadataIdentifier) {
        return toRootDir() + metadataIdentifier.getUniqueKey(MetadataIdentifier.KeyTypeEnum.PATH) + Constants.PATH_SEPARATOR + METADATA_NODE_NAME;
    }

以上:内容部分参考
https://mp.weixin.qq.com/st
https://blog.csdn.net/weixin_38308374/article/details/105984050
https://blog.csdn.net/u013076044/article/details/89035939
https://www.freesion.com/article/3146497596/
如有侵扰,联系删除。 内容仅用于自我记录学习使用。如有错误,欢迎指正

  • 1
    点赞
  • 3
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

猫吻鱼

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值