日期 | 更新说明 |
---|---|
2022年10月1日 | 初版 |
2022年10月7日 | 添加插图和流程图等 |
导读
前言
上篇文章《Dubbo3 源码系列 Dubbo“纠葛”(入门篇)》讲述了Dubbo为了兼容Spring的方式并且找到Dubbo启动的入口。本篇文章将继续深入了解,Dubbo启动时服务导出的流程;其核心内容:第一部分是前置工作,主要用于检查参数,组装 URL。第二部分是导出服务,包含导出服务到本地 (JVM),和导出服务到远程两个过程。第三部分是向注册中心注册服务,用于服务发现。
在阅读开始之前希望读者和我一样带着一个问题去阅读,Dubbo3新增了应用级的服务注册的,那么Dubbo是怎么在原来的接口级别的基础上支持的应用级注册呢?
下面是关于接口、应用级注册的配置,zookeeper的应用级注册节点的信息:
-Ddubbo.application.register-mode=all
# 可选值 interface、instance、all,默认是 all,即接口级地址、应用级地址都注册
环境
使用的是Dubbo3.0.7版本的代码,为了方便阅读维护了一个源码注释版本《dubbo-3.0.7_source》。
入口
spring容器入口
在spring的项目中,Dubbo是通过DubboDeployApplicationListener
的事件监听扩展机制启动的。
具体代码org.apache.dubbo.config.spring.context.DubboDeployApplicationListener#onApplicationEvent
如下:
public void onApplicationEvent(ApplicationContextEvent event) {
if (nullSafeEquals(applicationContext, event.getSource())) {
if (event instanceof ContextRefreshedEvent) {
//Spring 容器启动时触发该事件回调,Dubbo3启动时的入口
onContextRefreshedEvent((ContextRefreshedEvent) event);
} else if (event instanceof ContextClosedEvent) {
// 关闭时回调
onContextClosedEvent((ContextClosedEvent) event);
}
}
}
org.apache.dubbo.config.spring.context.DubboDeployApplicationListener#onContextRefreshedEvent
private void onContextRefreshedEvent(ContextRefreshedEvent event) {
ModuleDeployer deployer = moduleModel.getDeployer();
Assert.notNull(deployer, "Module deployer is null");
// start module Dubbo 启动入口
Future future = deployer.start();
// if the module does not start in background, await finish
if (!deployer.isBackground()) {
try {
future.get();
} catch (InterruptedException e) {
logger.warn("Interrupted while waiting for dubbo module start: " + e.getMessage());
} catch (Exception e) {
logger.warn("An error occurred while waiting for dubbo module start: " + e.getMessage(), e);
}
}
}
org.apache.dubbo.config.deploy.DefaultModuleDeployer#start
public synchronized Future start() throws IllegalStateException {
if (isStopping() || isStopped() || isFailed()) {
throw new IllegalStateException(getIdentifier() + " is stopping or stopped, can not start again");
}
try {
if (isStarting() || isStarted()) {
return startFuture;
}
onModuleStarting();
// initialize 初始化
applicationDeployer.initialize();
initialize();
// export services 服务导出
exportServices();
// prepare application instance
// exclude internal module to avoid wait itself
if (moduleModel != moduleModel.getApplicationModel().getInternalModule()) {
applicationDeployer.prepareInternalModule();
}
// refer services 服务引用
referServices();
// 省略....
org.apache.dubbo.config.ServiceConfig#doExport
// 服务导出起点(入口)
protected synchronized void doExport() {
if (unexported) {
throw new IllegalStateException("The service " + interfaceClass.getName() + " has already unexported!");
}
if (exported) {
return;
}
if (StringUtils.isEmpty(path)) {
path = interfaceName;
}
doExportUrls();
exported();
}
服务导出
org.apache.dubbo.config.ServiceConfig#doExportUrls
这里service-discovery-registry 和 registry 的注册协议
private void doExportUrls() {
ModuleServiceRepository repository = getScopeModel().getServiceRepository();
ServiceDescriptor serviceDescriptor;
final boolean serverService = ref instanceof ServerService;
if(serverService){
serviceDescriptor=((ServerService) ref).getServiceDescriptor();
repository.registerService(serviceDescriptor);
}else{
serviceDescriptor = repository.registerService(getInterfaceClass());
}
providerModel = new ProviderModel(getUniqueServiceName(),
ref,
serviceDescriptor,
this,
getScopeModel(),
serviceMetadata);
repository.registerProvider(providerModel);
// 注意:这里放入 service-discovery-registry 和 registry 的注册协议(如果配置双协议)
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);
// stub service will use generated service name
if(!serverService) {
// In case user specified path, register service one more time to map it to path.
repository.registerService(pathKey, interfaceClass);
}
doExportUrlsFor1Protocol(protocolConfig, registryURLs);
}
}
org.apache.dubbo.config.ServiceConfig#exportUrl
private void exportUrl(URL url, List<URL> registryURLs) {
String scope = url.getParameter(SCOPE_KEY);
// don't export when none is configured
if (!SCOPE_NONE.equalsIgnoreCase(scope)) {
// export to local if the config is not remote (export to remote only when config is remote)
if (!SCOPE_REMOTE.equalsIgnoreCase(scope)) {
// 本地导出
exportLocal(url);
}
// export to remote if the config is not local (export to local only when config is local)
if (!SCOPE_LOCAL.equalsIgnoreCase(scope)) {
// 远程导出
url = exportRemote(url, registryURLs);
if (!isGeneric(generic) && !getScopeModel().isInternal()) {
MetadataUtils.publishServiceDefinition(url, providerModel.getServiceModel(), getApplicationModel());
}
}
}
this.urls.add(url);
}
本地导出
本地的源码相对比较简单,使用的是org.apache.dubbo.rpc.protocol.injvm.InjvmProtocol#expor
。
远程导出
org.apache.dubbo.config.ServiceConfig#exportRemote
URL里面添加了参数,非核心内容org.apache.dubbo.config.ServiceConfig#doExportUrl
private void doExportUrl(URL url, boolean withMetaData) {
Invoker<?> invoker = proxyFactory.getInvoker(ref, (Class) interfaceClass, url);
if (withMetaData) {
invoker = new DelegateProviderMetaDataInvoker(invoker, this);
}
// 将URL暴露为 invoker 导出 (debug技巧:scopeModel.getExtensionLoader(org.apache.dubbo.rpc.Protocol.class).getExtension(invoker.getUrl().getProtocol()))
// service-discovery-registry --> org.apache.dubbo.rpc.protocol.ProtocolSerializationWrapper#export
// registry --> org.apache.dubbo.rpc.protocol.ProtocolSerializationWrapper#export(区别是包装的Protocol略有不同)
Exporter<?> exporter = protocolSPI.export(invoker);
exporters.add(exporter);
}
这里使用Dubbo的SPI机制有一些不是很好读,使用了Dubbo的SPI的机制包装了,最终是调用了org.apache.dubbo.registry.integration.RegistryProtocol#export
service-discovery-registry
协议情况dubbo
协议情况
org.apache.dubbo.registry.integration.RegistryProtocol#export
public <T> Exporter<T> export(final Invoker<T> originInvoker) throws RpcException {
// service-discovery-registry --> 注册URL:service-discovery-registry://127.0.0.1:2181/org.apache.dubbo.registry.RegistryService?REGISTRY_CLUSTER=zk-registry&application=dubbo-springboot-demo-provider&dubbo=2.0.2&pid=4324&qos.enable=false®istry=zookeeper&release=3.0.7×tamp=1665040235001// 注册URL:zookeeper://127.0.0.1:2181/org.apache.dubbo.registry.RegistryService?REGISTRY_CLUSTER=zk-registry&application=dubbo-springboot-demo-provider&dubbo=2.0.2&pid=15504&qos.enable=false&release=3.0.7×tamp=1665069137614
// dubbo --> 注册URL:zookeeper://127.0.0.1:2181/org.apache.dubbo.registry.RegistryService?REGISTRY_CLUSTER=zk-registry&application=dubbo-springboot-demo-provider&dubbo=2.0.2&pid=15580&qos.enable=false&release=3.0.7×tamp=1665125331594
URL registryUrl = getRegistryUrl(originInvoker);
// url to export locally
// dubbo://192.168.237.1:20880/org.apache.dubbo.springboot.demo.DemoService?anyhost=true&application=dubbo-springboot-demo-provider&background=false&bind.ip=192.168.237.1&bind.port=20880&deprecated=false&dubbo=2.0.2&dynamic=true&generic=false&interface=org.apache.dubbo.springboot.demo.DemoService&methods=sayHello,sayHelloAsync&pid=15504&qos.enable=false&release=3.0.7&service-name-mapping=true&side=provider×tamp=1665069137648
URL providerUrl = getProviderUrl(originInvoker);
// Subscribe the override data
// FIXME When the provider subscribes, it will affect the scene : a certain JVM exposes the service and call
// the same service. Because the subscribed is cached key with the name of the service, it causes the
// subscription information to cover.
final URL overrideSubscribeUrl = getSubscribedOverrideUrl(providerUrl);
final OverrideListener overrideSubscribeListener = new OverrideListener(overrideSubscribeUrl, originInvoker);
Map<URL, NotifyListener> overrideListeners = getProviderConfigurationListener(providerUrl).getOverrideListeners();
overrideListeners.put(registryUrl, overrideSubscribeListener);
// 监听器(监听动态配置中⼼此服务的参数数据的变化,⼀旦监听到变化,则重写服务URL,并且在服务导出时先重写⼀次服务URL)
providerUrl = overrideUrlWithConfig(providerUrl, overrideSubscribeListener);
//export invoker 服务导出:(DubboProtocol的export⽅法去导出服务)1.启动NettyServer 2.设置⼀系列的 RequestHandler
final ExporterChangeableWrapper<T> exporter = doLocalExport(originInvoker, providerUrl);
// url to registry 注意:这里会走两遍:
// 1.service-discovery-registry://127.0.0.1:2181/org.apache.dubbo.registry.RegistryService?REGISTRY_CLUSTER=zk-registry&application=dubbo-springboot-demo-provider&dubbo=2.0.2&pid=15064&qos.enable=false®istry=zookeeper&release=3.0.7×tamp=1662996885467
// |-ListenRegistryWrapper --> ServiceDiscoveryRegistry
// 2.zookeeper://127.0.0.1:2181/org.apache.dubbo.registry.RegistryService?REGISTRY_CLUSTER=zk-registry&application=dubbo-springboot-demo-provider&dubbo=2.0.2&pid=16316&qos.enable=false&release=3.0.7×tamp=1662996607526
// |-ZookeeperRegistry
final Registry registry = getRegistry(registryUrl);
final URL registeredProviderUrl = getUrlToRegistry(providerUrl, registryUrl);
// decide if we need to delay publish (provider itself and registry should both need to register)
boolean register = providerUrl.getParameter(REGISTER_KEY, true) && registryUrl.getParameter(REGISTER_KEY, true);
if (register) {
// 注册中心注册(在注册中心添加节点和watcher)
// 这里有两种情况
// 1. 接口级注册会将接口级服务提供者数据直接注册到 Zookeeper上面,服务发现(应用级注册)这里仅仅会将注册数据转换为服务元数据等后面来发布元数据
// 2. 应用级注册通常添加 metadataInfo 添加基本信息
register(registry, registeredProviderUrl);
}
// register stated url on provider model
// 服务注册后更新状态
registerStatedUrl(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);
}
// 导出通知(Listener)
notifyExport(exporter);
//Ensure that a new exporter instance is returned every time export
return new DestroyableExporter<>(exporter);
}
org.apache.dubbo.registry.integration.RegistryProtocol#doLocalExport
\
private <T> ExporterChangeableWrapper<T> doLocalExport(final Invoker<T> originInvoker, URL providerUrl) {
String key = getCacheKey(originInvoker);
return (ExporterChangeableWrapper<T>) bounds.computeIfAbsent(key, s -> {
Invoker<?> invokerDelegate = new InvokerDelegate<>(originInvoker, providerUrl);
// 协议导出 scopeModel.getExtensionLoader(org.apache.dubbo.rpc.Protocol.class).getExtension(invokerDelegate)
return new ExporterChangeableWrapper<>((Exporter<T>) protocol.export(invokerDelegate), originInvoker);
});
}
主要是1.启动NettyServer 2.设置⼀系列的 RequestHandler;这部分内容和Dubbo2的流程是一样的,参考上图。
org.apache.dubbo.registry.integration.RegistryProtocol#getRegistry
通过RegistryFactory
获取注册中心。
- RegistryFactory 注册中心对象获取
- AbstractRegistryFactory 模板类型封装注册中心对象获取的基本逻辑,比如缓存和基本的逻辑判断
- ServiceDiscoveryRegistryFactory 用于创建服务发现注册中心工厂对象 用于创建ServiceDiscoveryRegistry对象
- ZookeeperRegistryFactory 用于创建ZookeeperRegistry类型对象
- NacosRegistryFactory Nacos注册中心工厂对象 用于创建NacosRegistry
org.apache.dubbo.registry.integration.RegistryProtocol#register
最终通过SPI分别是:ServiceDiscoveryRegistry、ZookeeperRegistry
private void register(Registry registry, URL registeredProviderUrl) {
// ServiceDiscoveryRegistry(ListenRegistryWrapper --> FailbackRegistry)
// ZookeeperRegistry(ListenRegistryWrapper --> FailbackRegistry)
registry.register(registeredProviderUrl);
}
- Node 节点信息开放接口 比如节点 url的获取 ,销毁
- RegistryService 注册服务接口,比如注册,订阅,查询等操作
- Registry 注册中心接口,是否服务发现查询,注册,取消注册方法
- AbstractRegistry 注册中心逻辑抽象模板类型,封装了注册,订阅,通知的基本逻辑,和本地缓存注册中心信息的基本逻辑
- FailbackRegistry 封装了失败重试的逻辑
- NacosRegistry 封装了以nacos作为注册中心的基本逻辑
- ServiceDiscoveryRegistry 应用级服务发现注册中心逻辑,现在不需要这种网桥实现,协议可以直接与服务发现交互。ServiceDiscoveryRegistry是一种非常特殊的注册表实现,用于以兼容的方式将旧的接口级服务发现模型与3.0中引入的新服务发现模型连接起来。 它完全符合注册表SPI的扩展规范,但与zookeeper和Nacos的具体实现不同,因为它不与任何真正的第三方注册表交互,而只与过程中ServiceDiscovery的相关组件交互。简而言之,它架起了旧接口模型和新服务发现模型之间的桥梁:register()方法主要通过与MetadataService交互,将接口级数据聚合到MetadataInfo中subscribe() 触发应用程序级服务发现模型的整个订阅过程。-根据ServiceNameMapping将接口映射到应用程序。-启动新的服务发现侦听器(InstanceListener),并使NotifierListener成为InstanceListener的一部分。
- CacheableFailbackRegistry 提供了一些本地内存缓存的逻辑 对注册中心有用,注册中心的sdk将原始字符串作为提供程序实例返回,例如zookeeper和etcd
- ZookeeperRegistry Zookeeper作为注册中心的基本操作逻辑封装
- ServiceDiscoveryRegistry:添加元数据到metadataInfo
public void register(URL url) {
metadataInfo.addService(url);
}
- 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);
}
}
至此exportServices();
基本结束了。
应用级服务zookeeper导出
添加调试堆栈如下图:
org.apache.dubbo.registry.zookeeper.ZookeeperServiceDiscovery#doRegister
public void doRegister(ServiceInstance serviceInstance) {
try {
// 调用 curator-x-discovery 进行服务注册
serviceDiscovery.registerService(build(serviceInstance));
} catch (Exception e) {
throw new RpcException(REGISTRY_EXCEPTION, "Failed register instance " + serviceInstance.toString(), e);
}
}
添加断点;其调用的就是curator-x-discovery
的内部实现(Curator-x- discovery 说明),具体服务发现的注册信息可以通过断点信息如下图:
到这里开头问题基本也有呼应了,答案不言自明了。
dubbo://192.168.237.1:20880/org.apache.dubbo.springboot.demo.DemoService?anyhost=true&application=dubbo-springboot-demo-provider&background=false&bind.ip=192.168.237.1&bind.port=20880&deprecated=false&dubbo=2.0.2&dynamic=true&generic=false&interface=org.apache.dubbo.springboot.demo.DemoService&methods=sayHello,sayHelloAsync&pid=20316&qos.enable=false&release=3.0.7&service-name-mapping=true&side=provider×tamp=1665028264270