前言

三大中心
Dubbo 在 2.7 之前的版本只配备了注册中心,主流使用的注册中心为 zookeeper。
后来 2.7 版本官方进行了三大中心的改造,分别是:注册中心、元数据中心、配置中心。
元数据定义为描述数据的数据,在服务治理中,例如服务接口名,重试次数,版本号等等都可以理解为元数据。在 2.7 之前,元数据一股脑丢在了注册中心之中,带来问题是:推送量大 -> 存储数据量大 -> 网络传输量大 -> 延迟严重
Dubbo 2.7 进行了大刀阔斧的改动,只将真正属于服务治理的数据发布到注册中心之中,大大降低了注册中心的压力;同时,将全量的元数据发布到另外的元数据中心;配置中心主要是外部化配置的集中存储。

Dubbo3 的一项重要升级是终于支持应用级别的服务注册与发现了,之前一直都是接口级别的,当集群规模比较大的时候,注册中心地址推送的压力骤升,消费端也会随着每一次地址推送消耗大量资源。粒度改为应用级后,推送的数据规模降低了好几个数量级,性能可以得到很好的提升,也更加便于管理和维护。

综上所述,经历了几个版本的改造,Dubbo 源码的复杂度也随之上升了几个量级,如果一上来就啃 3.x 的源码,很容易迷糊。

服务发布与注册

Dubbo3 的源码比较复杂,为了不陷入泥潭,分析的过程只关注:服务元数据的发布、服务名称映射、服务注册。

服务发布与注册的关键节点:

ServiceConfig#doExport 服务暴露
  ServiceConfig#doExportUrls
    ServiceConfig#exportRemote 暴露给远程服务调用
      Protocol#export 根据指定协议暴露服务
        Exchanger#bind 数据交换层
          Transporter#bind 数据传输层
            NettyServer#doOpen 开启Netty服务

    MetadataUtils#publishServiceDefinition 发布服务定义 即元数据
      AbstractMetadataReport#storeProviderMetadataTask 存储 例如Nacos

  ServiceConfig#exported 暴露后事件
    ServiceConfig#mapServiceName 映射服务名称
      MetadataServiceNameMapping#map 建立 接口名 -> 应用名 的映射
        NacosMetadataReport#registerServiceAppMapping 发布到Nacos

开启服务

Dubbo 对服务的描述对象是ServiceConfig,暴露服务的方法是ServiceConfig#export()。又因为同一个服务可能会以多个协议的方式发布到多个注册中心,所以会调用doExportUrls()
默认情况下,Dubbo 会同时把服务注册到 JVM 和注册中心,本地注册咱们不管,直接看注册到远程,方法是ServiceConfig#exportRemote()

服务以什么协议来暴露给远程调用呢?这时候就需要依赖另一个组件了:Protocol。Dubbo会通过Protocol#export()来根据指定的协议暴露服务。
因为服务暴露的目的是为了给远程调用的,这个过程说白了就是一个数据交换的过程,即 Request 交换 Response,所以需要依赖另一个组件:Exchanger。
Exchanger 数据交换层需要通过网络来传输请求/相应数据,所以这里又会依赖 Transporter 组件,它的职责是负责网络传输的,也就是传输我们的 Request Response对象。对象怎么传输呢?再深入的话,就是 Dubbo 提供的序列化组件了。
Dubbo 默认使用的网络IO组件库是 Netty,所以最后会开启一个 NettyServer。
至此,服务就开启了,但是因为还没有发布出去,所以现在还不会有人调用。

元数据发布

ServiceConfig#exportRemote()开启服务后,Dubbo 紧接着会发布服务元数据到元数据中心,方法是MetadataUtils#publishServiceDefinition()
这里会依赖另一个组件:MetadataReport,方法storeProviderMetadataTask()会存储服务的元数据,默认是异步存储。

private void storeProviderMetadataTask(MetadataIdentifier providerMetadataIdentifier, ServiceDefinition serviceDefinition) {
    try {
        if (logger.isInfoEnabled()) {
            logger.info("store provider metadata. Identifier : " + providerMetadataIdentifier + "; definition: " + serviceDefinition);
        }
        allMetadataReports.put(providerMetadataIdentifier, serviceDefinition);
        failedReports.remove(providerMetadataIdentifier);
        // 服务元数据转JSON
        String data = JsonUtils.getJson().toJson(serviceDefinition);
        // 存储元数据 即发布 依赖外部存储媒介 例如Nacos
        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(PROXY_FAILED_EXPORT_SERVICE, "", "", "Failed to put provider metadata " + providerMetadataIdentifier + " in  " + serviceDefinition + ", cause: " + e.getMessage(), e);
    }
}

元数据发布以后,你可以在Nacos控制台看到:

dubbo 注册多服务_dubbo 注册多服务


Dubbo 对服务元数据的描述对象是ServiceDefinition,存储的内容示例:

{
    "annotations":[],
    "canonicalName":"org.apache.dubbo.demo.GreeterService",
    "codeSource":"file:/Users/panchanghe/dev/source/dubbo3.1.10/dubbo-demo/dubbo-demo-triple/target/classes/",
    "methods":[
        {
            "annotations":[

            ],
            "name":"sayHello",
            "parameterTypes":[
                "org.apache.dubbo.demo.hello.HelloRequest"
            ],
            "parameters":[

            ],
            "returnType":"org.apache.dubbo.demo.hello.HelloReply"
        }
    ],
    "parameters":{
        "application":"triple-provider",
        "dubbo":"2.0.2",
        "side":"provider",
        "pid":"95178",
        "anyhost":"true",
        "interface":"org.apache.dubbo.demo.GreeterService",
        "bind.ip":"192.168.98.34",
        "methods":"clientOrBiStream,sayHello,serverStream",
        "background":"false",
        "deprecated":"false",
        "dynamic":"true",
        "service-name-mapping":"true",
        "register-mode":"instance",
        "qos.enable":"false",
        "generic":"false",
        "bind.port":"20881",
        "timestamp":"1692701933740"
    },
    "types":[
        {
            "enums":[

            ],
            "items":[

            ],
            "properties":{
                "message":"java.lang.String"
            },
            "type":"org.apache.dubbo.demo.hello.HelloReply"
        }
    ],
    "uniqueId":"org.apache.dubbo.demo.GreeterService@file:/Users/panchanghe/dev/source/dubbo3.1.10/dubbo-demo/dubbo-demo-triple/target/classes/"
}

服务名称映射

提供者改成 应用级服务注册了,但是消费者发起调用的时候,依然只知道接口名,如果按照之前的方式,消费者就会找不到服务。
怎么办呢?Dubbo 提供的解决方案是,提供者再写入一个 接口名 -> 应用名 的映射关系,消费者先根据接口名定位到应用名,再去订阅服务就好了。

服务名称映射是在最后写入的,先开启发布、发布元数据后,再写入映射关系。对应的方法是MetadataReport#registerServiceAppMapping()

@Override
public boolean registerServiceAppMapping(String key, String group, String content, Object ticket) {
    try {
        if (!(ticket instanceof String)) {
            throw new IllegalArgumentException("nacos publishConfigCas requires string type ticket");
        }
        return configService.publishConfigCas(key, group, content, (String) ticket);
    } catch (NacosException e) {
        logger.warn(REGISTRY_NACOS_EXCEPTION, "", "", "nacos publishConfigCas failed.", e);
        return false;
    }
}

服务名称映射非常简单,Data Id 是接口名称,Group 是固定的mapping,内容是应用名。

dubbo 注册多服务_dubbo_02

服务注册

Dubbo3 的服务注册也做了大的改动,因为是应用级别的,所以现在不是在 ServiceConfig 里面注册服务了,而是在外层 ModuleDeployer 注册一次。

方法是ServiceInstanceMetadataUtils#registerMetadataAndInstance(),最终会依赖外部存储注册服务,例如Nacos。

public static void registerMetadataAndInstance(ApplicationModel applicationModel) {
    LOGGER.info("Start registering instance address to registry.");
    RegistryManager registryManager = applicationModel.getBeanFactory().getBean(RegistryManager.class);
    // register service instance
    registryManager.getServiceDiscoveries().forEach(ServiceDiscovery::register);
}

注意看方法名是registerMetadataAndInstance,为什么是注册元数据和实例呢?元数据不是已经注册过了吗?这里的 Metadata 指的是 应用级别的元数据,不是接口级别的元数据。

服务引用

Dubbo3 的服务引用引入了一个新组件MigrationClusterInvoker,它主要是为接口级服务注册升级到应用级服务注册用的,可以实现平滑迁移。
另一个改动点就是,因为是应用级服务注册了,但是消费者会根据接口名去引用服务,如果消费者不做处理,是引用不到服务的。所以 Dubbo3 在引用服务前会先调用ServiceNameMapping#getAndListen()拿接口名去元数据中心匹配应用名,再去注册中心引用服务。

Dubbo3 服务引用的关键节点:

ReferenceConfig#get 引用服务
  ReferenceConfig#createProxy 创建代理对象
    ReferenceConfig#createInvokerForRemote 创建远程服务Invoker
      RegistryProtocol#refer 通过注册中心引用服务
        ServiceDiscoveryMigrationInvoker#new 服务发现迁移Invoker 兼容 接口级 -> 应用级
          MigrationRuleHandler#refreshInvoker 根据订阅规则 刷新Invoker
            DynamicDirectory#subscribe 动态服务订阅
              ServiceNameMapping#getAndListen 订阅 接口名映射到应用名(如果是应用级订阅)
              Protocol#refer 根据具体的协议引用服务
                Invoker#new 创建协议对应的客户端Invoker
                  ExchangeClient#new 创建Client 建立连接
              Cluster#join 集群容错
  ProxyFactory#getProxy 基于Invoker创建代理对象

服务引用的入口没变,还是ReferenceConfig#get(),服务引用的本质是创建代理对象,代理对象来完成底层 RPC 调用,所以会调用ReferenceConfig#createProxy()创建代理对象。
创建代理对象需要 Invoker,Dubbo 靠 Invoker 发起远程调用,此时会调用Protocol#refer()来生成 Invoker对象。
MigrationRuleHandler#refreshInvoker()会根据配置的服务订阅规则来生成 Invoker 对象,订阅规则有三种:

  • FORCE_INTERFACE:只订阅接口级服务
  • APPLICATION_FIRST:双订阅,应用级服务优先
  • FORCE_APPLICATION:只订阅应用级服务

如果订阅了应用级服务,紧接着会调用ServiceNameMapping#getAndListen()把接口名转换为应用名再去注册中心拉取服务列表,最后调用Protocol#refer()根据指定的协议来应用远程服务,生成对应的 Invoker,此时会和远端建立连接,创建 ExchangeClient。
有了 Invoker,最后就是通过 ProxyFactory 创建代理对象,代理对象会通过 Invoker 按照指定协议发起 RPC 调用。