前言
三大中心
Dubbo 在 2.7 之前的版本只配备了注册中心,主流使用的注册中心为 zookeeper。
后来 2.7 版本官方进行了三大中心的改造,分别是:注册中心、元数据中心、配置中心。
元数据定义为描述数据的数据,在服务治理中,例如服务接口名,重试次数,版本号等等都可以理解为元数据。在 2.7 之前,元数据一股脑丢在了注册中心之中,带来问题是:推送量大 -> 存储数据量大 -> 网络传输量大 -> 延迟严重。
Dubbo 2.7 进行了大刀阔斧的改动,只将真正属于服务治理的数据发布到注册中心之中,大大降低了注册中心的压力;同时,将全量的元数据发布到另外的元数据中心;配置中心主要是外部化配置的集中存储。
Dubbo3 的一项重要升级是终于支持应用级别的服务注册与发现了,之前一直都是接口级别的,当集群规模比较大的时候,注册中心地址推送的压力骤升,消费端也会随着每一次地址推送消耗大量资源。粒度改为应用级后,推送的数据规模降低了好几个数量级,性能可以得到很好的提升,也更加便于管理和维护。
综上所述,经历了几个版本的改造,Dubbo 源码的复杂度也随之上升了几个量级,如果一上来就啃 3.x 的源码,很容易迷糊。
服务发布与注册
Dubbo3 的源码比较复杂,为了不陷入泥潭,分析的过程只关注:服务元数据的发布、服务名称映射、服务注册。
服务发布与注册的关键节点:
ServiceConfig#doExport 服务暴露
ServiceConfig#doExportUrls
ServiceConfig#exportRemote 暴露给远程服务调用
Protocol#export 根据指定协议暴露服务
Exchanger#bind 数据交换层
Transporter#bind 数据传输层
NettyServer#doOpen 开启Netty服务
MetadataUtils#publishServiceDefinition 发布服务定义 即元数据
AbstractMetadataReport#storeProviderMetadataTask 存储 例如Nacos
ServiceConfig#exported 暴露后事件
ServiceConfig#mapServiceName 映射服务名称
MetadataServiceNameMapping#map 建立 接口名 -> 应用名 的映射
NacosMetadataReport#registerServiceAppMapping 发布到Nacos
开启服务
Dubbo 对服务的描述对象是ServiceConfig
,暴露服务的方法是ServiceConfig#export()
。又因为同一个服务可能会以多个协议的方式发布到多个注册中心,所以会调用doExportUrls()
。
默认情况下,Dubbo 会同时把服务注册到 JVM 和注册中心,本地注册咱们不管,直接看注册到远程,方法是ServiceConfig#exportRemote()
。
服务以什么协议来暴露给远程调用呢?这时候就需要依赖另一个组件了:Protocol。Dubbo会通过Protocol#export()
来根据指定的协议暴露服务。
因为服务暴露的目的是为了给远程调用的,这个过程说白了就是一个数据交换的过程,即 Request 交换 Response,所以需要依赖另一个组件:Exchanger。
Exchanger 数据交换层需要通过网络来传输请求/相应数据,所以这里又会依赖 Transporter 组件,它的职责是负责网络传输的,也就是传输我们的 Request Response对象。对象怎么传输呢?再深入的话,就是 Dubbo 提供的序列化组件了。
Dubbo 默认使用的网络IO组件库是 Netty,所以最后会开启一个 NettyServer。
至此,服务就开启了,但是因为还没有发布出去,所以现在还不会有人调用。
元数据发布
ServiceConfig#exportRemote()
开启服务后,Dubbo 紧接着会发布服务元数据到元数据中心,方法是MetadataUtils#publishServiceDefinition()
。
这里会依赖另一个组件:MetadataReport,方法storeProviderMetadataTask()
会存储服务的元数据,默认是异步存储。
private void storeProviderMetadataTask(MetadataIdentifier providerMetadataIdentifier, ServiceDefinition serviceDefinition) {
try {
if (logger.isInfoEnabled()) {
logger.info("store provider metadata. Identifier : " + providerMetadataIdentifier + "; definition: " + serviceDefinition);
}
allMetadataReports.put(providerMetadataIdentifier, serviceDefinition);
failedReports.remove(providerMetadataIdentifier);
// 服务元数据转JSON
String data = JsonUtils.getJson().toJson(serviceDefinition);
// 存储元数据 即发布 依赖外部存储媒介 例如Nacos
doStoreProviderMetadata(providerMetadataIdentifier, data);
// 是否需要保存到本地文件
saveProperties(providerMetadataIdentifier, data, true, !syncReport);
} catch (Exception e) {
// retry again. If failed again, throw exception.
failedReports.put(providerMetadataIdentifier, serviceDefinition);
metadataReportRetry.startRetryTask();
logger.error(PROXY_FAILED_EXPORT_SERVICE, "", "", "Failed to put provider metadata " + providerMetadataIdentifier + " in " + serviceDefinition + ", cause: " + e.getMessage(), e);
}
}
元数据发布以后,你可以在Nacos控制台看到:
Dubbo 对服务元数据的描述对象是ServiceDefinition
,存储的内容示例:
{
"annotations":[],
"canonicalName":"org.apache.dubbo.demo.GreeterService",
"codeSource":"file:/Users/panchanghe/dev/source/dubbo3.1.10/dubbo-demo/dubbo-demo-triple/target/classes/",
"methods":[
{
"annotations":[
],
"name":"sayHello",
"parameterTypes":[
"org.apache.dubbo.demo.hello.HelloRequest"
],
"parameters":[
],
"returnType":"org.apache.dubbo.demo.hello.HelloReply"
}
],
"parameters":{
"application":"triple-provider",
"dubbo":"2.0.2",
"side":"provider",
"pid":"95178",
"anyhost":"true",
"interface":"org.apache.dubbo.demo.GreeterService",
"bind.ip":"192.168.98.34",
"methods":"clientOrBiStream,sayHello,serverStream",
"background":"false",
"deprecated":"false",
"dynamic":"true",
"service-name-mapping":"true",
"register-mode":"instance",
"qos.enable":"false",
"generic":"false",
"bind.port":"20881",
"timestamp":"1692701933740"
},
"types":[
{
"enums":[
],
"items":[
],
"properties":{
"message":"java.lang.String"
},
"type":"org.apache.dubbo.demo.hello.HelloReply"
}
],
"uniqueId":"org.apache.dubbo.demo.GreeterService@file:/Users/panchanghe/dev/source/dubbo3.1.10/dubbo-demo/dubbo-demo-triple/target/classes/"
}
服务名称映射
提供者改成 应用级服务注册了,但是消费者发起调用的时候,依然只知道接口名,如果按照之前的方式,消费者就会找不到服务。
怎么办呢?Dubbo 提供的解决方案是,提供者再写入一个 接口名 -> 应用名 的映射关系,消费者先根据接口名定位到应用名,再去订阅服务就好了。
服务名称映射是在最后写入的,先开启发布、发布元数据后,再写入映射关系。对应的方法是MetadataReport#registerServiceAppMapping()
:
@Override
public boolean registerServiceAppMapping(String key, String group, String content, Object ticket) {
try {
if (!(ticket instanceof String)) {
throw new IllegalArgumentException("nacos publishConfigCas requires string type ticket");
}
return configService.publishConfigCas(key, group, content, (String) ticket);
} catch (NacosException e) {
logger.warn(REGISTRY_NACOS_EXCEPTION, "", "", "nacos publishConfigCas failed.", e);
return false;
}
}
服务名称映射非常简单,Data Id 是接口名称,Group 是固定的mapping
,内容是应用名。
服务注册
Dubbo3 的服务注册也做了大的改动,因为是应用级别的,所以现在不是在 ServiceConfig 里面注册服务了,而是在外层 ModuleDeployer 注册一次。
方法是ServiceInstanceMetadataUtils#registerMetadataAndInstance()
,最终会依赖外部存储注册服务,例如Nacos。
public static void registerMetadataAndInstance(ApplicationModel applicationModel) {
LOGGER.info("Start registering instance address to registry.");
RegistryManager registryManager = applicationModel.getBeanFactory().getBean(RegistryManager.class);
// register service instance
registryManager.getServiceDiscoveries().forEach(ServiceDiscovery::register);
}
注意看方法名是registerMetadataAndInstance
,为什么是注册元数据和实例呢?元数据不是已经注册过了吗?这里的 Metadata 指的是 应用级别的元数据,不是接口级别的元数据。
服务引用
Dubbo3 的服务引用引入了一个新组件MigrationClusterInvoker
,它主要是为接口级服务注册升级到应用级服务注册用的,可以实现平滑迁移。
另一个改动点就是,因为是应用级服务注册了,但是消费者会根据接口名去引用服务,如果消费者不做处理,是引用不到服务的。所以 Dubbo3 在引用服务前会先调用ServiceNameMapping#getAndListen()
拿接口名去元数据中心匹配应用名,再去注册中心引用服务。
Dubbo3 服务引用的关键节点:
ReferenceConfig#get 引用服务
ReferenceConfig#createProxy 创建代理对象
ReferenceConfig#createInvokerForRemote 创建远程服务Invoker
RegistryProtocol#refer 通过注册中心引用服务
ServiceDiscoveryMigrationInvoker#new 服务发现迁移Invoker 兼容 接口级 -> 应用级
MigrationRuleHandler#refreshInvoker 根据订阅规则 刷新Invoker
DynamicDirectory#subscribe 动态服务订阅
ServiceNameMapping#getAndListen 订阅 接口名映射到应用名(如果是应用级订阅)
Protocol#refer 根据具体的协议引用服务
Invoker#new 创建协议对应的客户端Invoker
ExchangeClient#new 创建Client 建立连接
Cluster#join 集群容错
ProxyFactory#getProxy 基于Invoker创建代理对象
服务引用的入口没变,还是ReferenceConfig#get()
,服务引用的本质是创建代理对象,代理对象来完成底层 RPC 调用,所以会调用ReferenceConfig#createProxy()
创建代理对象。
创建代理对象需要 Invoker,Dubbo 靠 Invoker 发起远程调用,此时会调用Protocol#refer()
来生成 Invoker对象。MigrationRuleHandler#refreshInvoker()
会根据配置的服务订阅规则来生成 Invoker 对象,订阅规则有三种:
- FORCE_INTERFACE:只订阅接口级服务
- APPLICATION_FIRST:双订阅,应用级服务优先
- FORCE_APPLICATION:只订阅应用级服务
如果订阅了应用级服务,紧接着会调用ServiceNameMapping#getAndListen()
把接口名转换为应用名再去注册中心拉取服务列表,最后调用Protocol#refer()
根据指定的协议来应用远程服务,生成对应的 Invoker,此时会和远端建立连接,创建 ExchangeClient。
有了 Invoker,最后就是通过 ProxyFactory 创建代理对象,代理对象会通过 Invoker 按照指定协议发起 RPC 调用。