本章基于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包含了大量数据,随着特性越来越多,数据也越来越多;
针对于上述基于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;
接下来除了第一步,分析后面四步。
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,注册到了内存中。
在服务暴露之后,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服务引用,和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相同代表元数据相同。
比如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:
ServiceDiscoveryRegistry#expungeStaleRevisionExportedURLs:
从缓存里获取应用名对应revision和模板urls,删除注册中心中不存在的revision,保留注册中心中存在的revision。
ServiceDiscoveryRegistry#initializeRevisionExportedURLs:初始化revision对应模板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。
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
本站资料均由网友自行发布提供,仅用于学习交流。如有版权问题,请与我联系,QQ:4156828
© CopyRight 2020-2024 All Rights Reserved. Powered By 71396.com 闽ICP备11008920号-4
闽公网安备35020302034903号