Dubbo笔记 ㉗ : 服务自省-提供者

Dubbo笔记 ㉗ : 服务自省-提供者,第1张

Dubbo笔记 ㉗ : 服务自省-提供者

文章目录
  • 一、前言
    • 1. 概念
  • 二、服务自省
    • 1. 相关配置
      • 3.1 dubbo.application.metadata-type
      • 3.2 dubbo.application.register-consumer
      • 3.3 dubbo.registry.simplified
      • 3.4 dubbo.registry.parameters.registry-type
    • 2. 关键类
      • 2.1 metadataReport
      • 2.2 metadataService
      • 2.3 metadataServiceExporter
  • 三、提供者的注册
    • 1. exportServices();
      • 1.1 ServiceConfig#doExportUrls
      • 1.2 ServiceConfig#doExportUrlsFor1Protocol
      • 1.3 ServiceDiscoveryRegistryProtocol#export
      • 1.4 总结
    • 2. isonlyRegisterProvider() & hasExportedServices()
      • 2.1 DubboBootstrap#isOnlyRegisterProvider
      • 2.2 DubboBootstrap#hasExportedServices
      • 2.3 总结
    • 3. exportmetadataService();
    • 4. registerServiceInstance();
      • 4.1 DubboBootstrap#createServiceInstance
      • 4.2 DubboBootstrap#getServiceDiscoveries
      • 4.3 ServiceDiscovery#register
        • 4.3.1 ServiceInstancePreRegisteredEvent
        • 4.3.2 ZookeeperServiceDiscovery#register
        • 4.3.3 ServiceInstanceRegisteredEvent
    • 5. 总结
  • 四、问题
    • 1. 依赖问题
    • 2. 远程元数据中心

一、前言

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

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


本文基于 Dubbo 2.7.5 版本。关于该部分逻辑,如有需要可参考:

  1. Dubbo笔记 ㉕ : Spring 执行流程概述
  2. Dubbo笔记 ㉖ : DubboBootstrap 的服务暴露
  3. Dubbo笔记 ㉗ : 服务自省-提供者
  4. Dubbo笔记 ㉘ : 服务自省-消费者

关于服务自省的内容强烈推荐阅读 Apache dubbo 服务自省架构设计 。由于个人并未在实际项目中使用过服务自省模式,故本文的叙述可能有错漏。

1. 概念

关于服务自省的内容有许多文章都进行了介绍,这里引用 dubbo2.7.0版本以上 服务注册和服务调用方式改变) 一文的介绍,如下:

注册中心数据结构格式改变(service:接口服务,application:同个应用实例组成的集合,instance:单个应用实例),带来的是“服务自省”

以 Dubbo 当前的地址发现数据格式为例,它是“RPC 服务粒度”的,它是以 RPC 服务作为 key,以实例列表作为 value 来组织数据的:

而我们新引入的“应用粒度的服务发现”,它以应用名(Application)作为 key,以这个应用部署的一组实例(Instance)列表作为 value。这带来两点不同:

数据映射关系变了,从 RPC Service -> Instance 变为 Application -> Instance;数据变少了,注册中心没有了 RPC Service 及其相关配置信息。

应用粒度服务发现的数据模型有几个以下明显变化:数据中心的数据量少了,RPC 服务相关的数据在注册中心没有了,现在只有 application - instance 这两个层级的数据。为了保证这部分缺少的 RPC 服务数据仍然能被 Consumer 端正确的感知,我们在 Consumer 和 Provider 间建立了一条单独的通信通道:Consumer 和 Provider 两两之间通过特定端口交换信息,我们把这种 Provider 自己主动暴露自身信息的行为认为是一种内省机制,因此从这个角度出发,我们把整个机制命名为:服务自省。


个人理解:

在服务自省之前,Dubbo中的提供者是通过注册中心来完成数据更新,即当提供者信息有变化时,会重新发布到注册中心,而消费者通过监听注册中心的提供者节点,当提供者信息更新时,消费者可以感知。

而服务自省则是抹去了 “注册中心” 这个中间商,提供者在启动时会主动暴露 一个 metadateSevice(这个 Service并不需要我们手动暴露, 而是 Dubbo 主动会为每个应用暴露该服务),metadateSevice 作为一个 dubbo 服务对外提供了获取应用信息的能力。消费者通过 metadateSevice 可以获取到提供者的最新信息。图源: Apache dubbo 服务自省架构设计

关于 Dubbo 服务自省的必要性详参 : Apache dubbo 服务自省架构设计。


二、服务自省 1. 相关配置

我们以下面的配置为例来介绍几个配置属性。

dubbo:
  application:
    name: simple-provider
#    metadata-type: local # 是否使用远程的元数据中心,如果使用,则会将元数据信息注册到指定的元数据中心,否则使用本地元数据中心
    logger: slf4j
#    register-consumer: true # 待研究,已明了调用场景,待研究
  registry:
    address: zookeeper://localhost:2181
#    simplified: true # 简化 url,服务自省情况下不能简化,否则无法注册到注册中心
    parameters:
      registry-type: service # 开启服务自省
  protocol:
    name: dubbo
    port: 9999
  scan:
    base-packages: com.kingfish.service
  metadata-report:
    address: zookeeper://localhost:2181
3.1 dubbo.application.metadata-type
  • 参数意义: dubbo.application.metadata-type 的取值为 remote 或 local, 默认 local。对于 应用的元数据,Dubbo提供了两种保存方式 【本地保存】 和 【元数据中心保存】。如果为local ,则会将应用的元数据信息保存在应用本地,否则则会将元数据信息保存到 metadata-report.address 指定的元数据中心中。因此,当此属性为 remote 时, 必须存在 metadata-report.address 配置

  • 调用时机:在DubboBootstrap 中会调用 DubboBootstrap#initialize会调用 DubboBootstrap#initmetadataService方法,其中会根据 metadata-type 类型来获取不同的 metadataService 实现类从而实现本地或远程保存。

        private void initmetadataService() {
            this.metadataService = getExtension(getmetadataType());
        }
    
3.2 dubbo.application.register-consumer
  • 参数意义 :取值为 true 或 false,默认 false。如果为 true,即是元数据中心中没有暴露服务也会启用服务自省模式。
  • 调用时机: DubboBootstrap#start 中的 isonlyRegisterProvider 方法。
3.3 dubbo.registry.simplified
  • 参数意义 :dubbo.registry.simplified 代表是否简化 url,默认为 false。如果为true,则会将 dubbo service 的url 进行一个简化后注册。在服务自省模式下不能启用,否则无法识别是服务自省。

  • 调用时机:在RegistryProtocol#export 暴露服务时会调用 RegistryProtocol#getUrlToRegistry 方法判断,如果 simplified = true 则会简化URL,去除部分属性。当启用服务自省时会调用 ServiceDiscoveryRegistry#register 方法将Dubbo Service 注册到元数据中心,而在注册之前有一个 shouldRegister 判断,判断依据是 side 属性 是否为 provider。在简化后的 URL已经不存在该属性,所以无法注册到元数据中心。进而导致服务自省模式直接无法使用。

        @Override
        public final void register(URL url) {
        	// 判断是否应该注册到元数据中心 ,根据 side = provider 为判断
            if (!shouldRegister(url)) { // Should Not Register
                return;
            }
            super.register(url);
        }
    
3.4 dubbo.registry.parameters.registry-type
  • 参数意义 :当 dubbo.registry.parameters.registry-type 为 service 时会启用服务自省模式

  • 调用时机:Dubbo Service 在暴露过程中会调用 ServiceConfig#doExportUrls,其中会调用如下代码,在其中会判断,如果 dubbo.registry.parameters.registry-type = service,则会以 service-discovery-registry 作为注册中心协议类型:

    List registryURLs = ConfigValidationUtils.loadRegistries(this, true);
    
2. 关键类 2.1 metadataReport

该接口提供了可以对元数据中心的数据进行增删查 *** 作。不同的元数据中心具有不同的实现类,如 zk 的 ZookeepermetadataReport、Nacos 的 NacosmetadataReport、Redis 的 RedismetadataReport 等。metadataReport 的定义如下:

public interface metadataReport {

    void storeProvidermetadata(metadataIdentifier providermetadataIdentifier, ServiceDefinition serviceDefinition);

    void storeConsumermetadata(metadataIdentifier consumermetadataIdentifier, Map serviceParameterMap);

    void saveServicemetadata(ServicemetadataIdentifier metadataIdentifier, URL url);

    void removeServicemetadata(ServicemetadataIdentifier metadataIdentifier);

    List getExportedURLs(ServicemetadataIdentifier metadataIdentifier);

    void saveSubscribedData(SubscribermetadataIdentifier subscribermetadataIdentifier, Set urls);

    List getSubscribedURLs(SubscribermetadataIdentifier subscribermetadataIdentifier);

    String getServiceDefinition(metadataIdentifier metadataIdentifier);
}

与 metadataReport 对应的是 metadataReportFactory, metadataReportFactory 是 SPI 接口,默认为 redis ,用于创建 metadataReport 。org.apache.dubbo.metadata.report.metadataReportFactory 文件如下:

zookeeper=org.apache.dubbo.metadata.store.zookeeper.ZookeepermetadataReportFactory

redis=org.apache.dubbo.metadata.store.redis.RedismetadataReportFactory

consul=org.apache.dubbo.metadata.store.consul.ConsulmetadataReportFactory

etcd=org.apache.dubbo.metadata.store.etcd.EtcdmetadataReportFactory

nacos=org.apache.dubbo.metadata.store.nacos.NacosmetadataReportFactory
2.2 metadataService

以下来源 Apache dubbo 服务自省架构设计 :
【架构上,无论 Dubbo Service 属于 Provider 还是 Consumer,甚至是两者的混合,每个 Dubbo (Service)服务实例有且仅有一个 Dubbo 元数据服务。换言之,Dubbo Service 不存在纯粹的 Consumer,即使它不暴露任何业务服务,那么它也可能是 Dubbo 运维平台(如 Dubbo Admin)的 Provider。不过出于行文的习惯,Consumer 仍旧被定义为 Dubbo 服务消费者(应用)。由于每个 Dubbo Service 均发布自身的 Dubbo 元数据服务,那么,架构不会为不同的 Dubbo Service 设计独立的元数据服务接口(Java)。换言之,所有的 Dubbo Service 元数据服务接口是统一的,命名为 metadataService。从 Dubbo 服务(URL)注册与发现的视角, metadataService 扮演着传统 Dubbo 注册中心的角色。】


简单来说 : Dubbo 会为每一个 Dubbo应用 暴露的一个 metadataService 服务,metadataService 的发布过程是完全自动的,不需要人工干预。通过 metadataService 可以获取已发布的服务或进行服务发布。


metadataService 的子接口 WritablemetadataService 是 Dubbo SPI 接口,WritablemetadataService 具有三个实现类 :

  • InMemoryWritablemetadataService : 实现在导出时将 Dubbo 服务的元数据存储在本地内存中。当 metadata-type = local 时加载该类作为元数据服务,服务的元数据信息会保存在本地。
  • RemoteWritablemetadataService : 实现在导出时将 Dubbo 服务的元数据存储在元数据中心。会通过 metadataReport 将服务元数据写到元数据中心
  • RemoteWritablemetadataServiceDelegate :在 RemoteWritablemetadataServiceDelegate 中,针对于元数据的 *** 作会同时执行 RemoteWritablemetadataService 和 InMemoryWritablemetadataService 对应方法。当 metadata-type = remote 时加载该类作为元数据服务
2.3 metadataServiceExporter

metadataServiceExporter 作为元数据服务导出器,用于暴露 metadataService 服务。在 DubboBootstrap#initialize 中执行 DubboBootstrap#initmetadataServiceExporter 时初始化,默认实现为 ConfigurablemetadataServiceExporter。

    private void initmetadataServiceExporter() {
        this.metadataServiceExporter = new ConfigurablemetadataServiceExporter(metadataService);
    }

ConfigurablemetadataServiceExporter#export 实现如下, 是常规的 Dubbo服务暴露流程,这里不再赘述。

    @Override
    public ConfigurablemetadataServiceExporter export() {
        if (!isExported()) {
            ServiceConfig serviceConfig = new ServiceConfig<>();
            serviceConfig.setApplication(getApplicationConfig());
            serviceConfig.setRegistries(getRegistries());
            serviceConfig.setProtocol(generatemetadataProtocol());
            serviceConfig.setInterface(metadataService.class);
            serviceConfig.setRef(metadataService);
            serviceConfig.setGroup(getApplicationConfig().getName());
            serviceConfig.setVersion(metadataService.version());
            // export
            serviceConfig.export();
            this.serviceConfig = serviceConfig;
        } else { }

        return this;
    }

三、提供者的注册

上面我们介绍了 服务自省中的一些配置和关键类,下面我们来看看提供者服务自省的整个流程。

在 Dubbo笔记 ㉕ : Spring 执行流程概述 一文中我们 分析了 Spring 中 Dubbo 初始化的过程,其中提到 Dubbo初始化依赖于 DubboBootstrap#start 方法,如下:

    public DubboBootstrap start() {
        if (started.compareAndSet(false, true)) {
            initialize();
            // 1. 导出 Dubbo 服务
            exportServices();
            // Not only provider register
            // 2. 判断是否需要导出
            if (!isOnlyRegisterProvider() || hasExportedServices()) {
                // 3. 导出元数据中心服务
                exportmetadataService();
                // 4. 注册服务实例
                registerServiceInstance();
            }
			// 5. 引用服务
            referServices();
        }
        return this;
    }

这里我们来看具体 *** 作 :

1. exportServices();

DubboBootstrap#exportServices 完成了服务暴露的过程。

不同的是,在传统模式下, DubboBootstrap#exportServices 的服务暴露是将当前 Dubbo Service 注册到注册中心上 (以ZK 为例,即在ZK 上创建该 接口 的节点), 而在服务自省模式下,并不会将Dubbo Service注册到注册中心上,而是通过metadataService 进行服务暴露,同时会根据 dubbo.application.metadata-type 的取值来决定是保存到本地还是保存到元数据中心。下面我们具体来看:


1.1 ServiceConfig#doExportUrls

下面我们忽略亿点点调用细节,按照下面的调用逻辑,DubboBootstrap#exportServices 会调用 到 ServiceConfig#doExportUrls 方法

DubboBootstrap#exportServices  
-> org.apache.dubbo.config.ServiceConfig#export 
-> org.apache.dubbo.config.ServiceConfig#doExport 
-> org.apache.dubbo.config.ServiceConfig#doExportUrls

ServiceConfig#doExportUrls 简化实现如下:

    private void doExportUrls() {

        ... 注册服务提供者
        
		// 获取注册中心地址
        List registryURLs = ConfigValidationUtils.loadRegistries(this, true);

        for (ProtocolConfig protocolConfig : protocols) {
            ... 注册 Dubbo Service

            // 进行服务暴露
            doExportUrlsFor1Protocol(protocolConfig, registryURLs);
        }
    }

我们这里需要注意 ConfigValidationUtils.loadRegistries(this, true) ,内部会执行下面代码:

  url = URLBuilder.from(url)
          .addParameter(REGISTRY_KEY, url.getProtocol())
          // 这里会通过ConfigValidationUtils#extractRegistryType 判断 dubbo.registry.parameters.registry-type 是否为 service 
          // 如果是则将url Protocol 设置为 service-discovery-registry 否则为 registry
          .setProtocol(extractRegistryType(url))
          .build();

也就是说,如果我们设置了 dubbo.registry.parameters.registry-type = service,在这则会将注册中心协议修改 为 service-discovery-registry ,如下:

// 协议类型为 service-discovery-registry,在后续会对应加载 ServiceDiscoveryRegistryProtocol
service-discovery-registry://localhost:2181/org.apache.dubbo.registry.RegistryService?application=simple-provider&dubbo=2.0.2&logger=slf4j&metadata.type=remote&pid=25236&qos.enable=false®istry=zookeeper®istry-type=service®istry.type=service&release=2.7.5×tamp=1634545621271

否则协议类型会为设置为 registry,如下:

// 协议类型为 registry, 在后续对应加载 RegistryProtocol
registry://localhost:2181/org.apache.dubbo.registry.RegistryService?application=simple-provider&dubbo=2.0.2&logger=slf4j&metadata.type=local&pid=38688&qos.enable=false®istry=zookeeper&release=2.7.5&simplified=false×tamp=1636683500716

1.2 ServiceConfig#doExportUrlsFor1Protocol

上面的代码我们看到 ServiceConfig#doExportUrls 会调用 ServiceConfig#doExportUrlsFor1Protocol 来进行服务暴露。ServiceConfig#doExportUrlsFor1Protocol 方法太长,我们这里就不再贴出。


在 ServiceConfig#doExportUrlsFor1Protocol 中会调用如下代码来暴露服务。

	Exporter exporter = protocol.export(wrapperInvoker);

其中 protocol 是 Dubbo SPI 加载的是适配器类型,会根据 wrapperInvoker.getUrl().getProtocol() 的结果来加载对应的 Protocol实现类。而在上面我们提到,在 ServiceConfig#doExportUrls 中会根据 如果 dubbo.registry.parameters.registry-type = service,协议类型会修改为 service-discovery-registry ,否则为 registry。关于 Dubbo SPI 相关内容,如有需要详参: https://blog.csdn.net/qq_36882793/article/details/114597666

  • 如果 dubbo.registry.parameters.registry-type = service 会导致 protocol = service-discovery-registry, Protocol 实现类为 ServiceDiscoveryRegistryProtocol,这里会调用 ServiceDiscoveryRegistryProtocol#export
  • 否则 protocol = registry,Protocol 实现类为 RegistryProtocol, 这里会调用 RegistryProtocol#export

注:
在 ServiceConfig#doExportUrlsFor1Protocol 最后还有一段代码,这段代码是将 Dubbo 服务定义(包括 接口全路径、方法名、入参、返回值等信息) 发布到元数据中心,如下 :

	// 获取 metadataService 实例,这里的类型根据 url 的 metadata 参数获取,默认为 local。
     WritablemetadataService metadataService = WritablemetadataService.getExtension(url.getParameter(metaDATA_KEY, DEFAULT_metaDATA_STORAGE_TYPE));
     if (metadataService != null) {
     	// 将服务定义发布到 元数据中
         metadataService.publishServiceDefinition(url);
     }
1.3 ServiceDiscoveryRegistryProtocol#export

ServiceDiscoveryRegistryProtocol 实现如下,可以看到, 当注册中心协议类型为 service-discovery-registry 时不对url做处理,直接返回 :

public class ServiceDiscoveryRegistryProtocol extends RegistryProtocol {

    @Override
    protected URL getRegistryUrl(Invoker originInvoker) {
        URL registryUrl = originInvoker.getUrl();
        // 如果 protocol 是 service-discovery-registry, 则直接返回
        if (SERVICE_REGISTRY_PROTOCOL.equals(registryUrl.getProtocol())) {
            return registryUrl;
        }
        return super.getRegistryUrl(originInvoker);
    }

    @Override
    protected URL getRegistryUrl(URL url) {
	     // 如果 protocol 是 service-discovery-registry, 则直接返回
        if (SERVICE_REGISTRY_PROTOCOL.equals(url.getProtocol())) {
            return url;
        }
        return super.getRegistryUrl(url);
    }

}


	
	// super.getRegistryUrl(url); 的实现如下: 
	// RegistryProtocol#getRegistryUrl 会对注册中心协议做进一步处理
    protected URL getRegistryUrl(Invoker originInvoker) {
        URL registryUrl = originInvoker.getUrl();
        if (REGISTRY_PROTOCOL.equals(registryUrl.getProtocol())) {
            String protocol = registryUrl.getParameter(REGISTRY_KEY, DEFAULT_REGISTRY);
            registryUrl = registryUrl.setProtocol(protocol).removeParameter(REGISTRY_KEY);
        }
        return registryUrl;
    }

所以 ServiceDiscoveryRegistryProtocol#export 的即 RegistryProtocol#export ,如下:

    @Override
    public  Exporter export(final Invoker originInvoker) throws RpcException {
    	====================1. URL解析 ==================
    	// 1.1 获取注册中心信息 URL 
        URL registryUrl = getRegistryUrl(originInvoker);
        // url to export locally
        // 1.2 获取 需要暴露的 服务URL信息 : 通过 riginInvoker.getUrl().getParameterAndDecoded(EXPORT_KEY) 来获取
        URL providerUrl = getProviderUrl(originInvoker);

        // 1.3 订阅override数据
        // 提供者订阅时,会影响 同一JVM即暴露服务,又引用同一服务的的场景,
        // 因为subscribed以服务名为缓存的key,导致订阅信息覆盖。
        final URL overrideSubscribeUrl = getSubscribedOverrideUrl(providerUrl);
        // 根据 overrideSubscribeUrl  生成 OverrideListener,并缓存到 overrideListeners 中
        // OverrideListener 是监听器实例
        final OverrideListener overrideSubscribeListener = new OverrideListener(overrideSubscribeUrl, originInvoker);
        overrideListeners.put(overrideSubscribeUrl, overrideSubscribeListener);
        providerUrl = overrideUrlWithConfig(providerUrl, overrideSubscribeListener);
        //export invoker
        ====================2. 服务暴露 ==================
        // 进行服务暴露,这里会调用真正协议的 export 方法
        final ExporterChangeableWrapper exporter = doLocalExport(originInvoker, providerUrl);
		====================3. 服务注册 ==================
        // url to registry
        // 获取注册中心实例 : 根据调用者的地址获取注册表的实例
        final Registry registry = getRegistry(originInvoker);
        // 获取 当前注册的服务提供者 URL,如果开启了简化 URL,则返回的是简化的URL
        final URL registeredProviderUrl = getRegisteredProviderUrl(providerUrl, registryUrl);
       // ProviderInvokerWrapper 中保存了当前注册服务的一些信息(originInvoker、registryUrl、registeredProviderUrl 以及是否注册)
        ProviderInvokerWrapper providerInvokerWrapper = ProviderConsumerRegTable.registerProvider(originInvoker,
                registryUrl, registeredProviderUrl);
        //to judge if we need to delay publish
        // 判断服务是否需要延迟发布,延迟发布则不会立即进行注册,否则进行注册
        boolean register = registeredProviderUrl.getParameter("register", true);
        if (register) {
        	// 调用远端注册中心的register方法进行服务注册
        	// 此时如果有消费者订阅了该服务,则推送消息让消费者引用此服务
        	// 注册中心缓存了所有提供者注册的服务以供消费者发现。
            register(registryUrl, registeredProviderUrl);
            // 将当前服务置为已注册
            providerInvokerWrapper.setReg(true);
        }
		====================4. 服务订阅 ==================
        // Deprecated! Subscribe to override rules in 2.6.x or before.
        // 不推荐使用!订阅以覆盖2.6.x或更早版本中的规则。
        // 提供者向注册中心订阅所有注册服务,当注册中心有此服务的覆盖配置注册进来时,推送消息给提供者,重新暴露服务,这由管理页面完成。
        // org.apache.dubbo.registry.support.FailbackRegistry#subscribe
        // 向注册中心进行订阅 override 数据
        registry.subscribe(overrideSubscribeUrl, overrideSubscribeListener);
		======================================
        exporter.setRegisterUrl(registeredProviderUrl);
        exporter.setSubscribeUrl(overrideSubscribeUrl);
        //Ensure that a new exporter instance is returned every time export
        //  保证每次export都返回一个新的exporter实例
        // 返回暴露后的Exporter给上层ServiceConfig进行缓存,便于后期撤销暴露。
        return new DestroyableExporter<>(exporter);
    }


其中的逻辑我们在Dubbo笔记 ⑤ : 服务发布流程 - Protocol#export 中讲过,我们这里只强调几个地方:

  1. final Registry registry = getRegistry(originInvoker); : 我们在上面提到,此时注册中心的协议类型为 service-discovery-registry,所以这里获取到的 Registry 类型是 ServiceDiscoveryRegistry,其实现如下 :

        protected Registry getRegistry(final Invoker originInvoker) {
        	// 获取注册中心URL ,此时URL的Protocol 为 service-discovery-registry
            URL registryUrl = getRegistryUrl(originInvoker);
            // 获取 Registry 实例, 根据 Protocol 返回 ServiceDiscoveryRegistry
            return registryFactory.getRegistry(registryUrl);
        }
    

    这里会调用 AbstractRegistryFactory#getRegistry 获取注册中心实例,这里需要注意:AbstractRegistryFactory#getRegistry 在创建注册中心实例后还将其缓存到了 AbstractRegistryFactory#REGISTRIES 中,如下:

        private static final Map REGISTRIES = new HashMap<>();
    
        @Override
        public Registry getRegistry(URL url) {
            if (destroyed.get()) {
                return DEFAULT_NOP_REGISTRY;
            }
    
            url = URLBuilder.from(url)
                    .setPath(RegistryService.class.getName())
                    .addParameter(INTERFACE_KEY, RegistryService.class.getName())
                    .removeParameters(EXPORT_KEY, REFER_KEY)
                    .build();
           // 获取注册中心服务 key 
           // 如 service-discovery-registry://localhost:2181/org.apache.dubbo.registry.RegistryService
            String key = url.toServiceStringWithoutResolving();
            // Lock the registry access process to ensure a single instance of the registry
            LOCK.lock();
            try {
                Registry registry = REGISTRIES.get(key);
                if (registry != null) {
                    return registry;
                }
                //create registry by spi/ioc
                // 通过 SPI 创建 注册中心实例
                registry = createRegistry(url);
                if (registry == null) {
                    throw new IllegalStateException("Can not create registry " + url);
                }
                // 将注册中心保存到REGISTRIES 中
                REGISTRIES.put(key, registry);
                return registry;
            } finally {
                // Release the lock
                LOCK.unlock();
            }
        }
    
  2. register(registryUrl, registeredProviderUrl); :将当前 Dubbo Service 进行注册,在服务自省的情况下是通过 writablemetadataService暴露 URL。和第一步一样,这里获取到的 Registry 类型也是 ServiceDiscoveryRegistry

        public void register(URL registryUrl, URL registeredProviderUrl) {
        	// 获取 Registry 实例, 根据 Protocol 返回 ServiceDiscoveryRegistry
            Registry registry = registryFactory.getRegistry(registryUrl);
            // 进行注册,这里调用的是 ServiceDiscoveryRegistry#register
            registry.register(registeredProviderUrl);
            
    		...
        }
    

    ServiceDiscoveryRegistry#register 实现如下:

        @Override
        public final void register(URL url) {
        	// 判断是否应该注册,判断依据是 side 属性是否为 provider,如果side=provider ,shouldRegister 返回true
        	// 即提供者才需要注册
            if (!shouldRegister(url)) { // Should Not Register
                return;
            }
            // 这里经过一圈调用会再调用到 ServiceDiscoveryRegistry#doRegister
            super.register(url);
        }
    
        @Override
        public void doRegister(URL url) {
        	// 通过 writablemetadataService暴露 URL 
            if (writablemetadataService.exportURL(url)) {
                ... 日志打印
            } else {
    			... 日志打印
            }
        }
    
    

    这里需要注意 : 在 ServiceDiscoveryRegistry 的构造函数中会初始化 writablemetadataService ,而 writablemetadataService 的类型会通过 Dubbo 配置的 dubbo.metadata.storage-type 属性获取,默认为 lcoal,即实现类型为 InMemoryWritablemetadataService。所以这里的服务暴露是本地暴露。

        public ServiceDiscoveryRegistry(URL registryURL) {
            super(registryURL);
            this.serviceDiscovery = createServiceDiscovery(registryURL);
            this.subscribedServices = parseServices(registryURL.getParameter(SUBSCRIBED_SERVICE_NAMES_KEY));
            this.serviceNameMapping = ServiceNameMapping.getDefaultExtension();
            // 获取 url 中的 dubbo.metadata.storage-type 属性
            String metadataStorageType = getmetadataStorageType(registryURL);
            // 根据 dubbo.metadata.storage-type 属性加载元数据中心,默认为 local。
            this.writablemetadataService = WritablemetadataService.getExtension(metadataStorageType);
            this.subscribedURLsSynthesizers = initSubscribedURLsSynthesizers();
        }
    

    实际上,即是配置 dubbo.metadata.storage-type = remote 时,writablemetadataService 的类型为RemoteWritablemetadataServiceDelegate 其服务暴露仍然是本地暴露,因为 RemoteWritablemetadataService#exportURL 恒为true。

  3. registry.subscribe(overrideSubscribeUrl, overrideSubscribeListener); :这里只有消费者才需要订阅 URL。

        @Override
        public final void subscribe(URL url, NotifyListener listener) {
        	// // 判断是否应该订阅,判断依据是 side 属性是否为 provider, 如果side = provider,shouldSubscribe 返回false
        	// 即消费者才需要进行订阅
            if (!shouldSubscribe(url)) { // Should Not Subscribe
                return;
            }
            // 进行订阅
            super.subscribe(url, listener);
        }	
    

    需要注意的是 在 RegistryProtocol#export 中会调用 registry.subscribe(overrideSubscribeUrl, overrideSubscribeListener); ,该调用是为了兼容低版本的 Dubbo 。

    • 对于2.7.0 以下的Dubbo版本,提供者会订阅 zk 上的 configurations 节点,当有配置动态更新后会写入该节点,提供者监听到节点变动从而更新本地配置,重新发布服务。借此完成动态配置更新。

    • 对于 2.7.0 以上的Dubbo版本,Dubbo 提供了 ServiceConfigurationListener 和 ProviderConfigurationListener 监听器。在 RegistryProtocol#export 中通过 RegistryProtocol#overrideUrlWithConfig 来初始化监听逻辑,如下 :

          private URL overrideUrlWithConfig(URL providerUrl, OverrideListener listener) {
          	// 1. 获取应用级别现有配置规则,并在导出之前覆盖提供程序URL。
              providerUrl = providerConfigurationListener.overrideUrl(providerUrl);
              // 2. 初始化服务级别监听器,并保存到 serviceConfigurationListeners 中,完成了对当前服务的监听。
              ServiceConfigurationListener serviceConfigurationListener = new ServiceConfigurationListener(providerUrl, listener);
              serviceConfigurationListeners.put(providerUrl.getServiceKey(), serviceConfigurationListener);
              // 3. 获取服务级别现有配置规则,并在导出之前覆盖提供程序URL。
              return serviceConfigurationListener.overrideUrl(providerUrl);
          }
      

      该部分逻辑如有需要详参 Dubbo笔记 ㉒ :配置中心

1.4 总结

这里来做一个简单的服务暴露总结 :

  1. ServiceConfig#doExport中开始进行 Dubbo Service 的服务暴露
  2. ServiceConfig#doExportUrls 中在获取注册中心地址时,根据 dubbo.registry.parameters.registry-type 参数判断是否启用服务自省。如果启用,则注册中心协议类型修改为 service-discovery-registry,否则为 registry。
  3. ServiceConfig#doExportUrlsFor1Protocol 开始导出服务,由于注册中心协议类型修改为 service-discovery-registry 所以会调用 ServiceDiscoveryRegistryProtocol#export 来导出服务。
  4. 同样由于注册中心协议类型为 service-discovery-registry,所以会调用 ServiceDiscoveryRegistry#register 来进行服务注册。在其中会 WritablemetadataService#exportURL 来暴露服务,默认情况下为本地注入。

对于服务自省模式的服务暴露,仅仅是通过 WritablemetadataService#exportURL 进行暴露,而非服务自省的情况下则会直接在注册中心上创建服务节点。

2. isonlyRegisterProvider() & hasExportedServices()

当 (!isonlyRegisterProvider() || hasExportedServices()) 为true 时,才会执行3、4步的逻辑,这里我们来看其判断依据。

2.1 DubboBootstrap#isonlyRegisterProvider

当 dubbo.application.register-consumer = true 时 DubboBootstrap#isonlyRegisterProvider 返回false

    private boolean isOnlyRegisterProvider() {
    	// 该属性通过 dubbo.application.register-consumer 配置
        Boolean registerConsumer = getApplication().getRegisterConsumer();
        return registerConsumer == null || !registerConsumer;
    }
2.2 DubboBootstrap#hasExportedServices

当 metadataService 中存在暴露的服务时 返回true。

    private boolean hasExportedServices() {
    	// 获取 metadataService 发布的 URL
        return !metadataService.getExportedURLs().isEmpty();
    }

而我们在 第一步 DubboBootstrap#exportServices 中导出服务是通过 ServiceDiscoveryRegistry#doRegister 导出 (其中 无论是 ServiceDiscoveryRegistry#writablemetadataService 或 DubboBootstrap#metadataService 都是通过 Dubbo SPI 机制获取的实例),如下:

	    public void doRegister(URL url) {
	    	// 通过 writablemetadataService 发布 URL 
	        if (writablemetadataService.exportURL(url)) {
	            ... 日志打印
	        } else {
				... 日志打印
	        }
	    }
2.3 总结

当 dubbo.application.register-consumer = true || metadataService 存在发布的服务 时会执行下面的逻辑。

3. exportmetadataService();

这一步的逻辑很简单:即将 metadataService 作为一个 Dubbo Service 发布出去。

DubboBootstrap#exportmetadataService 实现如下:

    private void exportmetadataService() {
    	// 这里调用的是 ConfigurablemetadataServiceExporter#export 
        metadataServiceExporter.export();
    }

.ConfigurablemetadataServiceExporter#export 我们在上面介绍过,这个类的作用就是将 metadataService 发布。

4. registerServiceInstance();

可以发现,截止到目前为止,我们尚未在注册中心上创建服务节点。而这一步的目的是在注册中心上创建节点信息。


DubboBootstrap#registerServiceInstance 实现如下 :

    private void registerServiceInstance() {
        if (CollectionUtils.isEmpty(getServiceDiscoveries())) {
            return;
        }

        ApplicationConfig application = getApplication();
		// 应用名,如这里为: simple-provider
        String serviceName = application.getName();
		// 从暴露的服务 URl 中获取一个非 metadataService 服务
		// rest 协议优先,如果没有rest 协议,则随机获取一个服务用来获取host 和 port
        URL exportedURL = selectmetadataServiceExportedURL();

        String host = exportedURL.getHost();

        int port = exportedURL.getPort();
		//1.  创建应用实例, 这里的 ServiceInstance  数据并不完整,在后续暴露过程中会填充 ServiceInstance  的属性
        ServiceInstance serviceInstance = createServiceInstance(serviceName, host, port);
		// 2. 注册应用实例 (注册中心上创建节点)
        getServiceDiscoveries().forEach(serviceDiscovery -> serviceDiscovery.register(serviceInstance));
    }
    

4.1 DubboBootstrap#createServiceInstance

DubboBootstrap#createServiceInstance 创建了一个基础的 ServiceInstance 实例,其实现如下:

	// 创建 ServiceInstance
    private ServiceInstance createServiceInstance(String serviceName, String host, int port) {
    	// 构建  ServiceInstance 实例
        this.serviceInstance = new DefaultServiceInstance(serviceName, host, port);
        // 根据 metaType 设置 tmetadataStorageType。
        // 这里的 metaType 来源于 配置中的 dubbo.application.metadata-type 配置
        setmetadataStorageType(serviceInstance, getmetadataType());
        return this.serviceInstance;
    }
4.2 DubboBootstrap#getServiceDiscoveries

在上面我们提到过 AbstractRegistryFactory#getRegistry 在初次创建注册中心时还会将其缓存到 AbstractRegistryFactory#REGISTRIES 中。这里 AbstractRegistryFactory.getRegistries() 则是获取之前缓存的注册中心实例并经过一系列过滤转换后返回的结果集。

    private List getServiceDiscoveries() {
        return AbstractRegistryFactory.getRegistries()
                .stream()
                // registry 类型为 ServiceDiscoveryRegistry
                .filter(registry -> registry instanceof ServiceDiscoveryRegistry)
                // 类型强转
                .map(registry -> (ServiceDiscoveryRegistry) registry)
                // 调用 getServiceDiscovery 获取 ServiceDiscovery
                .map(ServiceDiscoveryRegistry::getServiceDiscovery)
                .collect(Collectors.toList());
    }
4.3 ServiceDiscovery#register

ServiceDiscovery 是 SPI 接口, 而 EventPublishingServiceDiscovery 是 ServiceDiscovery 接口的包装类,因此,这里的调用逻辑为
EventPublishingServiceDiscovery#register -> XxxServiceDiscovery#register。

我们这里以ZK 为注册中心,所以这里应该为 ZookeeperServiceDiscovery#register。即调用顺序如下:EventPublishingServiceDiscovery#register -> ZookeeperServiceDiscovery#register


EventPublishingServiceDiscovery#register 实现如下:

    @Override
    public final void register(ServiceInstance serviceInstance) throws RuntimeException {

        assertDestroyed(REGISTER_ACTION);
        assertInitialized(REGISTER_ACTION);

        executeWithEvents(
        		// 1. 执行前发送 ServiceInstancePreRegisteredEvent 事件
                of(new ServiceInstancePreRegisteredEvent(serviceDiscovery, serviceInstance)),
                // 2. 执行 ZookeeperServiceDiscovery#register
                () -> serviceDiscovery.register(serviceInstance),
                // 3. 执行后发送 ServiceInstanceRegisteredEvent 事件
                of(new ServiceInstanceRegisteredEvent(serviceDiscovery, serviceInstance))
        );
    }
    
    protected final void executeWithEvents(Optional beforeEvent,
                                           ThrowableAction action,
                                           Optional afterEvent) {
        beforeEvent.ifPresent(this::dispatchEvent);
        try {
            action.execute();
        } catch (Throwable e) {
        	// 执行异常发送 ServiceDiscoveryExceptionEvent 事件
            dispatchEvent(new ServiceDiscoveryExceptionEvent(this, serviceDiscovery, e));
        }
        afterEvent.ifPresent(this::dispatchEvent);
    }
4.3.1 ServiceInstancePreRegisteredEvent

ServiceInstancePreRegisteredEvent 会触发两个监听器 :LoggingEventListener 和 CustomizableServiceInstanceListener。其中 LoggingEventListener 仅仅是打印日志,我们这里主要看 CustomizableServiceInstanceListener的实现。

public class CustomizableServiceInstanceListener implements EventListener {

    @Override
    public void onEvent(ServiceInstancePreRegisteredEvent event) {
    	// SPI 加载 ServiceInstanceCustomizer 实例
        ExtensionLoader loader =
                ExtensionLoader.getExtensionLoader(ServiceInstanceCustomizer.class);
        // FIXME, sort customizer before apply
        // 
        loader.getSupportedExtensionInstances().forEach(customizer -> {
            // customizes
            // 进行定制。
            customizer.customize(event.getServiceInstance());
        });
    }
}

ServiceInstanceCustomizer 作为 ServiceInstance 定制器,会对 ServiceInstance 进行定制化,其主要通过下面四个类来定制化:

  1. ServiceInstancePortCustomizer#customize :如果 serviceInstance 的port 为空,则在此进行填充

        @Override
        public void customize(ServiceInstance serviceInstance) {
    		// port 不为空直接返回
            if (serviceInstance.getPort() != null) {
                return;
            }
    		// 获取配置中心协议配置
            Collection protocols = ApplicationModel.getConfigManager()
                    .getProtocols();
    
            if (CollectionUtils.isEmpty(protocols)) {
                throw new IllegalStateException("We should have at least one protocol configured at this point.");
            }
    		// 获取 rest 协议的配置,如果没有则直接获取第一个协议配置
            Stream protocolStream = protocols.stream();
            ProtocolConfig protocolConfig = protocolStream
                    // use rest as service instance's default protocol.
                    .filter(protocol -> "rest".equals(protocol.getName()))
                    .findFirst()
                    .orElseGet(() -> protocolStream.findFirst().get());
    		// 赋值port
            if (serviceInstance instanceof DefaultServiceInstance) {
                DefaultServiceInstance instance = (DefaultServiceInstance) serviceInstance;
                if (protocolConfig.getPort() != null) {
                    instance.setPort(protocolConfig.getPort());
                }
            }
        }
    
  2. ServiceInstancemetadataCustomizer#customize : 对 ServiceInstance#metadata 属性进行补充

        @Override
        public final void customize(ServiceInstance serviceInstance) {
    		// 获取 ServiceInstance#metadata  属性
            Map metadata = serviceInstance.getmetadata();
    		// 获取属性名和值, ServiceInstancemetadataCustomizer 有三个子类,该方法由子类实现
            String propertyName = resolvemetadataPropertyName(serviceInstance);
            String propertyValue = resolvemetadataPropertyValue(serviceInstance);
    		// 填充  ServiceInstance#metadata  属性
            if (!isBlank(propertyName) && !isBlank(propertyValue)) {
                String existedValue = metadata.get(propertyName);
                boolean put = existedValue == null || isOverride();
                if (put) {
                    metadata.put(propertyName, propertyValue);
                }
            }
        }
    

    ServiceInstancemetadataCustomizer 是一个抽象类,有三个子类如下:

    1. ExportedServicesRevisionmetadataCustomizer : 填充 dubbo.exported-services.revision 属性,即当前应用暴露服务计算而成的版本号。

    2. metadataServiceURLParamsmetadataCustomizer : 填充 dubbo.metadata-service.url-params 属性

    3. SubscribedServicesRevisionmetadataCustomizer : 填充 dubbo.subscribed-services.revision 属性,即应用引用的服务计算而成的版本号


    注: 当应用暴露后,可能存在多个应用的配置参数不同,如应用A 的timeout = 1000,应用B的timeout =2000,此时元数据中心需要分开保存这两个应用的元数据信心,并且对于消费者可能区分这两个应用,则会使用到Dubbo 服务修订版本。即应用在发布时会根据发布的配置、接口参数等信息计算出一个【修订版本号 revision】。消费者在消费时可以根据版本号区分不同的应用。其计算逻辑唉 URLRevisionResolver#resolve 中完成。关于Dubbo 服务修订版本 的内容,详参 Apache dubbo 服务自省架构设计 的 【Dubbo 服务修订版本】 内容。

  3. ProtocolPortsmetadataCustomizer#customize :填充 dubbo.endpoints 属性

        @Override
        public void customize(ServiceInstance serviceInstance) {
    
            String metadataStoredType = getmetadataStorageType(serviceInstance);
    		// 获取 metadataService
            WritablemetadataService writablemetadataService = getExtension(metadataStoredType);
    
            Map protocols = new HashMap<>();
            // 获取已经暴露的 URL,记录每个服务的 Protocol 和 port
            writablemetadataService.getExportedURLs()
                    .stream()
                    .map(URL::valueOf)
                    .filter(url -> !metadataService.class.getName().equals(url.getServiceInterface()))
                    .forEach(url -> {
                        // TODO, same protocol listen on different ports will override with each other.
                        protocols.put(url.getProtocol(), url.getPort());
                    });
    		// 保存到 ServiceInstance#metadata 中,其中key为 dubbo.endpoints
            setEndpoints(serviceInstance, protocols);
        }
    
  4. RefreshServicemetadataCustomizer#customize :在服务注册前再刷新一次 metadata,这里会刷新 ServiceInstance#metaData 中的 exportedRevision 和 subscribedRevision。

        @Override
        public void customize(ServiceInstance serviceInstance) {
    		// 获取 ServiceInstance#metadata 中的 dubbo.metadata.storage-type 属性
            String metadataStoredType = getmetadataStorageType(serviceInstance);
    	
            WritablemetadataService writablemetadataService = getExtension(metadataStoredType);
    		// 刷新 metadata
    		// getExportedServicesRevision(serviceInstance) 
    		//  getSubscribedServicesRevision(serviceInstance) 从 ServiceInstance#metadata 中获取 key为 dubbo.subscribed-services.revision 的值 
            writablemetadataService.refreshmetadata(getExportedServicesRevision(serviceInstance),
                    getSubscribedServicesRevision(serviceInstance));
        }
    

    需要注意的是,如果在dubbo属性配置 并且如果 dubbo.application.metadata-type = remote ,这里还会按照如下调用顺序在zk (元数据中心)创建对应的元数据节点。(如果为 local,则会保存在内存元数据中。)

    RemoteWritablemetadataService#refreshmetadata ->
    RemoteWritablemetadataService#saveServicemetadata -> 
    AbstractmetadataReport#saveServicemetadata ->
    AbstractmetadataReport#doSavemetadata
    

    如下图:

    value 值如下:

    dubbo%3A%2F%2F192.168.110.57%3A9999%2Fcom.kingfish.service.ProviderService%3Fanyhost%3Dtrue%26application%3Dsimple-provider%26deprecated%3Dfalse%26dubbo%3D2.0.2%26dynamic%3Dtrue%26generic%3Dfalse%26group%3Dspring%26interface%3Dcom.kingfish.service.ProviderService%26logger%3Dslf4j%26metadata.type%3Dremote%26methods%3DsayHello%2CsayHelloWorld%26pid%3D32156%26release%3D2.7.5%26revision%3D2.0.0%26side%3Dprovider%26timestamp%3D1637062353754%26version%3D2.0.0
    
4.3.2 ZookeeperServiceDiscovery#register

经历上面的填充后,此时 ServiceInstance 的数据已经完整,则开始在zk上创建节点,其路径为 /services/{应用名}/{ip}:{端口}

    public void register(ServiceInstance serviceInstance) throws RuntimeException {
        doInServiceRegistry(serviceDiscovery -> {
        	// serviceDiscovery.registerService 的实现为  org.apache.curator.x.discovery.details.ServiceDiscoveryImpl#registerService
        	// 这里会在zk上创建节点
            serviceDiscovery.registerService(build(serviceInstance));
        });
    }
    

此时在zk上的节点体现为 :

节点值为:

{
    "name": "simple-provider",
    "id": "192.168.121.57:9999",
    "address": "192.168.121.57",
    "port": 9999,
    "sslPort": null,
    "payload": {
        "@class": "org.apache.dubbo.registry.zookeeper.ZookeeperInstance",
        "id": null,
        "name": "simple-provider",
        "metadata": {
            "dubbo.metadata-service.url-params": "{"dubbo":{"version":"1.0.0","dubbo":"2.0.2","release":"2.7.5","port":"20880"}}",
            "dubbo.subscribed-services.revision": "N/A",
            "dubbo.endpoints": "[{"port":9999,"protocol":"dubbo"}]",
            "dubbo.metadata.storage-type": "remote",
            "dubbo.exported-services.revision": "3124311290257149701"
        }
    },
    "registrationTimeUTC": 1637062853062,
    "serviceType": "DYNAMIC",
    "uriSpec": null
}

需要注意:如果 ZookeeperServiceDiscovery#register 执行错误会触发 ServiceDiscoveryExceptionEvent 事件。


4.3.3 ServiceInstanceRegisteredEvent

在上面的逻辑执行结束后会触发 ServiceInstanceRegisteredEvent事件,而监听 ServiceInstanceRegisteredEvent 事件的监听器默认只有 LoggingEventListener,LoggingEventListener仅打印了一行记录日志。

5. 总结

我们这里来总结一下服务自省的情况下,提供者服务暴露的流程:

  1. 在 Dubbo笔记 ㉖ : DubboBootstrap 的服务暴露 一文中我们介绍了在 Spring 中 Dubbo 启动的过程。即 Dubbo 启动后会为需要暴露的服务创建对应的 ServiceBean,在 DubboBootstrap#start 时进行服务暴露。即是本文的开端。
  2. DubboBootstrap#start 首先指定 DubboBootstrap#exportServices 来暴露服务,在 DubboBootstrap#exportServices 中检查 Dubbo 配置,如果存在 dubbo.registry.parameters.registry-type = service。则认为启用了服务自省,在加载注册中心时会其协议修改为 service-discovery-registry 。
  3. 随后由于注册协议为 service-discovery-registry ,所以会加载 ServiceDiscoveryRegistryProtocol#export 来暴露服务。ServiceDiscoveryRegistryProtocol 在暴露过程中会保证注册中心的协议仍为 service-discovery-registry。
  4. 由于注册中心的协议仍为 service-discovery-registry,所以会加载 ServiceDiscoveryRegistry#registry 来进行服务注册,而 ServiceDiscoveryRegistry#registry 则是通过 WritablemetadataService#exportURL 来暴露服务,需要注意 这里的 WritablemetadataService 类型并不由 dubbo.application.metadata-type 参数控制,而是由 dubbo.registry.parameters.dubbo.metadata.storage-type 来控制,默认为 local。即这里默认会注册到本地的元数据中。至此 DubboBootstrap#exportServices 执行结束。
  5. 随后如果 dubbo.application.register-consumer = true || metadataService 存在发布的服务,则会将 metadataService 作为一个 Dubbo Service 发布出去。外界可以通过调用 metadataService 来获取到当前应用的元数据信息(包括暴露的服务、订阅的服务、服务名称、版本等)
  6. 最后会通过 DubboBootstrap#registerServiceInstance 注册应用实例。DubboBootstrap#registerServiceInstance 会创建当前应用的 ServiceInstance,并且对数据进行填充。填充结束后会在注册中心上创建对应的应用节点。

四、问题 1. 依赖问题

在项目搭建时,要引入该依赖,否则会出错,并且不打印日志。

   
       org.apache.dubbo
       dubbo-registry-zookeeper
       ${dubbo.version}
   

原因如截图,如果出现错误,只会分发给错误事件监听器,而不会打印日志

2. 远程元数据中心

个人在搭建时发现如果使用 dubbo.application.metadata-type = remote 时,消费者无法获取到提供者。(由于并未在实际使用过服务自省模式搭建的项目,所以这里不排除个人搭建项目问题)
以下是个人对问题的追溯:

归咎原因在于,当消费者启动后会通过提供者的 metadataService#getExportedURLs 来获取提供者暴露的 dubbo service 列表。

    default SortedSet getExportedURLs() {
    	// 这里的 ALL_SERVICE_INTERFACES 为 *
        return getExportedURLs(ALL_SERVICE_INTERFACES);
    }

如果提供者使用 dubbo.application.metadata-type = remote 时,消费者在启动后会获取提供者 ServiceInstance 的元数据,如下:

{
    "name": "simple-provider",
    "id": "192.168.121.57:9999",
    "address": "192.168.121.57",
    "port": 9999,
    "sslPort": null,
    "payload": {
        "@class": "org.apache.dubbo.registry.zookeeper.ZookeeperInstance",
        "id": null,
        "name": "simple-provider",
        "metadata": {
            "dubbo.metadata-service.url-params": "{"dubbo":{"version":"1.0.0","dubbo":"2.0.2","release":"2.7.5","port":"20880"}}",
            "dubbo.subscribed-services.revision": "N/A",
            "dubbo.endpoints": "[{"port":9999,"protocol":"dubbo"}]",
            "dubbo.metadata.storage-type": "remote",
            "dubbo.exported-services.revision": "3124311290257149701"
        }
    },
    "registrationTimeUTC": 1637062853062,
    "serviceType": "DYNAMIC",
    "uriSpec": null
}

,并根据其中 dubbo.metadata.storage-type 属性值来加载metadataService,而消费者从 dubbo.metadata.storage-type 参数知道提供者元数据中心是远程数据,因此会加载 RemotemetadataServiceProxy,来获取提供者端元数据信息,即调用 RemotemetadataServiceProxy#getExportedURLs 来获取暴露的URL,其实现如下,

    @Override
    public SortedSet getExportedURLs(String serviceInterface, String group, String version, String protocol) {
    	// 1. getmetadataReport() 获取 metadataReport
    	// 2. getmetadataReport().getExportedURLs 调用 metadataReport#getExportedURLs  方法获取提供者暴露service。此时 serviceInterface 为 * 
        return toSortedStrings(getmetadataReport().getExportedURLs(
                new ServicemetadataIdentifier(serviceInterface, group, version, PROVIDER_SIDE, revision, protocol)));
    }

这里会调用 metadataReport#getExportedURLs, 我们这里使用zk 作为元数据中心,因此这里调用的是 ZookeepermetadataReport#doGetExportedURLs,其实现如下:

    @Override
    protected List doGetExportedURLs(ServicemetadataIdentifier metadataIdentifier) {
    	// 这里getNodePath(metadataIdentifier) 构建出来的节点为  /dubbo/metadata/provider/revision3124311290257149701
    	// 因为无法获取到zk 上的服务节点。
        String content = zkClient.getContent(getNodePath(metadataIdentifier));
        if (StringUtils.isEmpty(content)) {
            return Collections.emptyList();
        }
        return new ArrayList(Arrays.asList(URL.decode(content)));
    }

如果提供者使用 dubbo.application.metadata-type = local 时, 则会调用 InMemoryWritablemetadataService#getExportedURLs 来获取全部服务,InMemoryWritablemetadataService#getExportedURLs 对 ALL_SERVICE_INTERFACES 场景做了特殊处理,其实现如下:

    @Override
    public SortedSet getExportedURLs(String serviceInterface, String group, String version, String protocol) {
    	// 如果 serviceInterface 是 ALL_SERVICE_INTERFACES,则获取全部服务返回
        if (ALL_SERVICE_INTERFACES.equals(serviceInterface)) {
            return getAllUnmodifiableServiceURLs(exportedServiceURLs);
        }
        // 否则构建 serviceKey 进行查找。
        String serviceKey = buildKey(serviceInterface, group, version);
        return unmodifiableSortedSet(getServiceURLs(exportedServiceURLs, serviceKey, protocol));
    }

以上:内容部分参考
Apache dubbo 服务自省架构设计
https://www.jianshu.com/p/3be81cfd28af
https://blog.csdn.net/weixin_38308374/article/details/105984050
https://mp.weixin.qq.com/s/V5S_vO6Mgtq9-v2ed9eDrw
https://blog.csdn.net/weixin_45583158/article/details/103900690
https://www.jianshu.com/p/8f72c14f6a79
https://mp.weixin.qq.com/s/m26_VnEwLSFIlscyEU_pTg
如有侵扰,联系删除。 内容仅用于自我记录学习使用。如有错误,欢迎指正

欢迎分享,转载请注明来源:内存溢出

原文地址: http://outofmemory.cn/zaji/5597437.html

(0)
打赏 微信扫一扫 微信扫一扫 支付宝扫一扫 支付宝扫一扫
上一篇 2022-12-15
下一篇 2022-12-15

发表评论

登录后才能评论

评论列表(0条)

保存