一、服务注册
当确定好了最终的服务配置后,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®ister-mode=interface®istry=zookeeper×tamp=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®ister-mode=interface&release=&side=provider×tamp=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®istry=zookeeper×tamp=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®istry=zookeeper×tamp=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),将实例信息存入注册中心,完成应用注册