Dubbo3 源码系列之启动篇--服务导出

1,199 阅读7分钟
日期更新说明
2022年10月1日初版
2022年10月7日添加插图和流程图等

导读

前言

上篇文章《Dubbo3 源码系列 Dubbo“纠葛”(入门篇)》讲述了Dubbo为了兼容Spring的方式并且找到Dubbo启动的入口。本篇文章将继续深入了解,Dubbo启动时服务导出的流程;其核心内容:第一部分是前置工作,主要用于检查参数,组装 URL。第二部分是导出服务,包含导出服务到本地 (JVM),和导出服务到远程两个过程。第三部分是向注册中心注册服务,用于服务发现。

在阅读开始之前希望读者和我一样带着一个问题去阅读,Dubbo3新增了应用级的服务注册的,那么Dubbo是怎么在原来的接口级别的基础上支持的应用级注册呢?

下面是关于接口、应用级注册的配置,zookeeper的应用级注册节点的信息:

-Ddubbo.application.register-mode=all
# 可选值 interface、instance、all,默认是 all,即接口级地址、应用级地址都注册

环境

使用的是Dubbo3.0.7版本的代码,为了方便阅读维护了一个源码注释版本《dubbo-3.0.7_source》。

入口

spring容器入口

在spring的项目中,Dubbo是通过DubboDeployApplicationListener的事件监听扩展机制启动的。

具体代码org.apache.dubbo.config.spring.context.DubboDeployApplicationListener#onApplicationEvent如下:

public void onApplicationEvent(ApplicationContextEvent event) {
    if (nullSafeEquals(applicationContext, event.getSource())) {
        if (event instanceof ContextRefreshedEvent) {
            //Spring 容器启动时触发该事件回调,Dubbo3启动时的入口
            onContextRefreshedEvent((ContextRefreshedEvent) event);
        } else if (event instanceof ContextClosedEvent) {
            // 关闭时回调
            onContextClosedEvent((ContextClosedEvent) event);
        }
    }
}

org.apache.dubbo.config.spring.context.DubboDeployApplicationListener#onContextRefreshedEvent

private void onContextRefreshedEvent(ContextRefreshedEvent event) {
    ModuleDeployer deployer = moduleModel.getDeployer();
    Assert.notNull(deployer, "Module deployer is null");
    // start module Dubbo 启动入口
    Future future = deployer.start();

    // if the module does not start in background, await finish
    if (!deployer.isBackground()) {
        try {
            future.get();
        } catch (InterruptedException e) {
            logger.warn("Interrupted while waiting for dubbo module start: " + e.getMessage());
        } catch (Exception e) {
            logger.warn("An error occurred while waiting for dubbo module start: " + e.getMessage(), e);
        }
    }
}

org.apache.dubbo.config.deploy.DefaultModuleDeployer#start

public synchronized Future start() throws IllegalStateException {
    if (isStopping() || isStopped() || isFailed()) {
        throw new IllegalStateException(getIdentifier() + " is stopping or stopped, can not start again");
    }

    try {
        if (isStarting() || isStarted()) {
            return startFuture;
        }

        onModuleStarting();

        // initialize 初始化
        applicationDeployer.initialize();
        initialize();

        // export services 服务导出
        exportServices();

        // prepare application instance
        // exclude internal module to avoid wait itself
        if (moduleModel != moduleModel.getApplicationModel().getInternalModule()) {
            applicationDeployer.prepareInternalModule();
        }

        // refer services 服务引用
        referServices();
        // 省略....

org.apache.dubbo.config.ServiceConfig#doExport

// 服务导出起点(入口)
protected synchronized void doExport() {
    if (unexported) {
        throw new IllegalStateException("The service " + interfaceClass.getName() + " has already unexported!");
    }
    if (exported) {
        return;
    }

    if (StringUtils.isEmpty(path)) {
        path = interfaceName;
    }
    doExportUrls();
    exported();
}

服务导出

org.apache.dubbo.config.ServiceConfig#doExportUrls这里service-discovery-registry 和 registry 的注册协议

private void doExportUrls() {
    ModuleServiceRepository repository = getScopeModel().getServiceRepository();
    ServiceDescriptor serviceDescriptor;
    final boolean serverService = ref instanceof ServerService;
    if(serverService){
        serviceDescriptor=((ServerService) ref).getServiceDescriptor();
        repository.registerService(serviceDescriptor);
    }else{
        serviceDescriptor = repository.registerService(getInterfaceClass());
    }
    providerModel = new ProviderModel(getUniqueServiceName(),
        ref,
        serviceDescriptor,
        this,
        getScopeModel(),
        serviceMetadata);

    repository.registerProvider(providerModel);

    // 注意:这里放入 service-discovery-registry 和 registry 的注册协议(如果配置双协议)
    List<URL> registryURLs = ConfigValidationUtils.loadRegistries(this, true);

    //多协议注册
    for (ProtocolConfig protocolConfig : protocols) {
        String pathKey = URL.buildKey(getContextPath(protocolConfig)
                .map(p -> p + "/" + path)
                .orElse(path), group, version);
        // stub service will use generated service name
        if(!serverService) {
            // In case user specified path, register service one more time to map it to path.
            repository.registerService(pathKey, interfaceClass);
        }
        doExportUrlsFor1Protocol(protocolConfig, registryURLs);
    }
}

org.apache.dubbo.config.ServiceConfig#exportUrl

private void exportUrl(URL url, List<URL> registryURLs) {
    String scope = url.getParameter(SCOPE_KEY);
    // don't export when none is configured
    if (!SCOPE_NONE.equalsIgnoreCase(scope)) {

        // export to local if the config is not remote (export to remote only when config is remote)
        if (!SCOPE_REMOTE.equalsIgnoreCase(scope)) {
            // 本地导出
            exportLocal(url);
        }

        // export to remote if the config is not local (export to local only when config is local)
        if (!SCOPE_LOCAL.equalsIgnoreCase(scope)) {
            // 远程导出
            url = exportRemote(url, registryURLs);
            if (!isGeneric(generic) && !getScopeModel().isInternal()) {
                MetadataUtils.publishServiceDefinition(url, providerModel.getServiceModel(), getApplicationModel());
            }
        }
    }
    this.urls.add(url);
}

本地导出

本地的源码相对比较简单,使用的是org.apache.dubbo.rpc.protocol.injvm.InjvmProtocol#expor

远程导出

  • org.apache.dubbo.config.ServiceConfig#exportRemote
    URL里面添加了参数,非核心内容
  • org.apache.dubbo.config.ServiceConfig#doExportUrl
private void doExportUrl(URL url, boolean withMetaData) {
    Invoker<?> invoker = proxyFactory.getInvoker(ref, (Class) interfaceClass, url);
    if (withMetaData) {
        invoker = new DelegateProviderMetaDataInvoker(invoker, this);
    }
    // 将URL暴露为 invoker 导出 (debug技巧:scopeModel.getExtensionLoader(org.apache.dubbo.rpc.Protocol.class).getExtension(invoker.getUrl().getProtocol()))
    //  service-discovery-registry --> org.apache.dubbo.rpc.protocol.ProtocolSerializationWrapper#export
    //  registry                   --> org.apache.dubbo.rpc.protocol.ProtocolSerializationWrapper#export(区别是包装的Protocol略有不同)
    Exporter<?> exporter = protocolSPI.export(invoker);
    exporters.add(exporter);
}

这里使用Dubbo的SPI机制有一些不是很好读,使用了Dubbo的SPI的机制包装了,最终是调用了org.apache.dubbo.registry.integration.RegistryProtocol#export

  • service-discovery-registry 协议情况
  • dubbo 协议情况
  • org.apache.dubbo.registry.integration.RegistryProtocol#export
public <T> Exporter<T> export(final Invoker<T> originInvoker) throws RpcException {
    // service-discovery-registry --> 注册URL:service-discovery-registry://127.0.0.1:2181/org.apache.dubbo.registry.RegistryService?REGISTRY_CLUSTER=zk-registry&application=dubbo-springboot-demo-provider&dubbo=2.0.2&pid=4324&qos.enable=false&registry=zookeeper&release=3.0.7&timestamp=1665040235001// 注册URL:zookeeper://127.0.0.1:2181/org.apache.dubbo.registry.RegistryService?REGISTRY_CLUSTER=zk-registry&application=dubbo-springboot-demo-provider&dubbo=2.0.2&pid=15504&qos.enable=false&release=3.0.7&timestamp=1665069137614
    // dubbo -->                      注册URL:zookeeper://127.0.0.1:2181/org.apache.dubbo.registry.RegistryService?REGISTRY_CLUSTER=zk-registry&application=dubbo-springboot-demo-provider&dubbo=2.0.2&pid=15580&qos.enable=false&release=3.0.7&timestamp=1665125331594
    URL registryUrl = getRegistryUrl(originInvoker);
    // url to export locally
    // dubbo://192.168.237.1:20880/org.apache.dubbo.springboot.demo.DemoService?anyhost=true&application=dubbo-springboot-demo-provider&background=false&bind.ip=192.168.237.1&bind.port=20880&deprecated=false&dubbo=2.0.2&dynamic=true&generic=false&interface=org.apache.dubbo.springboot.demo.DemoService&methods=sayHello,sayHelloAsync&pid=15504&qos.enable=false&release=3.0.7&service-name-mapping=true&side=provider&timestamp=1665069137648
    URL providerUrl = getProviderUrl(originInvoker);

    // Subscribe the override data
    // FIXME When the provider subscribes, it will affect the scene : a certain JVM exposes the service and call
    //  the same service. Because the subscribed is cached key with the name of the service, it causes the
    //  subscription information to cover.
    final URL overrideSubscribeUrl = getSubscribedOverrideUrl(providerUrl);
    final OverrideListener overrideSubscribeListener = new OverrideListener(overrideSubscribeUrl, originInvoker);
    Map<URL, NotifyListener> overrideListeners = getProviderConfigurationListener(providerUrl).getOverrideListeners();
    overrideListeners.put(registryUrl, overrideSubscribeListener);
    // 监听器(监听动态配置中⼼此服务的参数数据的变化,⼀旦监听到变化,则重写服务URL,并且在服务导出时先重写⼀次服务URL)

    providerUrl = overrideUrlWithConfig(providerUrl, overrideSubscribeListener);
    //export invoker 服务导出:(DubboProtocol的export⽅法去导出服务)1.启动NettyServer 2.设置⼀系列的 RequestHandler
    final ExporterChangeableWrapper<T> exporter = doLocalExport(originInvoker, providerUrl);

    // url to registry 注意:这里会走两遍:
    // 1.service-discovery-registry://127.0.0.1:2181/org.apache.dubbo.registry.RegistryService?REGISTRY_CLUSTER=zk-registry&application=dubbo-springboot-demo-provider&dubbo=2.0.2&pid=15064&qos.enable=false&registry=zookeeper&release=3.0.7&timestamp=1662996885467
    //  |-ListenRegistryWrapper --> ServiceDiscoveryRegistry
    // 2.zookeeper://127.0.0.1:2181/org.apache.dubbo.registry.RegistryService?REGISTRY_CLUSTER=zk-registry&application=dubbo-springboot-demo-provider&dubbo=2.0.2&pid=16316&qos.enable=false&release=3.0.7&timestamp=1662996607526
    //  |-ZookeeperRegistry
    final Registry registry = getRegistry(registryUrl);
    final URL registeredProviderUrl = getUrlToRegistry(providerUrl, registryUrl);

    // decide if we need to delay publish (provider itself and registry should both need to register)
    boolean register = providerUrl.getParameter(REGISTER_KEY, true) && registryUrl.getParameter(REGISTER_KEY, true);
    if (register) {
        // 注册中心注册(在注册中心添加节点和watcher)
        // 这里有两种情况 
        // 1. 接口级注册会将接口级服务提供者数据直接注册到 Zookeeper上面,服务发现(应用级注册)这里仅仅会将注册数据转换为服务元数据等后面来发布元数据
        // 2. 应用级注册通常添加 metadataInfo 添加基本信息
        register(registry, registeredProviderUrl);
    }

    // register stated url on provider model
    // 服务注册后更新状态
    registerStatedUrl(registryUrl, registeredProviderUrl, register);


    exporter.setRegisterUrl(registeredProviderUrl);
    exporter.setSubscribeUrl(overrideSubscribeUrl);

    if (!registry.isServiceDiscovery()) {
        // Deprecated! Subscribe to override rules in 2.6.x or before.
        registry.subscribe(overrideSubscribeUrl, overrideSubscribeListener);
    }

    // 导出通知(Listener)
    notifyExport(exporter);
    //Ensure that a new exporter instance is returned every time export
    return new DestroyableExporter<>(exporter);
}

org.apache.dubbo.registry.integration.RegistryProtocol#doLocalExport\

private <T> ExporterChangeableWrapper<T> doLocalExport(final Invoker<T> originInvoker, URL providerUrl) {
    String key = getCacheKey(originInvoker);

    return (ExporterChangeableWrapper<T>) bounds.computeIfAbsent(key, s -> {
        Invoker<?> invokerDelegate = new InvokerDelegate<>(originInvoker, providerUrl);
        // 协议导出 scopeModel.getExtensionLoader(org.apache.dubbo.rpc.Protocol.class).getExtension(invokerDelegate)
        return new ExporterChangeableWrapper<>((Exporter<T>) protocol.export(invokerDelegate), originInvoker);
    });
}

主要是1.启动NettyServer 2.设置⼀系列的 RequestHandler;这部分内容和Dubbo2的流程是一样的,参考上图。

org.apache.dubbo.registry.integration.RegistryProtocol#getRegistry

通过RegistryFactory获取注册中心。

  • RegistryFactory 注册中心对象获取
  • AbstractRegistryFactory 模板类型封装注册中心对象获取的基本逻辑,比如缓存和基本的逻辑判断
  • ServiceDiscoveryRegistryFactory 用于创建服务发现注册中心工厂对象 用于创建ServiceDiscoveryRegistry对象
  • ZookeeperRegistryFactory 用于创建ZookeeperRegistry类型对象
  • NacosRegistryFactory Nacos注册中心工厂对象 用于创建NacosRegistry

org.apache.dubbo.registry.integration.RegistryProtocol#register

最终通过SPI分别是:ServiceDiscoveryRegistry、ZookeeperRegistry

private void register(Registry registry, URL registeredProviderUrl) {
    // ServiceDiscoveryRegistry(ListenRegistryWrapper --> FailbackRegistry)
    // ZookeeperRegistry(ListenRegistryWrapper --> FailbackRegistry)
    registry.register(registeredProviderUrl);
}

  • Node 节点信息开放接口 比如节点 url的获取 ,销毁
  • RegistryService 注册服务接口,比如注册,订阅,查询等操作
  • Registry 注册中心接口,是否服务发现查询,注册,取消注册方法
  • AbstractRegistry 注册中心逻辑抽象模板类型,封装了注册,订阅,通知的基本逻辑,和本地缓存注册中心信息的基本逻辑
  • FailbackRegistry 封装了失败重试的逻辑
  • NacosRegistry 封装了以nacos作为注册中心的基本逻辑
  • ServiceDiscoveryRegistry 应用级服务发现注册中心逻辑,现在不需要这种网桥实现,协议可以直接与服务发现交互。ServiceDiscoveryRegistry是一种非常特殊的注册表实现,用于以兼容的方式将旧的接口级服务发现模型与3.0中引入的新服务发现模型连接起来。 它完全符合注册表SPI的扩展规范,但与zookeeper和Nacos的具体实现不同,因为它不与任何真正的第三方注册表交互,而只与过程中ServiceDiscovery的相关组件交互。简而言之,它架起了旧接口模型和新服务发现模型之间的桥梁:register()方法主要通过与MetadataService交互,将接口级数据聚合到MetadataInfo中subscribe() 触发应用程序级服务发现模型的整个订阅过程。-根据ServiceNameMapping将接口映射到应用程序。-启动新的服务发现侦听器(InstanceListener),并使NotifierListener成为InstanceListener的一部分。
  • CacheableFailbackRegistry 提供了一些本地内存缓存的逻辑 对注册中心有用,注册中心的sdk将原始字符串作为提供程序实例返回,例如zookeeper和etcd
  • ZookeeperRegistry Zookeeper作为注册中心的基本操作逻辑封装
  • ServiceDiscoveryRegistry:添加元数据到metadataInfo
public void register(URL url) {
    metadataInfo.addService(url);
}
  • ZookeeperRegistry:通过zkClient 添加数据到zookeeper
public void doRegister(URL url) {
    try {
    checkDestroyed();
    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);
}
}

至此exportServices();基本结束了。

应用级服务zookeeper导出

添加调试堆栈如下图:

org.apache.dubbo.registry.zookeeper.ZookeeperServiceDiscovery#doRegister

public void doRegister(ServiceInstance serviceInstance) {
    try {
    // 调用 curator-x-discovery 进行服务注册
    serviceDiscovery.registerService(build(serviceInstance));
} catch (Exception e) {
    throw new RpcException(REGISTRY_EXCEPTION, "Failed register instance " + serviceInstance.toString(), e);
}
}

添加断点;其调用的就是curator-x-discovery的内部实现(Curator-x- discovery 说明),具体服务发现的注册信息可以通过断点信息如下图:

到这里开头问题基本也有呼应了,答案不言自明了。

dubbo://192.168.237.1:20880/org.apache.dubbo.springboot.demo.DemoService?anyhost=true&application=dubbo-springboot-demo-provider&background=false&bind.ip=192.168.237.1&bind.port=20880&deprecated=false&dubbo=2.0.2&dynamic=true&generic=false&interface=org.apache.dubbo.springboot.demo.DemoService&methods=sayHello,sayHelloAsync&pid=20316&qos.enable=false&release=3.0.7&service-name-mapping=true&side=provider&timestamp=1665028264270

附件(图)