Dubbo源码解析-服务注册(五)

devtools/2024/11/20 15:39:19/

一、服务注册

当确定好了最终的服务配置后,Dubbo就会根据这些配置信息生成对应的服务URL,比如:

dubbo://192.168.65.221:20880/org.apache.dubbo.springboot.demo.DemoService?
application=dubbo-springboot-demo-provider&timeout=3000

这个URL就表示了一个Dubbo服务,服务消费者只要能获得到这个服务URL,就知道了关于这个
Dubbo服务的全部信息,包括服务名、支持的协议、ip、port、各种配置。

确定了服务URL之后,服务注册要做的事情就是把这个服务URL存到注册中心(比如Zookeeper)中去,说的再简单一点,就是把这个字符串存到Zookeeper中去,这个步骤其实是非常简单的,实现这个功能的源码在RegistryProtocol中的export()方法中,最终服务URL存在了Zookeeper的/dubbo/接口名/providers目录下。

但是服务注册并不仅仅就这么简单,既然上面的这个URL表示一个服务,并且还包括了服务的一些配置信息,那这些配置信息如果改变了呢?比如利用Dubbo管理台中的动态配置功能(注意,并不是配置中心)来修改服务配置,动态配置可以应用运行过程中动态的修改服务的配置,并实时生效。

如果利用动态配置功能修改了服务的参数,那此时就要重新生成服务URL并重新注册到注册中心,这样服务消费者就能及时的获取到服务配置信息。而对于服务提供者而言,在服务注册过程中,还需要能监听到动态配置的变化,一旦发生了变化,就根据最新的配置重新生成服务URL,并重新注册到中心。

一、接口级注册

一、接口级注册原理

 首先,我们可以通过配置dubbo.application.register-mode来控制:

1. instance:表示只进行应用级注册
2. interface:表示只进行接口级注册
3. all:表示应用级注册和接口级注册都进行,默认

在Dubbo3.0之前,Dubbo是接口级注册,服务注册就是把接口名以及服务配置信息注册到注册中心中,我们把dubbo.application.register-mode设置为interface,看到注册中心(zookeeper)存储的数据格式大概为:

总结来说就是:

接口名1:dubbo://192.168.65.221:20880/接口名1?application=应用名
接口名2:dubbo://192.168.65.221:20880/接口名2?application=应用名
接口名3:dubbo://192.168.65.221:20880/接口名3?application=应用名

key是接口名,value就是服务URL,上面的内容就表示现在有一个应用,该应用下有3个接口,应用实例部署在192.168.65.221,此时,如果给该应用增加一个实例,实例ip为192.168.65.222,那么新的实例也需要进行服务注册,会向注册中心新增3条数据:

接口名1:dubbo://192.168.65.221:20880/接口名1?application=应用名
接口名2:dubbo://192.168.65.221:20880/接口名2?application=应用名
接口名3:dubbo://192.168.65.221:20880/接口名3?application=应用名接口名1:dubbo://192.168.65.222:20880/接口名1?application=应用名
接口名2:dubbo://192.168.65.222:20880/接口名2?application=应用名
接口名3:dubbo://192.168.65.222:20880/接口名3?application=应用名

可以发现,如果一个应用中有3个Dubbo服务,那么每增加一个实例,就会向注册中心增加3条记录,那如果一个应用中有10个Dubbo服务,那么每增加一个实例,就会向注册中心增加10条记录,注册中心的压力会随着应用实例的增加而剧烈增加。
反过来,如果一个应用有3个Dubbo服务,5个实例,那么注册中心就有15条记录,此时增加一个
Dubbo服务,那么注册中心就会新增5条记录,注册中心的压力也会剧烈增加。

所以这就是接口级注册的弊端。

二、源码流程分析

dubbo服务启动的时候首先会来到doExportUrls方法:

private void doExportUrls() {ModuleServiceRepository repository = getScopeModel().getServiceRepository();ServiceDescriptor serviceDescriptor = repository.registerService(getInterfaceClass());providerModel = new ProviderModel(getUniqueServiceName(),ref,serviceDescriptor,this,getScopeModel(),serviceMetadata);repository.registerProvider(providerModel);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);// In case user specified path, register service one more time to map it to path.repository.registerService(pathKey, interfaceClass);doExportUrlsFor1Protocol(protocolConfig, registryURLs);}
}

ConfigValidationUtils.loadRegistries方法根据服务配置获取注册信息registryURLs,内容如下

registry://localhost:2181/org.apache.dubbo.registry.RegistryService?REGISTRY_CLUSTER=registryConfig&application=dubbo-demo-annotation-provider&dubbo=2.0.2&pid=21604&register-mode=interface&registry=zookeeper&timestamp=1731940701781

最终由于url带有registry,所以UrlUtils.isRegistry(invoker.getUrl())为true

public <T> Exporter<T> export(Invoker<T> invoker) throws RpcException {if (UrlUtils.isRegistry(invoker.getUrl())) {return protocol.export(invoker);}FilterChainBuilder builder = getFilterChainBuilder(invoker.getUrl());return protocol.export(builder.buildInvokerChain(invoker, SERVICE_FILTER_KEY, CommonConstants.PROVIDER));
}

dubbo根据url利用spi机制获取到RegistryProtocol,最终调用到export方法,这里会完成服务的暴
漏与注册,服务暴漏就是下面这行代码做的事情,执行完这行代码之后完成服务注册。
ExporterChangeableWrapper<T> exporter = doLocalExport(originInvoker, providerUrl);

public <T> Exporter<T> export(final Invoker<T> originInvoker) throws RpcException {URL registryUrl = getRegistryUrl(originInvoker);// url to export locallyURL providerUrl = getProviderUrl(originInvoker);final URL overrideSubscribeUrl = getSubscribedOverrideUrl(providerUrl);final OverrideListener overrideSubscribeListener = new OverrideListener(overrideSubscribeUrl, originInvoker);Map<URL, NotifyListener> overrideListeners = getProviderConfigurationListener(providerUrl).getOverrideListeners();overrideListeners.put(registryUrl, overrideSubscribeListener);providerUrl = overrideUrlWithConfig(providerUrl, overrideSubscribeListener);//export invokerfinal ExporterChangeableWrapper<T> exporter = doLocalExport(originInvoker, providerUrl);// url to registryfinal Registry registry = getRegistry(registryUrl);final URL registeredProviderUrl = getUrlToRegistry(providerUrl, registryUrl);// decide if we need to delay publishboolean register = providerUrl.getParameter(REGISTER_KEY, true);if (register) {register(registry, registeredProviderUrl);}// register stated url on provider modelregisterStatedUrl(registryUrl, registeredProviderUrl, register);exporter.setRegisterUrl(registeredProviderUrl);exporter.setSubscribeUrl(overrideSubscribeUrl);if (!registry.isServiceDiscovery()) {// Deprecated! Subscribe to override rules in 2.6.x or before.registry.subscribe(overrideSubscribeUrl, overrideSubscribeListener);}notifyExport(exporter);//Ensure that a new exporter instance is returned every time exportreturn new DestroyableExporter<>(exporter);
}

由于我们配置的注册中心为zookeeper,所以Registry registry = getRegistry(registryUrl)获取到的就是ZookeeperRegistry

而register(registry, registeredProviderUrl)就是利用ZookeeperRegistry中的zkClient向zookeeper中写入一条数据:

public void doRegister(URL url) {try {checkDestroyed();zkClient.create(toUrlPath(url), url.getParameter(DYNAMIC_KEY, true));} catch (Throwable e) {throw new RpcException("Failed to register " + url + " to zookeeper " + getUrl() + ", cause: " + e.getMessage(), e);}
}

此时的registeredProviderUrl就是要注册的接口的真实信息

dubbo://192.168.43.38:20880/org.apache.dubbo.demo.GreetingService?anyhost=true&application=dubbo-demo-annotation-provider&background=false&deprecated=false&dubbo=2.0.2&dynamic=true&generic=false&interface=org.apache.dubbo.demo.GreetingService&metadata-type=remote&methods=hello&pid=21604&register-mode=interface&release=&side=provider&timestamp=1731941219264

二、应用级注册

一、应用级注册原理

上面已经分析过,如果一个应用中有N个Dubbo服务,那么每增加一个实例,就会向注册中心增加N条记录;反过来如果有M个实例,此时增加一个Dubbo服务,那么注册中心就会新增M条记录,注册中心的压力会随着应用实例和服务的增加而剧烈增加;

注册中心的数据越多,数据就变化的越频繁,比如修改服务的timeout,那么对于注册中心和应用都需要消耗资源用来处理数据变化,所以为了降低注册中心的压力,Dubbo3.0支持了应用级注册。而一旦采用应用级注册,最终注册中心的数据存储就变成为:

应用名:192.168.65.221:20880
应用名:192.168.65.222:20880

表示在注册中心中,只记录应用所对应的实例信息(IP+绑定的端口),这样只有一个应用的实例增加了,那么注册中心的数据才会增加,而不关心一个应用中到底有多少个Dubbo服务。

这样带来的好处就是,注册中心存储的数据变少了,注册中心中数据的变化频率变小了(那服务的配置如果发生了改变怎么办呢?后面会讲),并且使用应用级注册,使得 Dubbo3 能实现与异构微服务体系如Spring Cloud等在地址发现层面更容易互通, 为连通 Dubbo与其他微服务体系提供可行方案。

应用级注册带来了好处,但是对于Dubbo来说又出现了一些新的问题,比如:原本,服务消费者可以直接从注册中心就知道某个Dubbo服务的所有服务提供者以及相关的协议、ip、port、配置等信息,那现在注册中心上只有ip、port,那对于服务消费者而言:服务消费者怎么知道现在它要用的某个Dubbo服务,也就是某个接口对应的应用是哪个呢?

对于这个问题,在进行服务导出的过程中,会在Zookeeper中存一个映射关系,在服务导出的最后一步,在ServiceConfig的exported()方法中,会保存这个映射关系:

接口名:应用名

这个映射关系存在Zookeeper的/dubbo/mapping目录下,存了这个信息后,消费者就能根据接口
名找到所对应的应用名了。

消费者知道了要使用的Dubbo服务在哪个应用,那也就能从注册中心中根据应用名查到应用的所有实例信息(ip+port),也就是可以发送方法调用请求了,但是在真正发送请求之前,还得知道服务的配置信息,对于消费者而言,它得知道当前要调用的这个Dubbo服务支持什么协议、timeout是多少,那服务的配置信息从哪里获取呢?

之前的服务配置信息是直接从注册中心就可以获取到的,就是服务URL后面,但是现在不行了,现在需要从服务提供者的元数据服务获取,在应用启动过程中会进行服务导出和服务引入,然后就会暴露一个应用元数据服务,其实这个应用元数据服务就是一个Dubbo服务(Dubbo框架内置的,自己实现的),消费者可以调用这个服务来获取某个应用中所提供的所有Dubbo服务以及服务配置信息,这样就能知道服务的配置信息了。后面分析服务引入时,会进一步分析具体细节。

我们可以通过配置dubbo.application.register-mode设置为instance来控制服务为应用级注册。

不管是什么注册,都需要存数据到注册中心,而Dubbo3的源码实现中会根据所配置的注册中心生成两个URL(不是服务URL,可以理解为注册中心URL,用来访问注册中心的):

1、service-discovery-registry://127.0.0.1:2181/org.apache.dubbo.registry.RegistryService?application=dubbospringboot-
demoprovider&
dubbo=2.0.2&pid=13072&qos.enable=false&registry=zookeeper&timestamp=16517555016602、registry://127.0.0.1:2181/org.apache.dubbo.registry.RegistryService?application=dubbo-springboot-demoprovider&
dubbo=2.0.2&pid=13072&qos.enable=false&registry=zookeeper&timestamp=1651755501660

这两个URL只有schema不一样,一个是service-discovery-registry,一个是registry,而registry是
Dubbo3之前就存在的,也就代表接口级服务注册,而service-discovery-registry就表示应用级服务注册。

在服务注册相关的源码中,当调用RegistryProtocol的export()方法处理registry://时,会利用
ZookeeperRegistry把服务URL注册到Zookeeper中去,这个前面已经看到了,这就是接口级注册。

而类似,当调用RegistryProtocol的export()方法处理service-discovery-registry://时,会利用
ServiceDiscoveryRegistry来进行相关逻辑的处理,那是不是就是在这里把应用信息注册到注册中心去呢?并没有这么简单。

1、首先,不可能每导出一个服务就进行一次应用注册,太浪费了,应用注册只要做一次就行了
2、另外,如果一个应用支持了多个端口,那么应用注册时只要挑选其中一个端口作为实例端口就可以了(该端口只要能接收到数据就行)
3、前面提到,应用启动过程中要暴露应用元数据服务,所以在此处也还是要收集当前所暴露的服务配置信息,以提供给应用元数据服务

所以ServiceDiscoveryRegistry在注册一个服务URL时,并不会往注册中心存数据,而只是把服务URL存到到一个MetadataInfo对象中,MetadataInfo对象中就保存了当前应用中所有的Dubbo服务信息(服务名、支持的协议、绑定的端口、timeout等)

前面提到过,在应用启动的最后,才会进行应用级注册,而应用级注册就是当前的应用实例上相关的信息存入注册中心,包括:

1. 应用的名字
2. 获取应用元数据的方式
3. 当前实例的ip和port
4. 当前实例支持哪些协议以及对应的port

确定好实例信息后之后,就进行最终的应用注册了,就把实例信息存入注册中心的/services/应用名目录下:

可以看出services节点下存的是应用名,应用名的节点下存的是实例ip和实例port,而ip和port这个节点中的内容就是实例的一些基本信息。

额外,我们可以配置dubbo.metadata.storage-type,默认是local,可以通过配置改为remote:

dubbo.application.name=dubbo-springboot-demo-provider
dubbo.application.metadata-type=remote

这个配置其实跟应用元数据服务有关系:

1. 如果为local,那就会启用应用元数据服务,最终服务消费者就会调用元数据服务获取到应用元数据信息
2. 如果为remote,那就不会暴露应用元数据服务,那么服务消费者从元数据中心获取应用元数据呢?

元数据中心,它其实就是用来减轻注册中心的压力的,Dubbo会把服务信息完整的存一份到元数据中心,元数据中心也可以用Zookeeper来实现,在暴露完元数据服务之后,在注册实例信息到注册中心之前,就会把MetadataInfo存入元数据中心,比如:

节点内容为:

{"app": "dubbo-demo-annotation-provider","revision": "454d743e98b436191b6d836c12ce06a8","services": {"org.apache.dubbo.demo.DemoService:dubbo": {"name": "org.apache.dubbo.demo.DemoService","protocol": "dubbo","path": "org.apache.dubbo.demo.DemoService","params": {"side": "provider","release": "","methods": "sayHello,sayHelloAsync","deprecated": "false","dubbo": "2.0.2","interface": "org.apache.dubbo.demo.DemoService","service-name-mapping": "true","register-mode": "instance","generic": "false","metadata-type": "remote","application": "dubbo-demo-annotation-provider","background": "false","dynamic": "true","REGISTRY_CLUSTER": "registryConfig","anyhost": "true"}},"org.apache.dubbo.demo.GreetingService:dubbo": {"name": "org.apache.dubbo.demo.GreetingService","protocol": "dubbo","path": "org.apache.dubbo.demo.GreetingService","params": {"side": "provider","release": "","methods": "hello","deprecated": "false","dubbo": "2.0.2","interface": "org.apache.dubbo.demo.GreetingService","service-name-mapping": "true","register-mode": "instance","generic": "false","metadata-type": "remote","application": "dubbo-demo-annotation-provider","background": "false","dynamic": "true","REGISTRY_CLUSTER": "registryConfig","anyhost": "true"}}},"initiated": false
}

这里面就记录了当前实例上提供了哪些服务以及对应的协议。元数据中心和元数据服务提供的功能是一样的,都可以用来获取某个实例的MetadataInfo。

二、源码流程解析

还是来到doExportUrls方法中,可以看到此时url前缀内容为service-discovery-registry

然后来到registryProtocol中的export方法,此时获取到的为ServiceDiscoveryRegistry

那么最终会来到ServiceDiscoveryRegistry的doRegister方法中

public void doRegister(URL url) {// fixme, add registry-cluster is not necessary anymoreurl = addRegistryClusterKey(url);serviceDiscovery.register(url);
}

 最终将url信息进行封装添加到元数据信息中去

public void register(URL url) {metadataInfo.addService(url);
}
public synchronized void addService(URL url) {// fixme, pass in application mode context during initialization of MetadataInfo.if (this.loader == null) {this.loader = url.getOrDefaultApplicationModel().getExtensionLoader(MetadataParamsFilter.class);}List<MetadataParamsFilter> filters = loader.getActivateExtension(url, "params-filter");// generate service level metadataServiceInfo serviceInfo = new ServiceInfo(url, filters);this.services.put(serviceInfo.getMatchKey(), serviceInfo);// extract common instance level paramsextractInstanceParams(url, filters);if (exportedServiceURLs == null) {exportedServiceURLs = new ConcurrentSkipListMap<>();}addURL(exportedServiceURLs, url);updated.compareAndSet(false, true);
}

可以看到将服务url包装为了serviceInfo,存入到了services中

导出完某个Dubbo服务后,就会调用MetadataUtils.publishServiceDefinition把服务接口名:应用名存入元数据中心。

zk上的metadata信息如下:

{"parameters": {},"canonicalName": "org.apache.dubbo.demo.GreetingService","codeSource": "file:/E:/dubbo/dubbo-3.0/dubbo-demo/dubbo-demo-interface/target/classes/","methods": [{"name": "hello","parameterTypes": [],"returnType": "java.lang.String","annotations": []}],"types": [{"type": "java.lang.String"}],"annotations": []
}

服务导出后会调用serviceConfig.exported方法,最终会调用serviceNameMapping.map方法将接口名与应用名的映射关系设置到zk上

protected void exported() {exported = true;List<URL> exportedURLs = this.getExportedUrls();exportedURLs.forEach(url -> {if (url.getParameters().containsKey(SERVICE_NAME_MAPPING_KEY)) {ServiceNameMapping serviceNameMapping = ServiceNameMapping.getDefaultExtension(getScopeModel());try {boolean succeeded = serviceNameMapping.map(url);if (succeeded) {logger.info("Successfully registered interface application mapping for service " + url.getServiceKey());} else {logger.error("Failed register interface application mapping for service " + url.getServiceKey());}} catch (Exception e) {logger.error("Failed register interface application mapping for service " + url.getServiceKey(), e);}}});onExported();
}

当导出了所有的服务后,会执行exportMetadataService(),将MetadataInfo存入元数据中心,并进行元数据服务的暴漏;然后执行registerServiceInstance将实例信息存入注册中心,完成应用注册

public void prepareApplicationInstance() {if (hasPreparedApplicationInstance.get()) {return;}if (isRegisterConsumerInstance()) {exportMetadataService();if (hasPreparedApplicationInstance.compareAndSet(false, true)) {// register the local ServiceInstance if requiredregisterServiceInstance();}}
}

实例信息注册内容,也就是services目录中的内容,格式为如下:
应用名:192.168.65.221:20880

至此,dubbo的应用级服务注册流程完毕。总结一下:
1. 在导出某个Dubbo服务URL时,会把服务URL存入MetadataInfo中
2. 导出完某个Dubbo服务后,就会把服务接口名:应用名存入元数据中心(可以用Zookeeper实现)
3. 导出所有服务后,完成服务引入后,判断要不要启动元数据服务,如果要就进行导出,固定使用Dubbo协议
5. 将MetadataInfo存入元数据中心
6. 确定当前实例信息(应用名、ip、port、endpoint),将实例信息存入注册中心,完成应用注册


http://www.ppmy.cn/devtools/135514.html

相关文章

深度学习中的mAP

在深度学习中&#xff0c;mAP是指平均精度均值(mean Average Precision)&#xff0c;它是深度学习中评价模型好坏的一种指标(metric)&#xff0c;特别是在目标检测中。 精确率和召回率的概念&#xff1a; (1).精确率(Precision)&#xff1a;预测阳性结果中实际正确的比例(TP / …

【Java】Linux、Mac、Windows 安装 Oracle JDK

一、Linux 环境安装JDK 1、下载 根据实际需求&#xff0c;在 Oracle 官网 上下载某版本JDK&#xff08;如 jdk-8u341-linux-x64.tar.gz&#xff09;&#xff0c;再通过文件传输工具&#xff08;如 Finalshell、FileZilla 等&#xff09;丢到服务器上。 2、安装 # 查看是否安…

计算光纤色散带来的相位移动 matlab

需要注意的地方 1.以下内容纯属个人理解&#xff0c;很有可能不准确&#xff0c;请大家仅做参考 2.光速不要直接用3e8 m/s&#xff0c;需要用精确的2.9979.... 3.光的频率无论在真空还是光纤(介质)都是不变的&#xff0c;是固有属性&#xff0c;但是波长lambdac/f在不同的介…

Scala 中迭代器的duplicate方法,toList方法,zip方法

duplicate方法&#xff1a; 复制迭代器 duplicate 返回值是一个元组&#xff0c;有两个数据源一样的&#xff0c;独立的迭代器 迭代器特点&#xff1a;不能回头val list8List("A","B","C")val (it8,it9)list8.iterator.duplicatewhile (it8.has…

opencv kdtree pcl kdtree 效率对比

由于项目中以一个环节需要使用kdtree ,对性能要求比较严苛&#xff0c;所以看看那个kdtree效率高一些。对比了opencv和pcl。 #include <array> #include <deque> #include <fstream> #include <opencv2/highgui.hpp> #include <opencv2/imgproc.hpp…

二本学院非科班天崩开局,如何逆天改命拿下大厂后端

天崩开局&#xff0c;如何破局自救 2021 年高考结束&#xff0c;我当时的第一志愿是计算机科学与技术专业&#xff0c;但是我被录取到了河南工程学院的高分子材料与工程专业。 我去咨询计算机相关行业的从业者&#xff0c;以及多方面网络搜集相关信息之后得到了一个很现实的消…

2024JYU计算机大赛决赛题解

U505016 退休 思路 至少需要的月数为 ⌈ 1000000 a b ⌉ \lceil\frac{1000000}{ab}\rceil ⌈ab1000000​⌉。 当月数为 k k k 时&#xff0c;可以用 ⌊ k 12 ⌋ \lfloor \frac{k}{12}\rfloor ⌊12k​⌋ 年 k % 12 k \% 12 k%12 月表示。 因为每 12 12 12 个月一年&am…

C# 文件及数据流技术

文章目录 1.文件系统操作1.1 常用文件系统类1.2 示例:文件和目录操作2.文件的读取与写入2.1 读取文件内容2.2 写入文件内容2.3 文件的读写优化3.数据流 (Stream)3.1 常用流类3.2 FileStream 示例3.3 MemoryStream 示例4.读写数据的高级技巧4.1 异步文件与流处理4.2 压缩与解压…