服务自省,Dubbo面向了应用级

1,300 阅读10分钟

微信公众号: 九点半的马拉
路途虽遥远,将来更美好
学海无涯,大家一起加油!

Dubbo是一款很优秀的RPC框架,目前Github的Star数已经达到34.6k,有效的反映出它的受欢迎程度。Dubbo提供高性能的基于代理的远程调用能力,服务以接口为粒度,为开发者屏蔽远程调用底层细节。Dubbo设计的稳定架构为数万服务的稳定运行提供了坚实的基础。

Dubbo的传统架构

对于传统架构,Dubbo主要可以分为3个组件:ConsumerProviderRegistryMonitor组件不是较为重要的组件,主要负责服务相关数据的统计与查询,可以忽略掉。

Provider为服务提供者,在启动时会根据设置的协议(如:Dubbo协议),将服务进行服务暴露,生成对应的Invoker(AbstractProxyInvoker),并存储在HashMap中,key为端口、接口名、接口版本和接口分组组成的字符串。最后将服务元数据信息注册到注册中心。

Consumer为服务消费者,从注册中心订阅要引用的服务元数据信息,封装成DubboInvoker,最后通过动态代理转换成一个代理对象。

在服务消费时,它会通过上述的代理对象进行调用,在调用之前会通过Directory获取所有可以调用的远程服务Invoker列表,然后通过负载均衡策略选择出一个进行调用,在具体的调用之前,它会经过一系列的Filter调用链,可以进行处理上下文,限流,回声检测,超时日志打印等。

传统结构的不足

从上面的分析中我们可以看出,传统的Dubbo架构过多依赖注册中心,当注册的服务元数据信息发生变化时,Consumer通过订阅感知信息变化后,会从注册中心重新拉取信息,如果变化频繁,对其网络也造成了一定的压力。同时,注册中心存储的是接口级别的服务信息,容易造成数据存储容量骤增,较多的信息存在冗余。

现在云原生技术兴起,Spring Cloud等提供了面向应用的服务注册与发现。从Dubbo2.7.5开始,Dubbo开始提出一种服务自省的概念,开始提供面向应用级别的服务注册与发现。此时,服务的维度从接口级别降到了应用级别,注册中心存储应用级别的信息,数据容量大幅度减少。

假设定义了两个服务DemoService和RoadService,传统架构下的节点信息和服务自省下的节点信息如下所示(选用Zookeeper作为注册中心)

上图是传统架构下的注册中心节点。它主要是一种树形结构,该结构分为四层。

/dubbo作为服务信息的根节点

org.apache.dubbo.demo.DemoServiceorg.apache.dubbo.demo.RoadService作为第二级节点,表示服务的全限定接口名,

comsumersconfigcuratorsprovidersroutors为第三级节点,其中:

consumers下的子节点表示多个消费者URL元数据信息;

configcurators下的子节点包含多个用于提供者动态配置URL元数据信息;

providers下的子节点包含多个服务提供者URL元数据信息

routors下的子节点包含用于多个消费者路由策略URL元数据信息。

上图是服务自省机制下的注册中心节点信息。(注意,为了方便,我们将Zookeeper也作为配置中心使用)

在最外层节点中,有两个重要的节点dubboservice

对于service节点

它主要存储的不同应用的元数据信息。比如,dubbo-zookeeper-service-introspection-provider-sample表示一个应用名,它的子节点名是该应用的ip地址,节点值存储的是有关元数据信息。

对于dubbo节点

最重要的是config节点,该节点是配置中心的节点,下面的子节点org.apache.dubbo.spring.boot.sample.consumer.DemoServiceorg.apache.dubbo.spring.boot.sample.consumer.RoadServce表示Dubbo服务接口名,下面的子节点表示对应的提供对应服务的应用名。这样就做到了Dubbo服务接口与应用的映射。

服务自省在服务端和消费端都有体现,出于篇幅的考虑,将服务自省拆分为服务端和消费端,各自用部分篇幅介绍。

服务端的服务自省部分

DubboBootstrap主要处理dubbo所有的配置信息,当调用start方法时,表示dubbo进行启动,服务暴露和服务自省等均在该方法中执行。

执行时机

我们可以使用注解@EnableDubbo开启Dubbo,其中包含了@DubboComponentScan注解。

通过@DubboComponentScan引入类DubboComponentScanRegistrar,后续创建了ServiceAnnotationBeanPostProcessor后置处理器,它实现了BeanDefinitionRegistryPostProcessor接口,Spring容器中的所有Bean注册之后会回调postProcessBeanDefinitionRegistry方法,将@Service注解的服务BeanDefinition注册到spring容器中,Spring容器启动完成后会发布ContextRefreshEvent事件,DubboBootstrapApplicationListener类会监听到该事件,之后调用dubboBootstrap.start方法,此时开启了Dubbo相关逻辑。

我们对其中的几个重要方法进行解释。

1. exportServices方法

在exportServices方法中进行服务暴露,这是一个很重要的额工作。

我们对定义的一个ServiceBean变量进行解释,即上面提到的DemoService。比如:

beanName : ServiceBean:org.apache.dubbo.spring.boot.sample.consumer.DemoService:1.0.0

interfaceName : org.apache.dubbo.spring.boot.sample.consumer.DemoService

ref : 实现该接口的具体类

id: ServiceBean:org.apache.dubbo.spring.boot.sample.consumer.DemoService:1.0.0

获取到具体的ServiceConfig对象后,开始执行它的export方法,进行服务暴露。

在具体的服务暴露之前,会更新一些相关的serviceMetadata,然后调用doExportUrls方法进行暴露。

2. doExportUrls方法

2.1)获取ServiceRepository对象,(该对象记录发布的服务信息,客户端需要访问的服务),从中获取 ServiceDescriptor对象存储暴露的服务

public class ApplicationModel {
        private static final ExtensionLoader<FrameworkExt> LOADER = ExtensionLoader.getExtensionLoader(FrameworkExt.class);
        public static ServiceRepository getServiceRepository() {
                return (ServiceRepository)LOADER.getExtension("repository");
 }
}

public ServiceDescriptor registerService(Class<?> interfaceClazz) {
        return (ServiceDescriptor)this.services.computeIfAbsent(interfaceClazz.getName(), (_k) -> {
            return new ServiceDescriptor(interfaceClazz);
        });
    }

2.2)在ServiceRepository注册服务提供者的详细信息

在ServiceRepository中有一个ConcurrentHashMap,key为服务接口名+":"+group+":"+":"+version组成的,value为ProviderModel,主要包含接口的实现实例,上述的serviceDescriptor,serviceConfig和seviceMetadata

2.3)获取当前服务对应的注册中心实例,本案例中只设置了Zookeeper,其中最重要的一点是会判断是否开启了服务自省,最终的URL的协议头是不一样的,在开启时,添加一个变量service-discovery-registry=service

private static String extractRegistryType(URL url) {
        return UrlUtils.isServiceDiscoveryRegistryType(url) ? "service-discovery-registry" : "registry";
    }
public static boolean isServiceDiscoveryRegistryType(Map<StringString> parameters) {
        return parameters != null && !parameters.isEmpty() ? "service".equals(parameters.get("registry-type")) : false;
    }

开启了服务自省后,对应的URL样例:

service-discovery-registry://127.0.0.1:2181/org.apache.dubbo.registry.RegistryService?application=dubbo-zookeeper-service-introspection-provider-sample&dubbo=2.0.2&metadata-type=composite&pid=14508&qos.enable=false&registry=zookeeper&registry-type=service&release=2.7.8&timestamp=1612096358401

如果是原先普通的话,开头 的协议应该为registry

2.4)获取ProtocolConfig,调用doExportUrlsFor1Protocol方法进行暴露。

2.5)获取元数据注册中心

2.6)封装URL进行本地服务暴露,通过动态代理转换成Invoker,这几步和之前的相同

2.7)调用protocol.export方法进行服务暴露,开启了服务自省,协议为service-discovery-registry

具体调用RegistryProtocol.export方法,里面有一步要将服务信息注册到注册中心,这里有一些区别。

服务自省是获取ServiceDiscoveryRegistry类,而普通的是ZookeeperRegistry

在这里主要简单讲下服务自省下的服务信息注册。

2.8)根据serviceKey找到对应的ProviderModel,将url添加进去

2.9)获取WritableMetadataService,默认是(InMemoryWritableMetadataService),调用publishServiceDefinition方法,将url中的pid,timestamp,bind.ip,bind.port等参数字段移除掉,读取side字段,判断是provider端还是consumer端,根据url生成ServiceDefinition,并生成json字符串,并存放到InMemoryWritableMetadataService的serviceDefinitions(ConcurrentSkipListMap)中。

2.10)发布ServiceConfigExportedEvent事件和ServiceBeanExportedEvent事件

在上面介绍服务自省下的Zookeeper树型图时存在一个/dubbo/mapping/接口名/应用名的路径配置,进行接口名和应用名的映射,那它是在什么时候产生的呢?

这时就要借助上述的ServiceConfigExportedEvent事件。

ServiceNameMappingListener监听该事件,调用onEvent方法

然后调用DynamicConfigurationServiceNameMapping的map方法。 其中,MetadataService服务信息不发布到配置中心,该类在服务自省中发挥着重要的作用,具体的解释在以后的篇幅解释。

private static final List<String> IGNORED_SERVICE_INTERFACES = Arrays.asList(MetadataService.class.getName());
public void map(URL exportedURL) {
        String serviceInterface = exportedURL.getServiceInterface();
        // MetadataService服务信息不发布到配置中心
        if (!IGNORED_SERVICE_INTERFACES.contains(serviceInterface)) {
            String group = exportedURL.getParameter("group");
            String version = exportedURL.getParameter("version");
            String protocol = exportedURL.getProtocol();
            String key = ApplicationModel.getName();
            String content = String.valueOf(System.currentTimeMillis());
            this.execute(() -> {
 // 将服务信息发布到配置中心,可能有多个配置中心  
DynamicConfiguration.getDynamicConfiguration().publishConfig(key, buildGroup(serviceInterface, group, version, protocol), content);
                }

            });
        }
    }

上述基本为服务暴露的过程,继续讲解Bootstrap.start()方法的接下来的步骤。

4.registerServiceInstance方法

当开启服务自省后,MetadataService中会存储暴露的url信息,

此时,会调用内部的exportMetadataService()和registerServiceInstance()方法。

在exportMetadataService方法中,会暴露相关的metadataServiceExporter,主要有两个,即ConfigurableMetadataServiceExporter和RemoteMetadataServiceExporter。

在样例的配置信息设置了dubbo.application.metadata-type=composite信息,默认值为local,

当为local时,暴露ConfigurableMetadataServiceExporter类,具体的暴露过程与上述的暴露过程一致,暴露方法如下所示:

当为composite时,除了暴露上面的exporter,另外也暴露RemoteMetadataServiceExporter类,

在暴露该类时,不会将信息注册到配置中心中,这是与普通的服务的重要区别之一。

之后,执行registerServiceInstance方法。

在上述的Zookeeper树形图中存在一个类似/service/应用名/host:port的路径,通过该路径,我们可以把应用相关的信息记录下来,进而可以面向应用调用,而不是往常的面向接口调用,存储容量大大降低。

在该方法中主要是获取服务端发布的任一一个服务URL,从中提取出host和port,然后与应用名等信息创建一个ServiceInstance,具体结构如下所示:

然后遍历具体的ServiceDiscovery.register方法,样例配置的是ZookeeperServiceDiscovery,将ServiceInstace信息注册到上述路径中。

至此,在服务端的启动部分就介绍结束了,在另一篇中,将会介绍在消费端的服务自省部分。