深入解析 Dubbo 3.0 服务端暴露全流程

Posted 阿里云开发者

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了深入解析 Dubbo 3.0 服务端暴露全流程相关的知识,希望对你有一定的参考价值。

简介:随着云原生时代的到来,Dubbo 3.0 的一个很重要的目标就是全面拥抱云原生。正因如此,Dubbo 3.0 为了能够更好的适配云原生,将原来的接口级服务发现机制演进为应用级服务发现机制。

作者介绍


熊聘,Github账号pinxiong,Apache Dubbo贡献者,关注RPC、Service Mesh和云原生等领域。现任职于携程国际事业部研发团队,负责市场营销、云原生等相关工作。


背景



随着云原生时代的到来,Dubbo 3.0 的一个很重要的目标就是全面拥抱云原生。正因如此,Dubbo 3.0 为了能够更好的适配云原生,将原来的接口级服务发现机制演进为应用级服务发现机制。


基于应用级服务发现机制,Dubbo 3.0 能大幅降低框架带来的额外资源消耗,大幅提升资源利用率,主要体现在:


  • 单机常驻内存下降 75%
  • 能支持的集群实例规模以百万计的集群
  • 注册中心总体数据量下降超 90%


目前关于 Dubbo 服务端暴露流程的技术文章很多,但是都是基于 Dubbo 接口级服务发现机制来解读的。在 Dubbo 3.0 的应用级服务发现机制下,服务端暴露流程与之前有很大的变化,本文希望可以通过 对Dubbo 3.0 源码理解来解析服务端暴露全流程。



什么是应用级服务发现



简单来说,以前 Dubbo 是将接口的信息全部注册到注册中心,而一个应用实例一般会存在多个接口,这样一来注册的数据量就要大很多,而且有冗余。应用级服务发现的机制是同一个应用实例仅在注册中心注册一条数据,这种机制主要解决以下几个问题:


  • 对齐主流微服务模型,如:Spring Cloud
  • 支持 Kubernetes native service,Kubernetes 中维护调度的服务都是基于应用实例级,不支持接口级
  • 减少注册中心数据存储能力,降低了地址变更推送的压力


假设应用 dubbo-application 部署了 3 个实例(instance1, instance2, instance3),并且对外提供了 3 个接口(sayHello, echo, getVersion)分别设置了不同的超时时间。在接口级和应用级服务发现机制下,注册到注册中心的数据是截然不同的。如下图所示:


  • 接口级服务发现机制下注册中心中的数据



"sayHello": [
  {"application":"dubbo-application","name":"instance1", "ip":"127.0.0.1", "metadata":{"timeout":1000}},
  {"application":"dubbo-application","name":"instance2", "ip":"127.0.0.2", "metadata":{"timeout":2000}},
  {"application":"dubbo-application","name":"instance3", "ip":"127.0.0.3", "metadata":{"timeout":3000}},
],
"echo": [
  {"application":"dubbo-application","name":"instance1", "ip":"127.0.0.1", "metadata":{"timeout":1000}},
  {"application":"dubbo-application","name":"instance2", "ip":"127.0.0.2", "metadata":{"timeout":2000}},
  {"application":"dubbo-application","name":"instance3", "ip":"127.0.0.3", "metadata":{"timeout":3000}},
],
"getVersion": [
  {"application":"dubbo-application","name":"instance1", "ip":"127.0.0.1", "metadata":{"timeout":1000}},
  {"application":"dubbo-application","name":"instance2", "ip":"127.0.0.2", "metadata":{"timeout":2000}},
  {"application":"dubbo-application","name":"instance3", "ip":"127.0.0.3", "metadata":{"timeout":3000}}
]


  • 应用级服务发现机制下注册中心中的数据


"dubbo-application": [
  {"name":"instance1", "ip":"127.0.0.1", "metadata":{"timeout":1000}},
  {"name":"instance2", "ip":"127.0.0.2", "metadata":{"timeout":2000}},
  {"name":"instance3", "ip":"127.0.0.3", "metadata":{"timeout":3000}}
]


通过对比我们可以发现,采用应用级服务发现机制确实使注册中心中的数据量减少了很多,那些原有的接口级的数据存储在元数据中心中。



服务端暴露全流程


引入应用级服务发现机制以后,Dubbo 3.0 服务端暴露全流程和之前有很大的区别。暴露服务端全流程的核心代码在 DubboBootstrap#doStart 中,具体如下:


private void doStart() {
    // 1. 暴露Dubbo服务
    exportServices();
    // If register consumer instance or has exported services
    if (isRegisterConsumerInstance() || hasExportedServices()) {
        // 2. 暴露元数据服务
        exportMetadataService();
        // 3. 定时更新和上报元数据
        registerServiceInstance();
        ....
    }
    ......
}


假设以 Zookeeper 作为注册中,对外暴露 Triple 协议的服务为例,服务端暴露全流程时序图如下:

我们可以看到,整个的暴露流程还是挺复杂的,一共可以分为四个部分:

  • 暴露 injvm 协议的服务
  • 注册 service-discovery-registry 协议
  • 暴露 Triple 协议的服务并注册 registry 协议
  • 暴露 MetadataService 服务


下面会分别从这四个部分对服务暴露全流程进行详细讲解。


1、暴露 injvm 协议的服务


injvm 协议的服务是暴露在本地的,主要原因是在一个应用上往往既有 Service(暴露服务)又有 Reference(服务引用)的情况存在,并且 Reference 引用的服务就是在该应用上暴露的 Service。为了支持这种使用场景,Dubbo 提供了 injvm 协议,将 Service 暴露在本地,Reference 就可以不需要走网络直接在本地调用 Service。


整体时序图


由于这部分内容在之前的接口级服务发现机制中是类似的,所以相关的核心代码就不在这里展开讨论了。


2、注册 service-discovery-registry 协议


注册 service-discovery-registry 协议的核心目的是为了注册与服务相关的元数据,默认情况下元数据通过 InMemoryWritableMetadataService 将数据存储在本地内存和本地文件。


整体时序图


核心代码在 ServiceConfig#exportRemote 中,具体如下:

  • 注册 service-discovery-registry 协议的入口


private URL exportRemote(URL url, List<URL> registryURLs) {
    if (CollectionUtils.isNotEmpty(registryURLs)) {
        // 如果是多个注册中心,通过循环对每个注册中心进行注册
        for (URL registryURL : registryURLs) {
            // 判断是否是service-discovery-registry协议
            // 将service-name-mapping参数的值设置为true
            if (SERVICE_REGISTRY_PROTOCOL.equals(registryURL.getProtocol())) {
                url = url.addParameterIfAbsent(SERVICE_NAME_MAPPING_KEY, "true");
            }
            ......
            // 注册service-discovery-registry协议复用服务暴露流程
            doExportUrl(registryURL.putAttribute(EXPORT_KEY, url), true);
        }
    ......
    return url;
}


  • invoker 中包装 Metadata


核心代码在 ServiceConfig#doExportUrl 中,具体如下:


private void doExportUrl(URL url, boolean withMetaData) {
    Invoker<?> invoker = PROXY_FACTORY.getInvoker(ref, (Class) interfaceClass, url);
    // 此时的withMetaData的值为true
    // 将invoker包装成DelegateProviderMetaDataInvoker
    if (withMetaData) {
        invoker = new DelegateProviderMetaDataInvoker(invoker, this);
    }
    Exporter<?> exporter = PROTOCOL.export(invoker);
    exporters.add(exporter);
}


  • 通过 RegistryProtocol 将 Invoker 转化成 Exporter


核心代码在 ProtocolListenerWrapper#export 中,具体如下:


public <T> Exporter<T> export(Invoker<T> invoker) throws RpcException {
    // 此时的protocol为RegistryProtocol类型
    if (UrlUtils.isRegistry(invoker.getUrl())) {
        return protocol.export(invoker);
    }
    ......
}


  • RegistryProtocol 将 Invoker 转化成 Exporter 的核心流程


核心代码在 RegistryProtocol#export 中,具体如下:


public <T> Exporter<T> export(final Invoker<T> originInvoker) throws RpcException {
    URL registryUrl = getRegistryUrl(originInvoker);
    URL providerUrl = getProviderUrl(originInvoker);
    ......
    // 再次暴露Triple协议的服务
    final ExporterChangeableWrapper<T> exporter = doLocalExport(originInvoker, providerUrl);
    // registryUrl中包含service-discovery-registry协议
    // 通过该协议创建ServiceDiscoveryRegistry对象
    // 然后组合RegistryServiceListener监听器,
    // 最后包装成ListenerRegistryWrapper对象
    final Registry registry = getRegistry(registryUrl);
    final URL registeredProviderUrl = getUrlToRegistry(providerUrl, registryUrl);
    boolean register = providerUrl.getParameter(REGISTER_KEY, true);
    if (register) {
        // 注册service-discovery-registry协议
        // 触发RegistryServiceListener的onRegister事件
        register(registry, registeredProviderUrl);
    }
    ......
    // 触发RegistryServiceListener的onRegister事件
    notifyExport(exporter);
    return new DestroyableExporter<>(exporter);
}


  • 暴露 Triple 协议的服务


核心代码在 RegistryProtocol#doLocalExport 中,具体如下:


private <T> ExporterChangeableWrapper<T> doLocalExport(final Invoker<T> originInvoker, URL providerUrl) {
    String key = getCacheKey(originInvoker);
    // 此时的protocol为Triple协议的代理类
    // 和暴露injvm协议的PROTOCOL相同
    return (ExporterChangeableWrapper<T>) bounds.computeIfAbsent(key, s -> {
        Invoker<?> invokerDelegate = new InvokerDelegate<>(originInvoker, providerUrl);
        return new ExporterChangeableWrapper<>((Exporter<T>) protocol.export(invokerDelegate), originInvoker);
    });
}


  • 注册service-discovery-registry协议


核心代码在 ServiceDiscoveryRegistry#register和ServiceDiscoveryRegistry#doRegister 中,具体如下:


1、ServiceDiscoveryRegistry#register


public final void register(URL url) {
    // 只有服务端(Provider)才需要注册
    if (!shouldRegister(url)) {
        return;
    }
    // 注册service-discovery-registry协议
    doRegister(url);
}


2、ServiceDiscoveryRegistry#doRegister


public void doRegister(URL url) {
    url = addRegistryClusterKey(url);
    // 注册元数据
    if (writableMetadataService.exportURL(url)) {
        if (logger.isInfoEnabled()) {
            logger.info(format("The URL[%s] registered successfully.", url.toString()));
        }
    } else {
        if (logger.isWarnEnabled()) {
            logger.warn(format("The URL[%s] has been registered.", url.toString()));
        }
    }
}


  • 注册元数据


核心代码在 InMemoryWritableMetadataService#exportURL 中,具体如下:


public boolean exportURL(URL url) {
    // 如果是MetadataService,则不注册元数据
    if (MetadataService.class.getName().equals(url.getServiceInterface())) {
        this.metadataServiceURL = url;
        return true;
    }
    updateLock.readLock().lock();
    try {
        String[] clusters = getRegistryCluster(url).split(",");
        for (String cluster : clusters) {
            MetadataInfo metadataInfo = metadataInfos.computeIfAbsent(cluster, k -> new MetadataInfo(ApplicationModel.getName()));
            // 将Triple协议的服务中接口相关的数据生成ServiceInfo
            // 将ServiceInfo注册到MetadataInfo中
            metadataInfo.addService(new ServiceInfo(url));
        }
        metadataSemaphore.release();
        return addURL(exportedServiceURLs, url);
    } finally {
        updateLock.readLock().unlock();
    }
}


  • 发布 onRegister 事件


核心代码在 ListenerRegistryWrapper#register 中,具体如下:


public void register(URL url) {
    try {
        // registry为ServiceDiscoveryRegistry对象
        // 此时已经调用完ServiceDiscoveryRegistry#registry方法
        registry.register(url);
    } finally {
        if (CollectionUtils.isNotEmpty(listeners) && !UrlUtils.isConsumer(url)) {
            RuntimeException exception = null;
            for (RegistryServiceListener listener : listeners) {
                if (listener != null) {
                    try {
                        // 注册完service-discovery-registry协议后发布onRegister事件
                        listener.onRegister(url, registry);
                    } catch (RuntimeException t) {
                        logger.error(t.getMessage(), t);
                        exception = t;
                    }
                }
            }
            if (exception != null) {
                throw exception;
            }
        }
    }
}


  • 发布服务注册事件


核心代码在 RegistryProtocol#notifyExport 中,具体如下:


private <T> void notifyExport(ExporterChangeableWrapper<T> exporter) {
    List<RegistryProtocolListener> listeners = ExtensionLoader.getExtensionLoader(RegistryProtocolListener.class)
        .getActivateExtension(exporter.getOriginInvoker().getUrl(), "registry.protocol.listener");
    if (CollectionUtils.isNotEmpty(listeners)) {
        for (RegistryProtocolListener listener : listeners) {
            // 发布RegistryProtocolListener的onExport事件
            listener.onExport(this, exporter);
        }
    }
}


我们可以看出注册 service-discovery-registry 协议的核心目的是为了将服务的接口相关的信息存储在内存中。从兼容性和平滑迁移两方面来考虑,社区在实现的时候采取复用 ServiceConfig 的暴露流程的方式。


3、暴露Triple协议服务并注册registry协议


暴露 Triple 协议的服务并注册 registry 协议是 Dubbo 3.0 服务暴露的核心流程,一共分为两部分:

  • 暴露 Triple 协议的服务
  • 注册 registry 协议


由于暴露 Triple 协议服务的流程和暴露 Injvm 协议服务的流程是一致的,所以不再赘述。注册 registry 协议的过程仅仅注册了应用实例相关的信息,也就是之前提到的应用级服务发现机制。


整体时序图


  • 通过 InterfaceCompatibleRegistryProtocol 将 Invoker 转化成 Exporter


核心代码在 ProtocolListenerWrapper#export 中,具体如下:


public <T> Exporter<T> export(Invoker<T> invoker) throws RpcException {
    // 此时的protocol为InterfaceCompatibleRegistryProtocol类型(继承了RegistryProtocol)
    // 注意:在注册service-discovery-registry协议的时候protocol为RegistryProtocol类型
    if (UrlUtils.isRegistry(invoker.getUrl())) {
        return protocol.export(invoker);
    }
    ......
}


  • RegistryProtocol 将 Invoker 转化成 Exporter 的核心流程


核心代码在 RegistryProtocol#export 中,具体如下:


public <T> Exporter<T> export(final Invoker<T> originInvoker) throws RpcException {
    URL registryUrl = getRegistryUrl(originInvoker);
    URL providerUrl = getProviderUrl(originInvoker);
    ......
    // 再次暴露Triple协议的服务
    final ExporterChangeableWrapper<T> exporter = doLocalExport(originInvoker, providerUrl);
    // registryUrl中包含registry协议
    // 通过该协议创建ZookeeperRegistry对象
    // 然后组合RegistryServiceListener监听器,
    // 最后包装成ListenerRegistryWrapper对象
    // 注意:
    // 1. service-discovery-registry协议对应的是ServiceDiscoveryRegistry
    // 2. registry协议对应的是ZookeeperRegistry
    final Registry registry = getRegistry(registryUrl);
    final URL registeredProviderUrl = getUrlToRegistry(providerUrl, registryUrl);
    boolean register = providerUrl.getParameter(REGISTER_KEY, true);
    if (register) {
        // 注册registry协议
        // 触发RegistryServiceListener的onRegister事件
        register(registry, registeredProviderUrl);
    }
    ......
    // 发布RegistryProtocolListener的onExport事件
    notifyExport(exporter);
    return new DestroyableExporter<>(exporter);
}


  • 注册 registry 协议


核心代码在 FailbackRegistry#register 和 ServiceDiscoveryRegistry#doRegister 中(ZookeeperRegistry 继承 FailbackRegistry)中,具体如下:


1、FailbackRegistry#register


public void register(URL url) {
    if (!acceptable(url)) {
        ......
        try {
            // 注册registry协议
            doRegister(url);
        } catch (Exception e) {
            ......
        }
    }
}


2、ServiceDiscoveryRegistry#doRegister


public void doRegister(URL url) {
    try {
        // 在zookeeper上注册Provider
        // 目录:/dubbo/xxxService/providers/***
        // 数据:dubbo://192.168.31.167:20800/xxxService?anyhost=true&
        //      application=application-name&async=false&deprecated=false&dubbo=2.0.2&
        //      dynamic=true&file.cache=false&generic=false&interface=xxxService&
        //      metadata-type=remote&methods=hello&pid=82470&release=&
        //      service-name-mapping=true&side=provider&timestamp=1629588251493
        zkClient.create(toUrlPath(url), url.getParameter(DYNAMIC_KEY, true));
    } catch (Throwable e) {
        throw new RpcException("Failed to register " + url + " to zookeeper " + getUrl() + ", cause: " + e.getMessage(), e);
    }
}


  • 订阅地址变更


核心代码在 FailbackRegistry#subscribe 和 ZookeeperRegistry#doSubscribe 中,具体如下:


1、FailbackRegistry#subscribe


public void subscribe(URL url, NotifyListener listener) {
    ......
    try {
        // 调用ZookeeperRegistry#doSubscribe
        doSubscribe(url, listener);
    } catch (Exception e) {
    ......
}


2、ZookeeperRegistry#doSubscribe


public void doSubscribe(final URL url, final NotifyListener listener) {
    try {
        if (ANY_VALUE.equals(url.getServiceInterface())) {
            ......
        } else {
            ......
            for (String path : toCategoriesPath(url)) {
                ConcurrentMap<NotifyListener, ChildListener> listeners = zkListeners.computeIfAbsent(url, k -> new ConcurrentHashMap<>());
                ChildListener zkListener = listeners.computeIfAbsent(listener, k -> new RegistryChildListenerImpl(url, path, k, latch));
                if (zkListener instanceof RegistryChildListenerImpl) {
                    ((RegistryChildListenerImpl) zkListener).setLatch(latch);
                }
                // 创建临时节点用来存储configurators数据
                // 目录:/dubbo/xxxService/configurators
                // 数据:应用的配置信息,可以在dubbo-admin中进行修改,默认为空
                zkClient.create(path, false);
                // 添加监听器,用来监听configurators中的变化
                List<String> children = zkClient.addChildListener(path, zkListener);
                if (children != null) {
                    urls.addAll(toUrlsWithEmpty(url, path, children));
                }
            }
            ......
        }
    } catch (Throwable e) {
        ......
    }
}


  • 建立暴露的 Triple 协议服务与 Metadata 之间的联系


核心代码在 ServiceConfig#exportUrl、MetadataUtils#publishServiceDefinition、InMemoryWritableMetadataService#publishServiceDefinition、RemoteMetadataServiceImpl#publishServiceDefinition 和 MetadataReport#storeProviderMetadata 中,具体如下:

1、ServiceConfig#exportUrl


private void exportUrl(URL url, List<URL> registryURLs) {
    ......
    if (!SCOPE_NONE.equalsIgnoreCase(scope)) {
        ......
        if (!SCOPE_LOCAL.equalsIgnoreCase(scope)) {
            url = exportRemote(url, registryURLs);
            // 发布事件,更新服务接口相关的数据
            MetadataUtils.publishServiceDefinition(url);
        }
    }
    ......
}


2、MetadataUtils#publishServiceDefinition


public static void publishServiceDefinition(URL url) {
    // 将服务接口相关的数据存在到InMemoryWritableMetadataService中
    WritableMetadataService.getDefaultExtension().publishServiceDefinition(url);
    // 将服务接口相关的数据存在到远端的元数据中心
    if (REMOTE_METADATA_STORAGE_TYPE.equalsIgnoreCase(url.getParameter(METADATA_KEY))) {
        getRemoteMetadataService().publishServiceDefinition(url);
    }
}


3、InMemoryWritableMetadataService#publishServiceDefinition


public void publishServiceDefinition(URL url) {
    try {
        String interfaceName = url.getServiceInterface();
        if (StringUtils.isNotEmpty(interfaceName)
            && !ProtocolUtils.isGeneric(url.getParameter(GENERIC_KEY))) {
            Class interfaceClass = Class.forName(interfaceName);
            ServiceDefinition serviceDefinition = ServiceDefinitionBuilder.build(interfaceClass);
            Gson gson = new Gson();
            String data = gson.toJson(serviceDefinition);
            // 存储服务接口相关数据
            // 数据格式:
            // {
            //   "canonicalName": "xxxService",
            //   "codeSource": "file:/Users/xxxx",
            //   "methods": [{
            //       "name": "hello",
            //       "parameterTypes": ["java.lang.String"],
            //       "returnType": "java.lang.String",
            //       "annotations": []
            //   }],
            //   "types": [{
            //       "type": "java.lang.String"
            //    }],
            //  "annotations": []
            // } 
            serviceDefinitions.put(url.getServiceKey(), data);
            return;
        } else if (CONSUMER_SIDE.equalsIgnoreCase(url.getParameter(SIDE_KEY))) {
            ......
        }
        ......
    } catch (Throwable e) {
        ......
    }
}


4、RemoteMetadataServiceImpl#publishServiceDefinition


public void publishServiceDefinition(URL url) {
    checkRemoteConfigured();
    String side = url.getSide();
    if (PROVIDER_SIDE.equalsIgnoreCase(side)) {
        // 发布服务端(Provider)的服务接口信息到元数据中心
        publishProvider(url);
    } else {
        ......
    }
}
RemoteMetadataServiceImpl#publishProvider
private void publishProvider(URL 

以上是关于深入解析 Dubbo 3.0 服务端暴露全流程的主要内容,如果未能解决你的问题,请参考以下文章

Dubbo原理和源码解析之服务暴露

Dubbo原理何源码解析之服务暴露

Dubbo中编码和解码的解析

Dubbo源码——服务的创建和暴露

堂妹问我:Dubbo的服务暴露过程

面试官问我:解释一下Dubbo服务暴露