本文基于dubbo 2.7.5版本代码

dubbo在传统服务发现功能的基础上,提供了服务自省模式的服务发现,下面简称为自省服务发现。
如果要使用自省服务发现功能,需要使用如下配置:

dubbo.registry.address=zookeeper://localhost:2181
dubbo.registry.parameters[registry-type]=service

dubbo在启动的时候,会将dubbo实例注册到注册中心,注册中心的ip地址端口以及注册中心的类型由上面的第一行配置指定,这里的注册中心使用的是zk,本文介绍也以zk为例。


文章目录

  • 一、ServiceDiscoveryRegistry
  • 二、ServiceDiscoveryRegistry之服务注册
  • 三、ServiceDiscoveryRegistry之服务订阅
  • 四、ServiceInstance注册


一、ServiceDiscoveryRegistry

在自省服务发现中,服务端注册发布的服务,客户端注册引用的服务,均使用ServiceDiscoveryRegistry完成注册。
下面我们分析一下这个类的构造方法。

public ServiceDiscoveryRegistry(URL registryURL) {
        super(registryURL);
        //创建ServiceDiscovery对象
        //后文详细分析该对象的创建
        this.serviceDiscovery = createServiceDiscovery(registryURL);
        //subscribedServices保存提供对应服务的应用名,
        //可以通过RegistryConfig或者ApplicationConfig的parameters属性设置参数“subscribed-services”值,一般无需设置该参数
        this.subscribedServices = parseServices(registryURL.getParameter(SUBSCRIBED_SERVICE_NAMES_KEY));
        //服务名匹配,实现类是DynamicConfigurationServiceNameMapping
        //serviceNameMapping访问配置中心获取匹配的服务提供者
        this.serviceNameMapping = ServiceNameMapping.getDefaultExtension();
        String metadataStorageType = getMetadataStorageType(registryURL);
        //获取元数据中心对象。默认是InMemoryWritableMetadataService,
        //可以通过参数“dubbo.metadata.storage-type”修改,一共两个值:local和remote
        this.writableMetadataService = WritableMetadataService.getExtension(metadataStorageType);
        //rest协议服务使用,用于合成rest协议服务的URL
        this.subscribedURLsSynthesizers = initSubscribedURLsSynthesizers();
    }

ServiceDiscovery类是自省服务发现中非常重要的类,构造方法中通过createServiceDiscovery创建出ServiceDiscovery对象:

protected ServiceDiscovery createServiceDiscovery(URL registryURL) {
    	//根据入参registryURL中协议使用SPI加载ServiceDiscoveryFactory对象,
    	//通过ServiceDiscoveryFactory创建ServiceDiscovery对象
    	//ServiceDiscoveryFactory只有一个实现类DefaultServiceDiscoveryFactory,SPI的名字是default
    	//方法代码见[2]
        ServiceDiscovery originalServiceDiscovery = getServiceDiscovery(registryURL);
        //使用EventPublishingServiceDiscovery装饰ServiceDiscovery,
        //EventPublishingServiceDiscovery可以在调用ServiceDiscovery的某些方法时,发布对应的事件,这些事件用于打印日志
        //方法代码见[3]
        ServiceDiscovery serviceDiscovery = enhanceEventPublishing(originalServiceDiscovery);
        execute(() -> {
        	//初始化ServiceDiscovery,该方法解析后文。
            serviceDiscovery.initialize(registryURL.addParameter(INTERFACE_KEY, ServiceDiscovery.class.getName())
                    .removeParameter(REGISTRY_TYPE_KEY));  [1]
        });
        return serviceDiscovery;
    }
    [2]
    private ServiceDiscovery getServiceDiscovery(URL registryURL) {
    	//ServiceDiscoveryFactory只有一个实现类DefaultServiceDiscoveryFactory
        ServiceDiscoveryFactory factory = getExtension(registryURL);
        //使用SPI加载ServiceDiscovery实现类,ServiceDiscovery有哪些实现类可以参见文件org.apache.dubbo.registry.client.ServiceDiscovery,文件内容在代码下方。
        //加载哪个实现类由dubbo.registry.address的协议指定,这里使用的协议是zookeeper
        return factory.getServiceDiscovery(registryURL);
    }
    [3]
    private ServiceDiscovery enhanceEventPublishing(ServiceDiscovery original) {
        return new EventPublishingServiceDiscovery(original);
    }

org.apache.dubbo.registry.client.ServiceDiscovery文件内容如下:

file=org.apache.dubbo.registry.client.FileSystemServiceDiscovery
zookeeper=org.apache.dubbo.registry.zookeeper.ZookeeperServiceDiscovery
consul=org.apache.dubbo.registry.consul.ConsulServiceDiscovery
etcd3=org.apache.dubbo.registry.etcd.EtcdServiceDiscovery
nacos=org.apache.dubbo.registry.nacos.NacosServiceDiscovery

上面代码创建ServiceDiscovery对象,在本文中以ZookeeperServiceDiscovery为例,创建出ZookeeperServiceDiscovery对象之后,需要执行[1]处代码对ZookeeperServiceDiscovery初始化。

public void initialize(URL registryURL) throws Exception {
		//创建事件分发器
        this.dispatcher = EventDispatcher.getDefaultExtension();
        //ZookeeperServiceDiscovery实现EventListener接口,
        //本对象监听事件ServiceInstancesChangedEvent,下面代码将本对象注册为监听器
        this.dispatcher.addEventListener(this);
        //使用CuratorFramework连接zk
        this.curatorFramework = buildCuratorFramework(registryURL);
        //获取zk的根路径,自省服务发现使用的数据存储在该目录下。
        //默认根路径是/services,可以通过rootPath参数修改根路径。
        this.rootPath = ROOT_PATH.getParameterValue(registryURL);
        //创建ServiceDiscoveryImpl对象,ServiceDiscoveryImpl是Curator提供的
        this.serviceDiscovery = buildServiceDiscovery(curatorFramework, rootPath);
        //启动ServiceDiscoveryImpl
        this.serviceDiscovery.start();
    }

Curator框架提供服务发现功能,dubbo的自省服务发现基于Curator。Curator的服务发现功能可以参见文章:


ZookeeperServiceDiscovery的initialize方法主要作用是:建立与zk的连接,设置数据存储的根目录,设置本对象监听事件ServiceInstancesChangedEvent。
到此为止,ServiceDiscoveryRegistry对象完全创建完毕。该对象中有两个重要方法:subscribe和register,分别用于服务订阅和服务注册。

二、ServiceDiscoveryRegistry之服务注册

服务注册由ServiceDiscoveryRegistry的register方法完成,在以下两个场景调用该方法:

  1. 服务端暴露服务,将被暴露的服务协议、IP、参数、接口名等信息注册到元数据中心;
  2. 服务端发布MetadataService服务,该服务发布是在DubboBootstrap的start方法中调用的;

下面解析注册过程:

public final void register(URL url) {
        if (!shouldRegister(url)) { //只有服务端才可以注册
            return;
        }
        //调用父类FailbackRegistry的register方法,
        //父类再调用ServiceDiscoveryRegistry的doRegister方法
        //FailbackRegistry的作用是当调用子类的doRegister失败时,FailbackRegistry可以发起重调,默认是每5s调用一次,定时器使用时间轮算法。
        super.register(url);
    }
    protected boolean shouldRegister(URL providerURL) {
        String side = providerURL.getParameter(SIDE_KEY);
        boolean should = PROVIDER_SIDE.equals(side); //只有服务端才可以注册
		//代码删减
        return should;
    }

上面的register方法最终调用ServiceDiscoveryRegistry的doRegister方法:

public void doRegister(URL url) {
		//writableMetadataService默认实现是InMemoryWritableMetadataService,
		//下面的代码exportURL方法将入参url注册到元数据中心
        if (writableMetadataService.exportURL(url)) {
            if (logger.isInfoEnabled()) {
                logger.info(format("The URL[%s] registered successfully.", url.toString()));
            }
            }
        } else {
            if (logger.isWarnEnabled()) {
                logger.info(format("The URL[%s] has been registered.", url.toString()));
            }
        }
    }

doRegister调用WritableMetadataService的exportURL方法将url注册到元数据中心,前面文章介绍WritableMetadataService时,WritableMetadataService有三个实现类:InMemoryWritableMetadataService、RemoteWritableMetadataServiceDelegate、RemoteWritableMetadataService。
RemoteWritableMetadataService的exportURL方法如下:

public boolean exportURL(URL url) {
        return true;
    }

InMemoryWritableMetadataService的exportURL方法如下:

public boolean exportURL(URL url) {
        return addURL(exportedServiceURLs, url);
    }
    //下面的代码将被注册的url添加到SortedSet集合中
    boolean addURL(Map<String, SortedSet<URL>> serviceURLs, URL url) {
        return executeMutually(() -> {
            SortedSet<URL> urls = serviceURLs.computeIfAbsent(url.getServiceKey(), this::newSortedURLs);
            // make sure the parameters of tmpUrl is variable
            return urls.add(url);
        });
    }

RemoteWritableMetadataServiceDelegate的exportURL则是分别调用上述两个类的exportURL方法,当两个类都返回true时,RemoteWritableMetadataServiceDelegate才返回true。
从上面代码分析可以看出,服务注册是将服务端暴露的服务元数据注册到元数据中心,但是仅仅注册到本地内存的元数据中心,这些数据供消费端做服务订阅使用。

三、ServiceDiscoveryRegistry之服务订阅

服务订阅调用subscribe方法,服务订阅在以下场景中调用:

  1. 消费端创建远程服务代理过程中调用subscribe方法建立与远程服务的连接;
  2. 消费端发布MetadataService服务时调用subscribe方法建立与远程服务的连接。

subscribe方法代码如下:

public final void subscribe(URL url, NotifyListener listener) {
        if (!shouldSubscribe(url)) { //判断是否是消费端,只有消费端才可以调用subscribe方法
            return;
        }
        //调用父类FailbackRegistry的subscribe方法
        super.subscribe(url, listener);
    }

super.subscribe调用父类FailbackRegistry的subscribe,然后在父类中调用ServiceDiscoveryRegistry的doSubscribe方法:

public void doSubscribe(URL url, NotifyListener listener) {
        subscribeURLs(url, listener);
    }
    protected void subscribeURLs(URL url, NotifyListener listener) {
		//subscribeURL方法与前文介绍的exportURL方法类似。
        writableMetadataService.subscribeURL(url);
        //查找提供服务的应用名,方法代码见[1]处
        Set<String> serviceNames = getServices(url);
        if (CollectionUtils.isEmpty(serviceNames)) {
            throw new IllegalStateException("Should has at least one way to know which services this interface belongs to, subscription url: " + url);
        }
        //遍历上面找到的应用名,方法代码看下面[2]处
        //subscribeURLs作用是根据应用名找到dubbo实例信息,并根据实例信息建立与服务端的连接
        serviceNames.forEach(serviceName -> subscribeURLs(url, listener, serviceName));
    }
    [1]
    //getServices方法用于查找提供所需服务的应用名,
    //一共有三种查找方式:1、参数“provided-by”指定,2、从配置中心获取,3、参数“subscribed-services”指定
    protected Set<String> getServices(URL subscribedURL) {
        Set<String> subscribedServices = new LinkedHashSet<>();
        //可以通过参数“provided-by”指定应用名,如果有多个应用,应用名之间使用“,”分隔
        String serviceNames = subscribedURL.getParameter(PROVIDED_BY);
        if (StringUtils.isNotEmpty(serviceNames)) {
            subscribedServices = parseServices(serviceNames);
        }
        if (isEmpty(subscribedServices)) {
        	//访问配置中心获取应用名,该应用名是由ServiceNameMappingListener发布到配置中心的
            subscribedServices = findMappedServices(subscribedURL);
            if (isEmpty(subscribedServices)) {
            	//如果上述两种方式都找不到合适的应用,
            	//那么使用参数“subscribed-services”指定的应用,如果有多个应用,之间使用“,”分隔
                subscribedServices = getSubscribedServices();
            }
        }
        return subscribedServices;
    }
    [2]
    protected void subscribeURLs(URL url, NotifyListener listener, String serviceName) {
		//根据应用名从注册中心找到服务实例ServiceInstance,
		//ServiceInstance的注册过程在后文介绍。ServiceInstance里面记录有ip地址,端口等信息
        List<ServiceInstance> serviceInstances = serviceDiscovery.getInstances(serviceName);
		//根据ServiceInstance里面的ip地址等信息,访问服务端的MetadataService服务的getExportedURLs方法,
		//从该方法中得到服务端发布的所有的服务元数据信息,并保存到ServiceDiscoveryRegistry的serviceRevisionExportedURLsCache属性中。
		//将服务元数据信息保存到serviceRevisionExportedURLsCache中的作用是:
		//后续其他的需要引用同一个dubbo实例服务的,便可以直接从
		//serviceRevisionExportedURLsCache获取可用服务元数据信息,而不用访问远程服务。
		//当获取了元数据服务信息后,dubbo根据这些信息建立与服务端的连接,
		//也就是创建连接远程服务的Invoker对象,消费端访问服务时,就是通过这些Invoker对象完成的。
        subscribeURLs(url, listener, serviceName, serviceInstances);
        //设置监听器监听目录/services/应用名,当该目录下数据发生变化时,通过调用subscribeURLs方法
        //重新建立对远程服务的引用
        registerServiceInstancesChangedListener(url, new ServiceInstancesChangedListener(serviceName) {
            @Override
            public void onEvent(ServiceInstancesChangedEvent event) {
                subscribeURLs(url, listener, event.getServiceName(), new ArrayList<>(event.getServiceInstances()));
            }
        });
    }

服务订阅功能总体来说,完成了如下几件事:

  1. 获取提供远程服务的实例信息;
  2. 建立与远程服务的连接;
  3. 监听指定目录,当目录数据发生变化时,重新建立与远程的连接。

四、ServiceInstance注册

消费端做服务订阅时,根据应用名从注册中心查找对应的服务实例ServiceInstance对象。下面介绍一下ServiceInstance对象如何注册到注册中心的。
ServiceInstance对象是服务端做DubboBootstrap启动时,调用registerServiceInstance方法注册的,代码如下:

private void registerServiceInstance() {
		//判断是否使用自省服务发现
        if (CollectionUtils.isEmpty(getServiceDiscoveries())) {
            return;
        }
        ApplicationConfig application = getApplication();
        String serviceName = application.getName();
        //获取服务端发布的任意一个服务URL
        URL exportedURL = selectMetadataServiceExportedURL();
        String host = exportedURL.getHost();
        int port = exportedURL.getPort();
        //创建DefaultServiceInstance对象,该对象持有IP、端口、应用名。
        ServiceInstance serviceInstance = createServiceInstance(serviceName, host, port);
        //遍历ServiceDiscovery对象,每个ServiceDiscovery实现类都由EventPublishingServiceDiscovery封装,
        //所以这里调用的都是EventPublishingServiceDiscovery的register方法
        getServiceDiscoveries().forEach(serviceDiscovery -> serviceDiscovery.register(serviceInstance));
    }
    private ServiceInstance createServiceInstance(String serviceName, String host, int port) {
        this.serviceInstance = new DefaultServiceInstance(serviceName, host, port);
        setMetadataStorageType(serviceInstance, getMetadataType());
        return this.serviceInstance;
    }

registerServiceInstance通过EventPublishingServiceDiscovery调用到ZookeeperServiceDiscovery的register方法:

public void register(ServiceInstance serviceInstance) throws RuntimeException {
        doInServiceRegistry(serviceDiscovery -> {
        	//使用curator框架的ServiceDiscoveryImpl完成dubbo实例的注册,下面[1]处分析build方法
            serviceDiscovery.registerService(build(serviceInstance));
        });
    }
    [1]
    public static org.apache.curator.x.discovery.ServiceInstance<ZookeeperInstance> build(ServiceInstance serviceInstance) {
        ServiceInstanceBuilder builder = null;
        String serviceName = serviceInstance.getServiceName();//应用名
        String host = serviceInstance.getHost();
        int port = serviceInstance.getPort();
        //这里对metadata做一下说明:调用EventPublishingServiceDiscovery的register方法时,发布事件ServiceInstancePreRegisteredEvent,
        //监听器监听到事件后会向serviceInstance的metadata属性中添加一些参数信息,详细参见监听器CustomizableServiceInstanceListener。
        Map<String, String> metadata = serviceInstance.getMetadata();
        String id = generateId(host, port);//id是ip:端口
        ZookeeperInstance zookeeperInstance = new ZookeeperInstance(null, serviceName, metadata);
        try {
        	//创建curator框架的ServiceInstance对象,
        	//ServiceInstance对象存储有:ip、端口、应用名、元数据。
            builder = builder()
                    .id(id)
                    .name(serviceName)
                    .address(host)
                    .port(port)
                    .payload(zookeeperInstance);
        } catch (Exception e) {
            throw new RuntimeException(e);
        }
        return builder.build();
    }

curator框架的ServiceDiscoveryImpl对象将dubbo实例信息发布到 “/services/应用名/ServiceInstance的id属性值” 路径下,比如/services/accountService/192.168.0.102:20880,节点的数据是JSON格式的ServiceInstance对象。

不知道大家有没有注意到:ServiceInstance注册的时候,通过selectMetadataServiceExportedURL方法获取任意一个服务URL,并将该URL的端口注册到注册中心,如果服务以多个协议或者多个端口发布,那么为什么只注册一个端口?
在ServiceDiscoveryRegistry的服务订阅中,dubbo根据应用名从注册中心获取数据并创建ServiceInstance对象,ServiceInstance保存了dubbo实例信息,之后根据ServiceInstance中的IP地址和metadata属性生成连接服务端的URL,代码如下:

//该方法用于创建连接服务端的URL
	public List<URL> build(ServiceInstance serviceInstance) {
		//getMetadataServiceURLsParams方法从入参的metadata属性中查找
		//key=dubbo.metadata-service.url-params的value值,
		//根据value生成了paramsMap,value里面记录的是服务端发布的MetadataService服务信息
		//其中包括了MetadataService服务发布的端口。
		//key=dubbo.metadata-service.url-params的value值是在
		//CustomizableServiceInstanceListener监听到事件ServiceInstancePreRegisteredEvent之后,调用MetadataServiceURLParamsMetadataCustomizer添加到metadata中的。
        Map<String, Map<String, String>> paramsMap = getMetadataServiceURLsParams(serviceInstance);
        List<URL> urls = new ArrayList<>(paramsMap.size());
        String serviceName = serviceInstance.getServiceName();
        String host = serviceInstance.getHost();
        for (Map.Entry<String, Map<String, String>> entry : paramsMap.entrySet()) {
            String protocol = entry.getKey();
            Map<String, String> params = entry.getValue();
            int port = Integer.parseInt(params.get(PORT_KEY));
            URLBuilder urlBuilder = new URLBuilder()
                    .setHost(host)
                    .setPort(port)//设置MetadataService服务的端口
                    .setProtocol(protocol)
                    .setPath(MetadataService.class.getName());
            params.forEach((name, value) -> urlBuilder.addParameter(name, valueOf(value)));
            // add the default parameters
            urlBuilder.addParameter(GROUP_KEY, serviceName);
            urls.add(urlBuilder.build());
        }
        return urls;
    }

创建出链接服务端的URL后,便建立与服务端的连接,访问MetadataService服务,以获取服务端发布的的所有服务。
从这里可以看出上面问题中提到的端口其实没有使用。