Dubbo源码:应用级别注册发现

前言

本章基于dubbo2.7.6。

分析应用级别服务注册与发现特性,使用默认local模式元数据服务。

主要内容包括:

1)rpc服务级别 vs 应用级别

2)DubboBootstrap#start API的作用,整体流程梳理

3)应用级别服务注册发现如何适配rpc服务级别老逻辑

该特性在rpc调用期间没有变更(运行期间也不依赖注册中心),主要在启动阶段和注册表变更阶段做了适配。

现状

2.7.5之前dubbo仅支持rpc服务级别注册与发现。

注册中心以zk为例。

provider在暴露rpc服务阶段,向注册中心注册数据如下,在/dubbo/{rpc服务}/providers下列举了所有服务提供者:

在同一个rpc服务下,会有多个providers,每个provider对应一个url,如:

dubbo://127.0.0.1:20881/org.apache.dubbo.demo.DemoService?anyhost=true&application=heihei-app&deprecated=false&dubbo=2.0.2&dynamic=true&generic=false&interface=org.apache.dubbo.demo.DemoService&methods=sayHello,sayHelloAsync&pid=7451&release=&scope=remote&side=provider×tamp=1679471005075

consumer在引用阶段,根据订阅rpc服务,找到所有providers,用url构造Invoker缓存在RegistryDirectory中。

优势

假设DemoService要进行迁移,从a服务迁移到b服务。

在传统Dubbo中,对于consumer来说不需要关心谁提供了这个服务,不需要进行代码变更。

而用SpringCloud+OpenFeign,如果DemoService迁移,我们就需要改代码。

consumer在rpc调用阶段,从RegistryDirectory选择invoker,调用目标节点的rpc服务。

虽然FeignClient注解支持根据Environment解析占位符,但是本质上consumer对于服务迁移有感知。

劣势

1)和当下的流行的服务注册发现模型不匹配

比如SpringCloud服务注册发现的模型都是基于应用级别,每个ServiceInstance代表一个应用实例。

2)注册中心压力大

压力大体现在几个方面:

如果1个应用有n个实例,每个实例提供m个rpc服务,则注册中心会有n*m条数据;

如果1个实例下线,该实例有n个rpc服务,每个rpc服务有m个consumer,则注册中心需要通知n*m个consumer;

单从zk的znode存储来看,providers的url包含了大量数据,随着特性越来越多,数据也越来越多;

DubboBootstrap

针对于上述基于rpc服务级别服务注册发现带来的问题,dubbo提出了服务自省架构。

笔者认为通俗来说,一个是应用级别服务注册发现,另一个是元数据服务

为了支撑新的服务自省架构,在用户侧提供了全新的DubboBootstrap api,统一管理dubbo的配置和启动时序。

案例

服务提供方:

ServiceConfig service = new ServiceConfig<>();
service.setInterface(DemoService.class);
service.setRef(new DemoServiceImpl());
// DubboBootstrap单例对象创建
DubboBootstrap bootstrap = DubboBootstrap.getInstance();
RegistryConfig registryConfig = new RegistryConfig("zookeeper://127.0.0.1:2181");
registryConfig.setParameters(new HashMap<>());
// 【关键】 开启【应用级别】服务注册发现
registryConfig.getParameters().put(
        RegistryConstants.REGISTRY_TYPE_KEY/*registry-type*/,
        RegistryConstants.SERVICE_REGISTRY_TYPE/*service*/);
bootstrap.application(new ApplicationConfig("hahaha-app"))
        .registry(registryConfig)
        .service(service)
        .start() // 启动dubbo
        .await();
复制代码

服务消费方:

ReferenceConfig reference = new ReferenceConfig<>();
reference.setInterface(DemoService.class);
// 【关键】设置rpc服务提供方应用名称(可省略)
// reference.setProvidedBy("hahaha-app");
RegistryConfig registryConfig = new RegistryConfig("zookeeper://127.0.0.1:2181");
// 【关键】 开启【应用级别】服务注册发现
registryConfig.setParameters(new HashMap<>());
registryConfig.getParameters().put(
        RegistryConstants.REGISTRY_TYPE_KEY/*registry-type*/,
        RegistryConstants.SERVICE_REGISTRY_TYPE/*service*/);
// DubboBootstrap单例对象创建
DubboBootstrap bootstrap = DubboBootstrap.getInstance();
bootstrap.application(new ApplicationConfig("service-consumer-app"))
        .registry(registryConfig)
        .reference(reference)
        .start(); // 启动dubbo
DemoService demoService = ReferenceConfigCache.getCache().get(reference);
String message = demoService.sayHello("dubbo");
System.out.println(message);
复制代码

效果

基于上述改动,provider注册到zk里的数据如下。

rpc服务->应用列表:

应用->应用实例列表:

应用实例信息:

源码概览

DubboBootstrap#application、registry、service、reference都是往全局ConfigManager里存配置。

ConfigManager在第二章介绍过,用于统一存储AbstractConfig

DubboBootstrap#start控制Dubbo客户端整体启动流程,一共分为5步:

1)DubboBootstrap#initialize:初始化,之前讲传统服务暴露与引用时说到过,就是初始化全局Envrionment;

2)DubboBootstrap#exportServices:暴露rpc服务,底层循环调用ServiceConfig#export;

3)DubboBootstrap#exportMetadataService:如果启用应用级别服务注册,暴露元数据rpc服务;

4)DubboBootstrap#registerServiceInstance:如果启用应用级别服务注册,向注册中心注册应用实例;

5)DubboBootstrap#referServices:引用rpc服务,底层循环调用ReferenceConfig#get;

接下来除了第一步,分析后面四步。

rpc服务暴露

rpc服务注册

ServiceConfig#doExportUrls:在执行暴露逻辑之前,构造注册url发生变更。

ConfigValidationUtils#loadRegistries:如果设置了registry-type=service,则注册协议会变为service-discovery-registry,而不是原来的registry协议。

注册协议变更,不走RegistryProtocol,走ServiceDiscoveryRegistryProtocol继承RegistryProtocol。

但是新的实现ServiceDiscoveryRegistryProtocol并没有重写export主方法,所以整体暴露流程还是和原来一致:

1)开启nettyserver

2)服务注册(发生变更)

由于ServiceDiscoveryRegistryProtocol#getRegistryUrl重写,所以最终Registry实现发生变更

其实这个方法不重写,逻辑上也没有问题,主要是消费侧的那个getRegistryUrl要重写。

RegistryProtocol#register:这里registryUrl的协议从zookeeper变为service-discovery-registry。

原来工厂会创建一个具体的注册中心Registry,如ZookeeperRegsitry

现在变更为service-discovery-registry实现,工厂创建ServiceDiscoveryRegistry

ServiceDiscoveryRegistry内部url协议是具体注册中心协议,如zookeeper。

ServiceDiscoveryRegistry调用元数据服务,暴露url。

元数据服务分为两大类,一类是local本地内存元数据服务,一类是远程remote中心化元数据服务(比如zk、nacos)。

默认使用InMemoryWritableMetadataService,本地元数据服务

将暴露url存储到内存map中,key是serviceKey(接口+分组+版本),value是原始暴露url。

相当于将原来注册到zk的providers节点下的url,注册到了内存中

rpc服务与应用映射

在服务暴露之后,ServiceConfig#exported发送服务暴露完成事件。

public synchronized void export() {
    // 【step1】2.7.5新单例api 初始化Environment全局配置
    if (bootstrap == null) {
        bootstrap = DubboBootstrap.getInstance();
        bootstrap.init();
    }
    // 【step2】serviceConfig二次填充并校验
    checkAndUpdateSubConfigs();
    // 【step3】暴露【核心】
    doExport();
    // 2.7.5 发布ServiceConfigExportedEvent事件
    exported();
}
复制代码

ServiceNameMappingListener调用ServiceNameMapping#map。

默认实现DynamicConfigurationServiceNameMapping

用配置中心保存rpc服务和应用的映射关系

对于zk来说,保存znode=/dubbo/config/mapping/rpc服务/应用名。

为什么在没有设置配置中心的情况下,这里会走zk来存储?

因为在DubboBoostrap#initialize中做了特殊处理。

如果用户没有设置配置中心,且注册中心具备配置中心能力(zk、consul、nacos),就会用注册中心作为配置中心使用。

元数据服务暴露

回到DubboBoostrap#start,在所有rpc服务暴露完成后,执行DubboBoostrap#exportMetadataService。

底层ConfigurableMetadataServiceExporter将Dubbo自己内部的MetadataService暴露为rpc服务。

需要注意几个细节。

第一,由于使用ConfigManager管理的全局注册中心配置,所以这里registry-type还是service,暴露流程和应用级别服务注册一致。

区别在于MetadataService不会向配置中心发布rpc服务和应用的映射关系。

即对于zk配置中心,/dubbo/config/mapping下不会存在MetadataService。

第二,MetadataService固定使用Dubbo协议,且端口从20880向上找到一个未被占用的port。

这意味着通讯层会为了MetadataService会新开一个NettyServer。

元数据rpc服务和业务rpc服务底层通讯io线程和业务线程都是隔离的。

注册应用实例

回到DubboBootstrap#start,在暴露元数据服务之后,注册应用实例。

DubboBootstrap#registerServiceInstance:

1)获取serviceName、host、port,封装ServiceInstance应用实例模型;

2)向注册中心注册ServiceInstance;

一个注册中心对应一个ServiceDiscoveryRegistry。

每个ServiceDiscoveryRegistry会根据实际注册中心协议,创建一个ServiceDiscovery实例。

public class ServiceDiscoveryRegistry extends FailbackRegistry {

    private final ServiceDiscovery serviceDiscovery;

    public ServiceDiscoveryRegistry(URL registryURL) {
        super(registryURL);
        this.serviceDiscovery = createServiceDiscovery(registryURL);
        // ...
    }
}
复制代码

比如zookeeper对应ZookeeperServiceDiscovery

ZookeeperServiceDiscovery利用Curator客户端扩展包curator-x-discovery实现应用实例注册。

正因为使用应用级别注册,curator-x-discovery可以直接使用。

相比较rpc服务级别注册,dubbo需要自己写逻辑。

curator-x-discovery底层创建"/services/应用/实例地址"临时znode。

rpc服务引用

主流程

rpc服务引用,和provider一样,由于配置了registry-type=service,底层Registry变为ServiceDiscoveryRegistry,导致服务引用行为发生变更。

RegistryProtocol#doRefer:主流程不变

1)Directory#subscribe:向注册中心订阅并注册监听

2)Directory#subscribe:首次同步刷新RegistryDirectory中的内存注册表(invokers)

3)Cluster#join:将RegistryDirectory通过Cluster封装为Invoker

区别在于Registry从ZookeeperRegistry变为ServiceDiscoveryRegistry

RegistryDirectory#subscribe:调用ServiceDiscoveryRegistry。

ServiceDiscoveryRegistry#subscribeURLs:这里是应用级别服务发现的核心逻辑入口

1)InMemoryWritableMetadataService#subscribeURL:在元数据服务内存中存储订阅url

2)ServiceDiscoveryRegistry#getServices:根据订阅rpc服务找应用

优先走用户配置reference.providedBy指定的应用(可多个);

否则走配置中心,取serviceKey对应所有应用;

比如zk取/dubbo/config/mapping/rpcService下的所有应用。

3)ServiceDiscoveryRegistry#subscribeURLs:

找应用的应用实例,构造providerUrls,通知RegistryDirectory。

根据应用找应用实例比较简单,对于zk来说,就是找"/services/应用"下的所有节点,构造ServiceInstance实例。

接下来是核心:

ServiceDiscoveryRegistry#getExportedURLs:找对应元数据,拼接rpc服务级别url,从而实现适配老逻辑

RegistryDirectory#notify:老逻辑,根据providerUrls构造Rpc协议Invoker,放入内存;

怎么适配

为了适配rpc服务级别注册发现,实现的关键是将现有的信息转换为原来注册中心的providerUrl,比如zk中/dubbo/{rpc服务}/providers存储的urls。

暴力

循环所有ServiceInstance,调用对应ServiceInstance的MetadataService Rpc服务,获取对端暴露的url。

但是一般情况下,同一个应用提供的所有rpc服务都是一样的。

revision

所以这里会提出一个revision的概念,revision相同代表元数据相同。

比如rpc方法A,在不同应用实例中,配置的超时时间可能不同,就会导致元数据不同。

那么相同revision的情况下,多个ServiceInstance只需要调用一次MetadataService,调用次数取决于revision数量。

在provider应用实例注册之前会计算一个revision,代表当前ServiceInstance负责的所有rpc服务信息。

URLRevisionResolver#resolve:

1)统计所有rpc服务参数、rpc服务方法,整理成一个有序String集合

2)用URLRevisionResolver#hashCode计算哈希值

3)将所有哈希值求和作为最终的revision

其实也比较容易想到,这个revision就是提取当前应用实例的关键信息,形成一个摘要,比如md5等算法也可以。

将这个revision连同ServiceInstance一同注册到注册中心,给consumer拼接provider的url做准备。

比如zk注册中心/services/hahaha-app/127.0.0.1:21880节点数据包含dubbo.exported-services.revision,就是上面计算出来的revision。

对于consumer来说,同一个revision对应同样的元数据,对应多个url(多个rpc服务),这些url称为模板url。

ServiceDiscoveryRegistry#serviceRevisionExportedURLsCache:缓存模板url,应用名->revision->模板urls。

根据模板url,多个ServiceInstance只需要改变host和port就能构造出多个providerUrl,适配老逻辑的同时减少了MetadataService远程调用。

热点问题

如果/services/hahaha-app节点下有多个应用实例,且revision相同。

对于consumer来说请求谁来获取元数据呢?

如果每个consumer都走第一个实例获取,那么将会导致热点问题。

所以目前dubbo的实现是随机选实例调用MetadataService。

适配逻辑

整体流程如下。

ServiceDiscoveryRegistry#getExportedURLs:

1)计算revision对应模板url

1-1)去除缓存中过期的revision

1-2)重新根据revision初始化模板urls

2)根据模板url,改变host和port,得到所有url

ServiceDiscoveryRegistry#prepareServiceRevisionExportedURLs:

去除过期revision

ServiceDiscoveryRegistry#expungeStaleRevisionExportedURLs:

从缓存里获取应用名对应revision和模板urls,删除注册中心中不存在的revision,保留注册中心中存在的revision。

ServiceDiscoveryRegistry#initializeRevisionExportedURLs:初始化revision对应模板url。

初始化模板url

ServiceDiscoveryRegistry#initializeSelectedRevisionExportedURLs:

为了避免热点问题,会先走RandomServiceInstanceSelector#select随机选择ServiceInstance调用MetadataService获取暴露urls,这部分忽略,我们直接看公共方法initializeRevisionExportedURLs。

随机选实例之后,还有可能有revision的模板url没覆盖到,所以循环所有ServiceInstance再来走一次initializeRevisionExportedURLs。

ServiceDiscoveryRegistry#initializeRevisionExportedURLs:

判断缓存中是否有ServiceInstance.revision对应模板urls,

如果没有,需要走rpc调用ServiceInstance对应MetadataService#getExportedURLs()获取对端暴露的所有rpc服务urls。

这里MetadataService是一个rpc服务代理,和普通referenceConfig差不多,不深入分析。

provider会收到consumer查询暴露url的rpc请求,返回自己暴露的所有url,即InMemoryWritableMetadataService#exportedServiceURLs。

根据模板url构造所有url

ServiceDiscoveryRegistry#cloneExportedURLs:最终根据模板urls构造所有provider暴露url,比如:

dubbo://127.0.0.1:20990/org.apache.dubbo.demo.DemoService,

dubbo://127.0.0.1:20880/org.apache.dubbo.demo.DemoService。

总结

本章分析了应用级别服务注册与发现的特性。

启用该特性需要两步

1)使用DubboBootstrap Api,统一管理dubbo启动;

2)RegistryConfig需要配置registry-type=service;

启用该特性后zk中的注册数据会发生变化

启用前,在/dubbo/{rpc服务}/providers下列举了所有服务提供者。

启用后,数据被分为三份

1)rpc服务->应用列表:/dubbo/config/mapping/{rpc服务}/{应用} - 注册时间

2)应用->应用实例列表:/services/{应用}/{实例地址} - 实例信息

3)元数据都:放在内存InMemoryWritableMetadataService本地内存元数据服务中

最关键的是当前实例暴露的所有url,即原来/dubbo/{rpc服务}/providers下当前实例暴露的urls。

应用级别会适配老逻辑,所有适配在启动阶段完成(1-7),整体流程如下:

Provider侧:

1)ServiceConfig#doExport:暴露rpc服务,开启底层通讯Server

2)ServiceNameMapping#map:向配置中心发布rpc服务和应用名称的关系

3)DubboBootstrap#exportMetadataService:暴露内置的元数据rpc服务,开启底层通讯Server

4)DubboBootstrap#registerServiceInstance:向注册中心注册应用实例

Consumer侧:

5)ServiceDiscoveryRegistry#getServices:根据rpc服务,从配置中心获取应用列表

比如zk取/dubbo/config/mapping/{rpc服务}下的所有znode

注:设置ReferenceConfig.providedBy可以指定应用列表,则可以不依赖配置中心。

6)ServiceDiscoveryRegistry#subscribeURLs:根据5的应用列表,从注册中心获取应用下所有实例

比如zk取/services/{应用名}下所有znode

7)ServiceDiscoveryRegistry#getExportedURLs:新老逻辑适配的核心。

对6的部分应用实例,发起rpc调用MetadataService#getExportedURLs,获取provider暴露url,拼接后通知RegistryDirectory构建invoker。

rpc调用次数取决于第六步中应用实例的revision数量

8)rpc调用阶段,所有逻辑不变

展开阅读全文

页面更新:2024-04-10

标签:级别   发现   初始化   底层   源码   实例   逻辑   模板   协议   数据   中心

1 2 3 4 5

上滑加载更多 ↓
推荐阅读:
友情链接:
更多:

本站资料均由网友自行发布提供,仅用于学习交流。如有版权问题,请与我联系,QQ:4156828  

© CopyRight 2020-2024 All Rights Reserved. Powered By 71396.com 闽ICP备11008920号-4
闽公网安备35020302034903号

Top