# Dubbo3

\[TOC]

## 服务导出

当我们在某个接口上加上@`DubboService`后，就表示定义了一个**Dubbo**服务，启动应用的时候就会扫描到，并且解析对应的类，得到相对服务的配置信息：

1. 服务的类型
2. 服务的具体实现类
3. 服务的Version、Timeout等等(也就是`@DubboService`里面配置的信息)

解析完过后就会把这些配置信息封装成一个`ServiceConfig`对象，并调用`export()`进行服务的导出

所谓的服务导出，其实就是做三件事情：

1. 确定服务的最终参数配置
2. 按照不同的协议启动不同的服务器
3. 将服务进行注册，注册到注册中心

### 确定参数

这一个环节其实就是，确定好我们配置的参数：

* 在application里面配置的：name、protocol、timout……
* 在`@DubboService`里面配置的： version之类的接口级别的信息

最终确定出服务的各种参数

#### 服务注册

当确定好最终的配置参数过后，**Dubbo**就会根据配置信息生成对应 服务URL：

```txt
tri://192.168.65.221:20880/org.apache.dubbo.springboot.demo.DemoService?application=dubbo-springboot-demo-provider&timeout=3000
```

URL包括了服务的：ip、port、协议、服务名……

URL也确定了，是时候把这个URL存到注册中心里面去了

但是服务注册不仅仅这样简单了， 他需要注意：

* 如果配置信息改变了，消费者要如何感知到`change`呢
* 消费者要怎么知道现在他要用的某个**Dubbo**服务，是哪一个呢
* ……

#### 应用级注册

在Dubbo3.0之前，Dubbo进行的都是接口级注册，每个接口在注册中心(ZK)上都会有一个字符串

在Dubbo3.0之后，他兼容了以前的接口级注册，但同时提供了应用级的注册：

```sh
接口名1：tri://192.168.65.221:20880/接口名1?application=应用名
```

> 其中：
>
> key是接口名，value就是服务URL

那么为什么好端端的，要学SpringCloud Nacos那套，应用级注册呢？

来看看：

```sh
接口名1：tri://192.168.65.221:20880/接口名1?application=应用名 
接口名2：tri://192.168.65.221:20880/接口名2?application=应用名 
接口名3：tri://192.168.65.221:20880/接口名3?application=应用名 

接口名1：tri://192.168.65.222:20880/接口名1?application=应用名 
接口名2：tri://192.168.65.222:20880/接口名2?application=应用名 
接口名3：tri://192.168.65.222:20880/接口名3?application=应用名
```

一个分布式系统接口调用，就会存在就是说：一个应用中有3个Dubbo服务，那么每增加一个实例，就会向注册中心添加3条记录，那如果一个应用中有10个Dubbo服务，那么每增加一个实例，就会向注册中心添加10条记录，注册中i心的压力随着应用实例的增加而剧烈增加。

所以为了降低注册中心的压力，Dubbo3选择支持**应用级注册**，同时也兼容**接口级注册**，用户可逐步迁移称**应用级注册**

上面提到：

> 消费者要怎么知道现在他要用的某个**Dubbo**服务，是哪一个呢

在进行服务到处的过程中，会在ZK中存一个映射关系，在服务到导出的最后一步，`ServiceConfig`的`exported()`方法保存了这个映射关系：

```java
protected void exported() {
        exported = true;
        List<URL> exportedURLs = this.getExportedUrls();
        exportedURLs.forEach(url -> {
            if (url.getParameters().containsKey(SERVICE_NAME_MAPPING_KEY)) {
                ServiceNameMapping serviceNameMapping = ServiceNameMapping.getDefaultExtension(getScopeModel());
                ScheduledExecutorService scheduledExecutor = getScopeModel().getBeanFactory()
                    .getBean(FrameworkExecutorRepository.class).getSharedScheduledExecutor();
                mapServiceName(url, serviceNameMapping, scheduledExecutor);
            }
        });
        onExported();
    }
```

其中`mapServiceName(url, serviceNameMapping, scheduledExecutor);`保存了映射关系

消费者知道了要使用的**Dubbo**服务在哪个应用，那也就可以从注册中心中根据应用名查到应用的所有实例信息。但是在真正发送请求之前，还得知道服务的配置信息，对与消费者而言，他得知道当前要调用的这个Dubbo服务支持什么协议、timeout是多少……那么这个配置信息从上面最开始说到了，他们已经被注册到了注册中心了。

那么应用级注册是如何实现的呢？

首先，需要通过配置application里面来选择自己是要接口等级还是应用级注册，，当调用`RegistryProtocol`的`export()`方法处理`registry://`时，会利用**ZookeeperRegistry**把服务URL注册到**Zookeeper**中去，这就是接口级注册。

在`ServiceDiscoveryRegistry`里面的`doRegistry(URL url)`方法，就是**应用级注册**

```java
@Override
    public void doRegister(URL url) {
        // fixme, add registry-cluster is not necessary anymore
        url = addRegistryClusterKey(url);
        serviceDiscovery.register(url);
    }
```

这个方法做了两件事情：

* 将传进来的url进行处理
* 对处理过后的url进行注册

那么再来看看这个`register`方法吧：

```java
@Override
    public void register(URL url) {
        metadataInfo.addService(url);
    }
```

再进去：

```java
// key format is '{group}/{interface name}:{version}:{protocol}'
private final Map<String, ServiceInfo> services;

private transient ConcurrentNavigableMap<String, SortedSet<URL>> exportedServiceURLs;

public synchronized void addService(URL url) {
        // fixme, pass in application mode context during initialization of MetadataInfo.
        if (this.loader == null) {
            this.loader = url.getOrDefaultApplicationModel().getExtensionLoader(MetadataParamsFilter.class);
        }
        List<MetadataParamsFilter> filters = loader.getActivateExtension(url, "params-filter");
        // generate service level metadata
        ServiceInfo serviceInfo = new ServiceInfo(url, filters);
        this.services.put(serviceInfo.getMatchKey(), serviceInfo);
        // extract common instance level params
        extractInstanceParams(url, filters);

        if (exportedServiceURLs == null) {
            exportedServiceURLs = new ConcurrentSkipListMap<>();
        }
        addURL(exportedServiceURLs, url);
        updated = true;
    }
```

可以看出来，就是吧传进来的url构造出**ServiceInfo**对象放到**services**里面，一个**MetadataInfo**对象就存储以上的信息

前面提到过，在应用启动的最后，才会进行应用级注册，而应用级注册就是当前的应用实例上相关的信息存入注册中心， 包括：

1. 应用的名字
2. 获取与应用元数据的方式
3. 当前实例的ip和port
4. 当前实例支持的协议以及对应的端口

那么如果存在这样的一种情况呢：一个实例支持多个协议以及多个端口，呢么如何确定实例的ip和port？

就像这样：

```yaml
dubbo:
	application:
    	name: dubbo-springboot-demo-provider
    protocols:
    	p1:
    		name: dubbo
    		port: 20881
    	p2: 
    		name: dubbo
    		port: 20882
    	p3:
    		name: tri
    		port: 50051
```

如果是这样，最终存入`endpoint`中的会保证一个协议只对应一个端口，另外那个将被忽略，最终服消费者在进行服务引入的时候就会用到这个`endpoint`信息

在Dubbo2.7中就有了元数据中心，它其实就是减轻注册中心的压力的，Dubbo会把服务信息完整的存一份到元数据中心，元数据中心也可以用ZK来实现，自傲暴露完元数据服务之后，在注册实例信息到注册中心之前，就会把`MetadataInfo`存入元数据中心。

总结一下：

> 1. 在导出某个Dubbo服务URL时，会把服务URL存入MetadataInfo中(ServiceConfig)
>    1. 根据不同的协议，启动不同的服务器
>    2. 将URL存入MetadataInfo中
>    3. 所有服务都存入过后，将(接口:应用名)存入**元数据中心**
> 2. 应用级注册
>    1. Remote:将MedatadaInfo对象存入元数据中心
>    2. Local:启动**导出元数据服务**(默认dubbo协议，端口20880)
>       1. 确定实例port
>       2. 确定`dubbo.endpoints`
>       3. 确定实例编号revision
>       4. 将实例对象存入注册中心

## 服务引入

服务引入就是通过引入加了@`DubboReference`注解的类的**代理对象**

通过`ReferenceConfig`的`get()`方法得到一个当前接口的代理对象

```java
	 /**
     * The interface proxy reference
     */
    private transient volatile T ref;


	@Override
    @Transient
    public T get(boolean check) {
        if (destroyed) {
            throw new IllegalStateException("The invoker of ReferenceConfig(" + url + ") has already destroyed!");
        }

        if (ref == null) {
            // ensure start module, compatible with old api usage
            getScopeModel().getDeployer().start();

            init(check);
        }

        return ref;
    }
```

```java
private T createProxy(Map<String, String> referenceParameters) {
        urls.clear();

        meshModeHandleUrl(referenceParameters);

        if (StringUtils.isNotEmpty(url)) {
            // user specified URL, could be peer-to-peer address, or register center's address.
            parseUrl(referenceParameters);
        } else {
            // if protocols not in jvm checkRegistry
            aggregateUrlFromRegistry(referenceParameters);
        }
        createInvoker();

        if (logger.isInfoEnabled()) {
            logger.info("Referred dubbo service: [" + referenceParameters.get(INTERFACE_KEY) + "]." +
                    (Boolean.parseBoolean(referenceParameters.get(GENERIC_KEY)) ?
                            " it's GenericService reference" : " it's not GenericService reference"));
        }

        URL consumerUrl = new ServiceConfigURL(CONSUMER_PROTOCOL, referenceParameters.get(REGISTER_IP_KEY), 0,
                referenceParameters.get(INTERFACE_KEY), referenceParameters);
        consumerUrl = consumerUrl.setScopeModel(getScopeModel());
        consumerUrl = consumerUrl.setServiceModel(consumerModel);
        MetadataUtils.publishServiceDefinition(consumerUrl, consumerModel.getServiceModel(), getApplicationModel());

        // create service proxy
        return (T) proxyFactory.getProxy(invoker, ProtocolUtils.isGeneric(generic));
    }
```

然后通过`RegistryProtocol.doRefer()`方法返回一个MigrationInvoker对象：

```java
protected <T> Invoker<T> doRefer(Cluster cluster, Registry registry, Class<T> type, URL url, Map<String, String> parameters) {
        Map<String, Object> consumerAttribute = new HashMap<>(url.getAttributes());
        consumerAttribute.remove(REFER_KEY);
        String p = isEmpty(parameters.get(PROTOCOL_KEY)) ? CONSUMER : parameters.get(PROTOCOL_KEY);
        URL consumerUrl = new ServiceConfigURL(
            p,
            null,
            null,
            parameters.get(REGISTER_IP_KEY),
            0, getPath(parameters, type),
            parameters,
            consumerAttribute
        );
        url = url.putAttribute(CONSUMER_URL_KEY, consumerUrl);
        ClusterInvoker<T> migrationInvoker = getMigrationInvoker(this, cluster, registry, type, url, consumerUrl);
        return interceptInvoker(migrationInvoker, url, consumerUrl);
    }
```

接着通过`MigrationRule.getStep()`方法对`step`进行赋值：

```java
public MigrationStep getStep(URL consumerURL) {
        MigrationStep value = getValue(consumerURL, SubMigrationRule::getStep);
        if (value != null) {
            return value;
        }

        /**
         * FIXME, it's really hard to follow setting default values here.
         */
        if (step == null) {
            // initial step : APPLICATION_FIRST
            step = MigrationStep.APPLICATION_FIRST;
            step = Enum.valueOf(MigrationStep.class,
                consumerURL.getParameter(MIGRATION_STEP_KEY, getDefaultStep(consumerURL, step.name())));
        }

        return step;
    }
    
```

反正最终就是得到一个基于`MigrationInvoker`的接口代理对象

## 服务调用

在引入服务之后，通过接口代理对象执行方法：执行`MigrationInvoker.invoke()`方法，去除`currentAvailableInvoker`属性对应的`ClusterInvoker`，然后执行`ClusterInvoker.invoke`方法，最后通过负载均衡根据不同的协议发送不同的数据包
