背景
随着云原生时代的到来,Dubbo 3.0 的一个很重要的目标就是全面拥抱云原生。正因如此,Dubbo 3.0 为了能够更好的适配云原生,将原来的接口级服务发现机制演进为应用级服务发现机制。
基于应用级服务发现机制,Dubbo 3.0 能大幅降低框架带来的额外资源消耗,大幅提升资源利用率,主要体现在:
- 单机常驻内存下降 75%
- 能支持的集群实例规模以百万计的集群
- 注册中心总体数据量下降超 90%
目前关于 Dubbo 服务端暴露流程的技术文章很多,但是都是基于 Dubbo 接口级服务发现机制来解读的。在 Dubbo 3.0 的应用级服务发现机制下,服务端暴露流程与之前有很大的变化,本文希望可以通过 对Dubbo 3.0 源码理解来解析服务端暴露全流程。
什么是应用级服务发现
简单来说,以前 Dubbo 是将接口的信息全部注册到注册中心,而一个应用实例一般会存在多个接口,这样一来注册的数据量就要大很多,而且有冗余。应用级服务发现的机制是同一个应用实例仅在注册中心注册一条数据,这种机制主要解决以下几个问题:
- 对齐主流微服务模型,如:Spring Cloud
- 支持 Kubernetes native service,Kubernetes 中维护调度的服务都是基于应用实例级,不支持接口级
- 减少注册中心数据存储能力,降低了地址变更推送的压力
假设应用 dubbo-application 部署了 3 个实例(instance1, instance2, instance3),并且对外提供了 3 个接口(sayHello, echo, getVersion)分别设置了不同的超时时间。在接口级和应用级服务发现机制下,注册到注册中心的数据是截然不同的。如下图所示:
- 接口级服务发现机制下注册中心中的数据
"sayHello": [
{"application":"dubbo-application","name":"instance1", "ip":"127.0.0.1", "metadata":{"timeout":1000}},
{"application":"dubbo-application","name":"instance2", "ip":"127.0.0.2", "metadata":{"timeout":2000}},
{"application":"dubbo-application","name":"instance3", "ip":"127.0.0.3", "metadata":{"timeout":3000}},
],
"echo": [
{"application":"dubbo-application","name":"instance1", "ip":"127.0.0.1", "metadata":{"timeout":1000}},
{"application":"dubbo-application","name":"instance2", "ip":"127.0.0.2", "metadata":{"timeout":2000}},
{"application":"dubbo-application","name":"instance3", "ip":"127.0.0.3", "metadata":{"timeout":3000}},
],
"getVersion": [
{"application":"dubbo-application","name":"instance1", "ip":"127.0.0.1", "metadata":{"timeout":1000}},
{"application":"dubbo-application","name":"instance2", "ip":"127.0.0.2", "metadata":{"timeout":2000}},
{"application":"dubbo-application","name":"instance3", "ip":"127.0.0.3", "metadata":{"timeout":3000}}
]
- 应用级服务发现机制下注册中心中的数据
"dubbo-application": [
{"name":"instance1", "ip":"127.0.0.1", "metadata":{"timeout":1000}},
{"name":"instance2", "ip":"127.0.0.2", "metadata":{"timeout":2000}},
{"name":"instance3", "ip":"127.0.0.3", "metadata":{"timeout":3000}}
]
通过对比我们可以发现,采用应用级服务发现机制确实使注册中心中的数据量减少了很多,那些原有的接口级的数据存储在元数据中心中。
服务端暴露全流程
引入应用级服务发现机制以后,Dubbo 3.0 服务端暴露全流程和之前有很大的区别。暴露服务端全流程的核心代码在 DubboBootstrap#doStart 中,具体如下:
private void doStart() {
// 1. 暴露Dubbo服务
exportServices();
// If register consumer instance or has exported services
if (isRegisterConsumerInstance() || hasExportedServices()) {
// 2. 暴露元数据服务
exportMetadataService();
// 3. 定时更新和上报元数据
registerServiceInstance();
....
}
......
}
假设以 Zookeeper 作为注册中,对外暴露 Triple 协议的服务为例,服务端暴露全流程时序图如下:
我们可以看到,整个的暴露流程还是挺复杂的,一共可以分为四个部分:
- 暴露 injvm 协议的服务
- 注册 service-discovery-registry 协议
- 暴露 Triple 协议的服务并注册 registry 协议
- 暴露 MetadataService 服务
下面会分别从这四个部分对服务暴露全流程进行详细讲解。
1、暴露 injvm 协议的服务
injvm 协议的服务是暴露在本地的,主要原因是在一个应用上往往既有 Service(暴露服务)又有 Reference(服务引用)的情况存在,并且 Reference 引用的服务就是在该应用上暴露的 Service。为了支持这种使用场景,Dubbo 提供了 injvm 协议,将 Service 暴露在本地,Reference 就可以不需要走网络直接在本地调用 Service。
整体时序图
由于这部分内容在之前的接口级服务发现机制中是类似的,所以相关的核心代码就不在这里展开讨论了。
2、注册
service-discovery-registry 协议
注册
service-discovery-registry 协议的核心目的是为了注册与服务相关的元数据,默认情况下元数据通过 InMemoryWritableMetadataService 将数据存储在本地内存和本地文件。
整体时序图
核心代码在 ServiceConfig#exportRemote 中,具体如下:
- 注册 service-discovery-registry 协议的入口
private URL exportRemote(URL url, List<URL> registryURLs) {
if (CollectionUtils.isNotEmpty(registryURLs)) {
// 如果是多个注册中心,通过循环对每个注册中心进行注册
for (URL registryURL : registryURLs) {
// 判断是否是service-discovery-registry协议
// 将service-name-mapping参数的值设置为true
if (SERVICE_REGISTRY_PROTOCOL.equals(registryURL.getProtocol())) {
url = url.addParameterIfAbsent(SERVICE_NAME_MAPPING_KEY, "true");
}
......
// 注册service-discovery-registry协议复用服务暴露流程
doExportUrl(registryURL.putAttribute(EXPORT_KEY, url), true);
}
......
return url;
}
- invoker 中包装 Metadata
核心代码在 ServiceConfig#doExportUrl 中,具体如下:
private void doExportUrl(URL url, boolean withMetaData) {
Invoker<?> invoker = PROXY_FACTORY.getInvoker(ref, (Class) interfaceClass, url);
// 此时的withMetaData的值为true
// 将invoker包装成DelegateProviderMetaDataInvoker
if (withMetaData) {
invoker = new DelegateProviderMetaDataInvoker(invoker, this);
}
Exporter<?> exporter = PROTOCOL.export(invoker);
exporters.add(exporter);
}
- 通过 RegistryProtocol 将 Invoker 转化成 Exporter
核心代码在 ProtocolListenerWrapper#export 中,具体如下:
public <T> Export