Dubbo服务导出过程及源码解析

801 阅读12分钟

本次源码解析基于Dubbo 2.7.5版本,从以下三个方面进行解析:

  • 服务导出基本原理分析
  • 服务注册流程源码分析
  • 服务暴露流程源码分析

1. 基本概念

1.1 URL

一般而言我们说的 URL 指的就是统一资源定位符,在网络上一般指代地址,本质上看其实就是一串包含特殊格式的字符串,标准格式如下:

protocol://username:password@host:port/path?key=value&key=value

Dubbo 就是采用 URL 的方式来作为约定的参数类型,被称为公共契约,就是我们都通过 URL 来交互,来交流。

具体参数如下:

  • protocol:指的是 dubbo 中的各种协议,如:dubbo thrift http
  • username/password:用户名/密码
  • host/port:主机/端口
  • path:接口的名称
  • parameters:参数键值对

1.2 SPI机制

前文介绍过SPI的机制,Dubbo SPI机制 这里重点说下@Adaptive注解进行代理的条件,比如:

  • 该⽅法如果是⽆参的,那么则会报错
  • 该⽅法有参数,可以有多个,并且其中某个参数类型是URL,那么则可以进⾏代理
  • 该⽅法有参数,可以有多个,但是没有URL类型的参数,那么则不能进⾏代理
  • 该⽅法有参数,可以有多个,没有URL类型的参数,但是如果这些参数类型,对应的类中存在getUrl⽅法(返回值类型为URL),那么也可以进⾏代理

本次服务暴露过程源码就用到了最后一种代理方式,对应类中存在getUrl方法。

1.3 Exporter结构

⼀个服务导出成功后,会⽣成对应的Exporter:

  • DestroyableExporter:Exporter的最外层包装类,这个类的主要作⽤是可以⽤来unexporter对应的服务
  • ExporterChangeableWrapper:这个类主要负责在unexport对应服务之前,把服务URL从注册中⼼中 移除,把该服务对应的动态配置监听器移除
  • ListenerExporterWrapper:这个类主要负责在unexport对应服务之后,把服务导出监听器移除
  • DubboExporter:这个类中保存了对应服务的Invoker对象,和当前服务的唯⼀标志,当NettyServer 接收到请求后,会根据请求中的服务信息,找到服务对应的DubboExporter对象,然后从对象中得到

Exporter架构

1.4 Invoker架构

  • ProtocolFilterWrapper$CallbackRegistrationInvoker:会去调⽤下层Invoker,下层Invoker执⾏完了之后,会遍历过滤器,查看是否有过滤器实现了ListenableFilter接⼝,如果有,则回调对应的 onResponse⽅法,⽐如TimeoutFilter,当调⽤完下层Invoker之后,就会计算服务的执⾏时间
  • ProtocolFilterWrapper$1:ProtocolFilterWrapper中的过滤器组成的Invoker,利⽤该Invoker,可以执⾏服务端的过滤器,执⾏完过滤器之后,调⽤下层Invoker
  • RegistryProtocol$InvokerDelegate:服务的的委托类,⾥⾯包含了 DelegateProviderMetaDataInvoker对象和服务对应的providerUrl,执⾏时直接调⽤下层Invoker
  • elegateProviderMetaDataInvoker:服务的的委托类,⾥⾯包含了AbstractProxyInvoker对象和 ServiceConfig对象,执⾏时直接调⽤下层Invoker
  • AbstractProxyInvoker:服务接⼝的代理类,绑定了对应的实现类,执⾏时会利⽤反射调⽤服务实现 类实例的具体⽅法,并得到结果

Invoker架构

2. 服务导出原理

2.1 源码入口

由于Dubbo 2.7.5在这部分进行了改动,服务导出的入口不再是ServiceBean中的export()方法了,现在我们就来找一下入口。

还是先从@EnableDubbo注解开始,注意到有一个@EnableDubboLifecycle注解。查看该注解@Import了一个DubboLifecycleComponentRegistrar类。按照Spring的套路,这里还是会执行该类的注册方法,注册一个Bean,来看DubboBootstrapApplicationListener类。

该类继承了OneTimeExecutionApplicationContextEventListener类,而这个类又实现了ApplicationListener类,如果有Spring源码基础的同学可以得知,这是Spring里的事件监听机制,当Spring启动之后,会执行该类的onApplicationEvent方法。

    @Override
    public void onApplicationContextEvent(ApplicationContextEvent event) {
        if (event instanceof ContextRefreshedEvent) {//表示启动事件
            onContextRefreshedEvent((ContextRefreshedEvent) event);
        } else if (event instanceof ContextClosedEvent) {//表示结束事件
            onContextClosedEvent((ContextClosedEvent) event);
        }
    }

再看onContextRefreshedEvent()方法,这里调用了dubboBootstrap.start()

    private void onContextRefreshedEvent(ContextRefreshedEvent event) {
        dubboBootstrap.start();
    }

再看dubboBootstrap.start()方法

public DubboBootstrap start() {
        if (started.compareAndSet(false, true)) {
            //初始化
            initialize();
            if (logger.isInfoEnabled()) {
                logger.info(NAME + " is starting...");
            }
            // 服务导出
            exportServices();

            // 不仅仅注册提供者
            if (!isOnlyRegisterProvider() || hasExportedServices()) {
                // 导出MetadataService
                exportMetadataService();
                //注册本地服务的实例
                registerServiceInstance();
            }
            //刷新服务
            referServices();

            if (logger.isInfoEnabled()) {
                logger.info(NAME + " has started.");
            }
        }
        return this;
    }

找到了服务导出入口exportServices()方法。

2.2 服务概念的演化

2.3 服务导出思路

  • 确定服务的参数
  • 确定服务支持的协议
  • 构建服务最终的URL
  • 将服务URL注册到注册中心
  • 根据服务支持的不同协议,启动不同的Server,用来接收和处理请求
  • 因为Dubbo⽀持动态配置服务参数,所以服务导出时还需要绑定⼀个监听器Listener来监听服务的参数是否有修改,如果发现有修改,则需要重新进⾏导出

3. 服务注册源码分析

首先从服务注册的准备工作开始,在服务注册之前,首先要获取所有的配置信息。

3.1 服务导出源码解析

3.1.1 DubboBootstrap.start()方法

首先从入口方法看

    public DubboBootstrap start() {
        if (started.compareAndSet(false, true)) {
            //初始化服务参数
            initialize();
            if (logger.isInfoEnabled()) {
                logger.info(NAME + " is starting...");
            }
            // 服务导出
            exportServices();

            // Not only provider register
            if (!isOnlyRegisterProvider() || hasExportedServices()) {
                // 2.导出MetadataService
                exportMetadataService();
                //3. 注册本地Service实例
                registerServiceInstance();
            }
            //刷新参数
            referServices();

            if (logger.isInfoEnabled()) {
                logger.info(NAME + " has started.");
            }
        }
        return this;
    }

3.1.2 initialize()方法

该方法进行参数初始化,包含很多小方法,我们一个一个分析。

    private void initialize() {
        if (!initialized.compareAndSet(false, true)) {
            return;
        }
        //初始化参数信息
        ApplicationModel.iniFrameworkExts();
        //初始化配置中心
        startConfigCenter();
        //没有配置配置中心,就把注册中心当做配置中心
        useRegistryAsConfigCenterIfNecessary();
        //开启元数据配置
        startMetadataReport();
        //加载远程配置
        loadRemoteConfigs();
        //检查全局配置
        checkGlobalConfigs();       
        //初始化MetadataService
        initMetadataService();
        //初始化MetadataService服务导出
        initMetadataServiceExporter();
        //初始化事件监听器
        initEventListener();

        if (logger.isInfoEnabled()) {
            logger.info(NAME + " has been initialized!");
        }
    }

3.1.2.1 ApplicationModel.iniFrameworkExts()方法

这个方法主要是从ApplicationModel这里拿配置中心信息,最终放入Map里,由于这里没有进行配置中心的地址,所以此方法跳过。

3.1.2.2 startConfigCenter()方法

这个方法是开启配置中心,所以会执行configManager.refreshAll()方法。

    private void startConfigCenter() {
        Collection<ConfigCenterConfig> configCenters = configManager.getConfigCenters();

        if (CollectionUtils.isNotEmpty(configCenters)) {
            CompositeDynamicConfiguration compositeDynamicConfiguration = new CompositeDynamicConfiguration();
            for (ConfigCenterConfig configCenter : configCenters) {
                configCenter.refresh();
                ConfigValidationUtils.validateConfigCenterConfig(configCenter);
                compositeDynamicConfiguration.addConfiguration(prepareEnvironment(configCenter));
            }
            environment.setDynamicConfiguration(compositeDynamicConfiguration);
        }
        configManager.refreshAll();
    }

refreshAll()方法,这里会调用每个类的refresh,最终会先调用AbstractConfig.refresh的方法,在这里调用服务配置参数,优先级如下: SystemConfiguration -> AppExternalConfiguration -> ExternalConfiguration -> AbstractConfig -> PropertiesConfiguration

可以看出,-D⽅式配置的参数优先级最⾼,配置中⼼次之,注解随后,dubbo.properties最后。

注意到这里,由于我们并没有配置一个配置中心,所以这里还是没有配置中心的具体参数。

    public void refreshAll() {
        write(() -> {
            // refresh all configs here,
            getApplication().ifPresent(ApplicationConfig::refresh);
            getMonitor().ifPresent(MonitorConfig::refresh);
            getModule().ifPresent(ModuleConfig::refresh);

            getProtocols().forEach(ProtocolConfig::refresh);
            getRegistries().forEach(RegistryConfig::refresh);
            getProviders().forEach(ProviderConfig::refresh);
            getConsumers().forEach(ConsumerConfig::refresh);
        });

    }

3.1.2.3 useRegistryAsConfigCenterIfNecessary()方法

这个方法的作用是,如果没有配置配置中心且注册中心是zookeeper的话,就会把zookeeper作为默认的配置中心,赋值给configManager,然后再执行startConfigCenter()方法。

private void useRegistryAsConfigCenterIfNecessary() {
        // we use the loading status of DynamicConfiguration to decide whether ConfigCenter has been initiated.
        if (environment.getDynamicConfiguration().isPresent()) {
            return;
        }

        if (CollectionUtils.isNotEmpty(configManager.getConfigCenters())) {
            return;
        }

        configManager.getDefaultRegistries().stream()
                .filter(registryConfig -> registryConfig.getUseAsConfigCenter() == null || registryConfig.getUseAsConfigCenter())
                .forEach(registryConfig -> {
                    String protocol = registryConfig.getProtocol();
                    String id = "config-center-" + protocol + "-" + registryConfig.getPort();
                    ConfigCenterConfig cc = new ConfigCenterConfig();
                    .....//省略一些给cc赋值的代码
                    configManager.addConfigCenter(cc);
                });
        //开启配置中心        
        startConfigCenter();
    }

3.1.2.4 startMetadataReport()

开启元数据,这里解释一下元数据。元数据定义为描述数据的数据,在服务治理中,例如服务接口名,重试次数,版本号等等都可以理解为元数据。在 2.7 之前,元数据一股脑丢在了注册中心之中,这造成了一系列的问题:

  • 推送量大 -> 存储数据量大 -> 网络传输量大 -> 延迟严重

生产者端注册 30+ 参数,有接近一半是不需要作为注册中心进行传递;消费者端注册 25+ 参数,只有个别需要传递给注册中心。有了以上的理论分析,Dubbo 2.7 进行了大刀阔斧的改动,只将真正属于服务治理的数据发布到注册中心之中,大大降低了注册中心的负荷。

同时,将全量的元数据发布到另外的组件中:元数据中心。元数据中心目前支持 redis(推荐),zookeeper。

这里可以看这篇文章,Dubbo 2.7三大特性

由于本次示例,没有配置metadata-report所以,该方法直接返回。

3.1.2.5 loadRemoteConfigs()方法

这个方法,我猜测如果配置了元数据中心的话,服务的整体参数可能会发生变化,所以这里会再次加载一遍参数。但是如果没有配置元数据中心的话,此方法无用。

3.1.2.6 checkGlobalConfigs()

校验全局配置信息

3.1.2.7 initMetadataService()

初始化元数据中心服务

3.1.2.8 initMetadataServiceExporter

初始化元数据中心导出

3.1.2.9 initEventListener

初始化事件监听器

3.1.3 exportServices()方法

分析完initialize()方法之后,来到了重点方法服务导出方法。

        configManager.getServices().forEach(sc -> {
            // TODO, compatible with ServiceConfig.export()
            ServiceConfig serviceConfig = (ServiceConfig) sc;
            serviceConfig.setBootstrap(this);

            if (exportAsync) {
                ExecutorService executor = executorRepository.getServiceExporterExecutor();
                Future<?> future = executor.submit(() -> {
                    sc.export();
                });
                asyncExportingFutures.add(future);
            } else {
                sc.export();
                exportedServices.add(sc);
            }
        });
    }

重点看export方法

3.1.3.1 export方法

public synchronized void export() {
        if (!shouldExport()) {
            return;
        }

        if (bootstrap == null) {
            bootstrap = DubboBootstrap.getInstance();
            bootstrap.init();
        }
        //检查并更新子配置信息
        checkAndUpdateSubConfigs();

        //初始化服务元数据
        serviceMetadata.setVersion(version);
        serviceMetadata.setGroup(group);
        serviceMetadata.setDefaultGroup(group);
        serviceMetadata.setServiceType(getInterfaceClass());
        serviceMetadata.setServiceInterfaceName(getInterface());
        serviceMetadata.setTarget(getRef());

        if (shouldDelay()) {//需要延迟启动
            DELAY_EXPORT_EXECUTOR.schedule(this::doExport, getDelay(), TimeUnit.MILLISECONDS);
        } else {//立即启动
            doExport();
        }
    }

3.1.3.2 doExport()方法

重点看doExportUrls方法

    protected synchronized void doExport() {
        if (unexported) {
            throw new IllegalStateException("The service " + interfaceClass.getName() + " has already unexported!");
        }
        if (exported) {
            return;
        }
        exported = true;

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

        // dispatch a ServiceConfigExportedEvent since 2.7.4
        dispatch(new ServiceConfigExportedEvent(this));
    }

3.1.3.3 doExportUrls()方法

在这个方法里获取URL列表,并根据协议信息,注册不同的服务。

    private void doExportUrls() {
        ServiceRepository repository = ApplicationModel.getServiceRepository();
        ServiceDescriptor serviceDescriptor = repository.registerService(getInterfaceClass());
        repository.registerProvider(
                getUniqueServiceName(),
                ref,
                serviceDescriptor,
                this,
                serviceMetadata
        );
        //获取URL列表
        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);
            //根据协议名注册不同的服务
            repository.registerService(pathKey, interfaceClass);
            // TODO, uncomment this line once service key is unified
            serviceMetadata.setServiceKey(pathKey);
            doExportUrlsFor1Protocol(protocolConfig, registryURLs);
        }
    }

3.1.3.4 doExportUrlsFor1Protocol

这个方法太长,这里只说关键的,该方法根据host、port等信息,构建了服务对外提供的URL。

String host = findConfigedHosts(protocolConfig, registryURLs, map);
        Integer port = findConfigedPorts(protocolConfig, name, map);
        URL url = new URL(name, host, port, getContextPath(protocolConfig).map(p -> p + "/" + path).orElse(path), map);

该方法还提供服务暴露功能,分两种情况,一种是本地服务、一种是远程服务。

3.1.3.4.1 本地暴露

为什么要有本地暴露呢?因为可能存在同一个 JVM 内部引用自身服务的情况,因此暴露的本地服务在内部调用的时候可以直接消费同一个 JVM 的服务避免了网络间的通信。

来看exportLocal方法

    private void exportLocal(URL url) {
        //从新构建URL,将协议改为injvm
        URL local = URLBuilder.from(url)
                .setProtocol(LOCAL_PROTOCOL)
                .setHost(LOCALHOST_VALUE)
                .setPort(0)
                .build();
        //根据SPI机制,调用InjvmProtocol.export()
        Exporter<?> exporter = protocol.export(
                PROXY_FACTORY.getInvoker(ref, (Class) interfaceClass, local));
        exporters.add(exporter);
        logger.info("Export dubbo service " + interfaceClass.getName() + " to local registry url : " + local);
    }

最终会生成一个InjvmExporter对象,作为服务暴露对象。

3.1.3.4.2 远程暴露

远程暴露会现在URL中加入动态配置参数,然后因为目前的URL里还是registry:前缀的,所以还是会先调用RegistryProtocol.export方法 该方法主要完成了以下几件事

  • ⽣成监听器,监听动态配置中⼼此服务的参数数据的变化,⼀旦监听到变化,则重写服务URL,并且在服务导出时先重写⼀次服务URL
  • 拿到重写之后的URL之后,调⽤doLocalExport()进⾏服务导出,在这个⽅法中就会调⽤DubboProtocol的export⽅法去导出服务了,导出成功后将得到⼀个 ExporterChangeableWrapper
    • 在DubboProtocol的export⽅法中主要要做的事情就是启动NettyServer,并且设置⼀系列的 RequestHandler,以便在接收到请求时能依次被这些RequestHandler所处理
  • 从originInvoker中获取注册中⼼的实现类,⽐如ZookeeperRegistry
  • 将重写后的服务URL进⾏简化,把不⽤存到注册中⼼去的参数去除
  • 把简化后的服务URL调⽤ZookeeperRegistry.registry()⽅法注册到注册中⼼去
  • 最后将ExporterChangeableWrapper封装为DestroyableExporter对象返回,完成服务导出

至此服务导出过程完毕。

3.2 启动Service的过程

其实这个过程是在服务导出的过程中启动的,主要在DubboProtocol的export方法中的openServer方法。

3.2.1 openServer

    private void openServer(URL url) {
        //获取服务地址
        String key = url.getAddress();
        //client can export a service which's only for server to invoke
        boolean isServer = url.getParameter(IS_SERVER_KEY, true);
        if (isServer) {
            ProtocolServer server = serverMap.get(key);
            //双重检查
            if (server == null) {
                synchronized (this) {
                    server = serverMap.get(key);
                    if (server == null) {
                        //创建服务并放入缓存
                        serverMap.put(key, createServer(url));
                    }
                }
            } else {
                // server supports reset, use together with override
                server.reset(url);
            }
        }
    }

3.2.2 createServer()方法

重点看这几行

try {
            server = Exchangers.bind(url, requestHandler);
        } catch (RemotingException e) {
            throw new RpcException("Fail to start server(url: " + url + ") " + e.getMessage(), e);
        }

这里最终会调用NettyTransporter.bind方法

3.2.3 NettyTransporter.bind()方法

    @Override
    public RemotingServer bind(URL url, ChannelHandler listener) throws RemotingException {
        return new NettyServer(url, listener);
    }

3.2.4 NettyServer构造方法

    public NettyServer(URL url, ChannelHandler handler) throws RemotingException {
        // you can customize name and type of client thread pool by THREAD_NAME_KEY and THREADPOOL_KEY in CommonConstants.
        // the handler will be warped: MultiMessageHandler->HeartbeatHandler->handler
        super(ExecutorUtil.setThreadName(url, SERVER_THREAD_POOL_NAME), ChannelHandlers.wrap(handler, url));
    }

3.2.5 AbstractServer构造方法

public AbstractServer(URL url, ChannelHandler handler) throws RemotingException {
        super(url, handler);
        localAddress = getUrl().toInetSocketAddress();

        String bindIp = getUrl().getParameter(Constants.BIND_IP_KEY, getUrl().getHost());
        int bindPort = getUrl().getParameter(Constants.BIND_PORT_KEY, getUrl().getPort());
        if (url.getParameter(ANYHOST_KEY, false) || NetUtils.isInvalidLocalHost(bindIp)) {
            bindIp = ANYHOST_VALUE;
        }
        bindAddress = new InetSocketAddress(bindIp, bindPort);
        this.accepts = url.getParameter(ACCEPTS_KEY, DEFAULT_ACCEPTS);
        this.idleTimeout = url.getParameter(IDLE_TIMEOUT_KEY, DEFAULT_IDLE_TIMEOUT);
        try {
            doOpen();
            if (logger.isInfoEnabled()) {
                logger.info("Start " + getClass().getSimpleName() + " bind " + getBindAddress() + ", export " + getLocalAddress());
            }
        } catch (Throwable t) {
            throw new RemotingException(url.toInetSocketAddress(), null, "Failed to bind " + getClass().getSimpleName()
                    + " on " + getLocalAddress() + ", cause: " + t.getMessage(), t);
        }
        executor = executorRepository.createExecutorIfAbsent(url);
    }

最终会调用NettyServer.doOpen()方法创建Netty服务。

3.3 服务监听

这里只介绍服务监听的作用,如果修改服务动态配置之后,整个同步流程如下:

  • 修改服务动态配置,底层会修改Zookeeper中的数据
  • ServiceConfigurationListener会监听到节点内容的变化,会触发ServiceConfigurationListener的 ⽗类AbstractConfiguratorListener的process(ConfigChangeEvent event)⽅法
  • ConfigChangeEvent表示⼀个事件,事件中有事件类型,还有事件内容(节点内容),还有触发这个 事件的节点名字,事件类型有三个
    • ADDED
    • MODIFIED
    • DELETED
  • 当接收到⼀个ConfigChangeEvent事件后,会根据事件类型做对应的处理
    • ADDED、MODIFIED:会根据节点内容去⽣成override://协议的URL,然后根据URL去⽣成 Configurator, Configurator对象很重要,表示⼀个配置器,根据配置器可以去重写URL
    • DELETED:删除ServiceConfigurationListener内的所有的Configurator
  • ⽣成了Configurator后,调⽤notifyOverrides()⽅法对服务URL进⾏重写

4. 总体流程图

服务导出

参考资料

  1. 《Dubbo系列》-Dubbo服务暴露过程