Dubbo源码分析 - 远程暴露

概述

本地暴露 中已经对服务暴露的整个流程进行了介绍,并深入分析了本地服务暴露,本篇文章将接着本地服务暴露,继续分析远程服务暴露。远程服务暴露相比本地服务暴露,区别点如下:

  • 暴露并启动服务,本地暴露是无需启动服务的
  • 服务注册,本地暴露无需注册到注册中心
  • 配置订阅,订阅配置信息,当配置发生变化尝试重新暴露

远程暴露

由于服务暴露的主干流程在本地暴露中已经详细说明,这里对远程暴露的细节进行说明。下面我们继续从服务 URL 入手分析。经过服务暴露准备操作之后,得到的服务 URL 如下所示:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
dubbo://10.1.1.202:20880/com.alibaba.dubbo.demo.DemoService?
anyhost=true
&application=demo-provider
&bean.name=com.alibaba.dubbo.demo.DemoService
&bind.ip=10.1.17.202
&bind.port=20880
&dubbo=2.0.2
&generic=false
&group=abc
&interface=com.alibaba.dubbo.demo.DemoService
&methods=sayHello
&owner=gentryhuang
&pid=1665&qos.port=22222
&server=netty4&
side=provider
&timestamp=1638155815325

协议暴露服务

完成服务 URL 的组装后,服务暴露的准备工作也就完成了,接下来就可以执行服务暴露工作了。宏观层面上服务暴露逻辑如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
+--- ServiceConfig
/**
* 使用不同的协议,逐个向注册中心分组暴露服务。该方法中包含了本地和远程两种暴露方式
*
* @param protocolConfig 协议配置对象
* @param registryURLs 处理过的注册中心分组集合【已经添加了ApplicationConfig和RegistryConfig的参数】
*/
private void doExportUrlsFor1Protocol(ProtocolConfig protocolConfig, List<URL> registryURLs) {

// ${省略其它代码}

// 如果存在当前协议对应的 ConfiguratorFactory 扩展实现,就创建配置规则器 Configurator,将配置规则应用到url todo 这里应该不会存在把?
if (ExtensionLoader.getExtensionLoader(ConfiguratorFactory.class).hasExtension(url.getProtocol())) {
// 加载ConfiguratorFactory ,并生成Configurator,将配置规则应用到url中
url = ExtensionLoader.getExtensionLoader(ConfiguratorFactory.class).getExtension(url.getProtocol()).getConfigurator(url).configure(url);
}

// 从URL中获取暴露方式
String scope = url.getParameter(Constants.SCOPE_KEY);

// 如果 scope = none,则不进行暴露,直接结束
if (!Constants.SCOPE_NONE.equalsIgnoreCase(scope)) {

// scope != remote,本地暴露
if (!Constants.SCOPE_REMOTE.equalsIgnoreCase(scope)) {
exportLocal(url);
}

// scope != local,远程暴露,包含了服务暴露和服务注册两个过程
if (!Constants.SCOPE_LOCAL.equalsIgnoreCase(scope)) {
if (logger.isInfoEnabled()) {
logger.info("Export dubbo service " + interfaceClass.getName() + " to url " + url);
}

// 至少一个注册中心
if (registryURLs != null && !registryURLs.isEmpty()) {
// 遍历注册中心URL数组,向每个注册中心发布服务
for (URL registryURL : registryURLs) {
// dynamic属性:服务是否动态注册,如果设为false,注册后将显示disable状态,需要人工启用,并且服务提供者停止时,也不会自动下线,需要人工禁用
url = url.addParameterIfAbsent(Constants.DYNAMIC_KEY, registryURL.getParameter(Constants.DYNAMIC_KEY));

// 获取监控中心URL
URL monitorUrl = loadMonitor(registryURL);

if (monitorUrl != null) {
// 监控URL不能空,就将监控中心的URL作为monitor参数添加到服务提供者的URL中,并且需要编码。通过这样方式,服务提供者的URL中就包含了监控中心的配置
url = url.addParameterAndEncoded(Constants.MONITOR_KEY, monitorUrl.toFullString());
}

if (logger.isInfoEnabled()) {
logger.info("Register dubbo service " + interfaceClass.getName() + " url " + url + " to registry " + registryURL);
}


// 获取配置的动态代理的生成方式 <dubbo:service proxy=""/>,可选jdk/javassist,默认使用javassist
String proxy = url.getParameter(Constants.PROXY_KEY);
if (StringUtils.isNotEmpty(proxy)) {
registryURL = registryURL.addParameter(Constants.PROXY_KEY, proxy);
}

//为服务实现类的对象创建相应的Invoker,getInvoker()方法的第三个参数中,会将服务URL作为export参数添加到RegistryURL中
Invoker<?> invoker = proxyFactory.getInvoker(
ref,
(Class) interfaceClass,
registryURL.addParameterAndEncoded(Constants.EXPORT_KEY, url.toFullString())
);

// 创建 DelegateProviderMetaDataInvoker 装饰对象,在Invoker对象基础上,增加了当前服务提供者ServiceConfig对象,即把Invoker和ServiceConfig结合在了一起
DelegateProviderMetaDataInvoker wrapperInvoker = new DelegateProviderMetaDataInvoker(invoker, this);

// 暴露服务,生成Exporter:
Exporter<?> exporter = protocol.export(wrapperInvoker);

// 添加到 Exporter 集合
exporters.add(exporter);
}

// 不存在或无效注册中心,仅暴露服务,不会将服务信息发布到注册中心。Consumer没法在注册中心找到该服务的信息,但是可以直连。
// 服务暴露过程都是类似的
} else {
// 使用ProxyFactory 创建 Invoker 对象
Invoker<?> invoker = proxyFactory.getInvoker(ref, (Class) interfaceClass, url);
// 创建 DelegateProviderMetaDataInvoker 对象
DelegateProviderMetaDataInvoker wrapperInvoker = new DelegateProviderMetaDataInvoker(invoker, this);

// 使用Protocol 暴露Invoker 对象
Exporter<?> exporter = protocol.export(wrapperInvoker);
// 添加到 Exporter 集合
exporters.add(exporter);
}
}
}
this.urls.add(url);
}

在服务暴露的流程中可知,会尝试向每个注册中心发布服务。如果不存在或无效注册中心,那么仅暴露服务,不会将服务信息发布到注册中心。Consumer没法在注册中心找到该服务的信息,但是可以直连。其中,无效的原因可能是服务只订阅不注册,这样的话就不会向注册中心发布

在服务暴露的流程中,会先将服务对象 ref 通过 ProxyFactory 包装成 AbstractProxyInvoker 对象,完成包装后才会根据 Dubbo SPI 使用对应的 Protocol 实现暴露服务。由于可能有注册中心,也可能没有注册中心,因此暴露流程分为两个分支:

  • 有注册中心:会遍历全部 RegistryURL,并根据 RegistryURL 选择对应的 Protocol 扩展实现进行发布。因为 RegistryURL 是 registry:// 协议,所以这里使用的是 RegistryProtocol 实现。这个协议是注册中心、服务提供者以及服务消费者连接的强梁。
  • 没有注册中心:直接根据服务提供者 URL 选择对应的 Protocol 扩展实现进行发布。由于服务提供者可能使用不同协议暴露,因此这里可能是 dubbo://http:// 等协议,针对不同的协议使用对应的 Protocol 实现暴露服务即可。

不基于注册中心发布服务

不基于注册中心发布服务,就是直接使用服务提供者 URL 对应的协议实现发布服务。

针对 dubbo:// 协议暴露实现,参考 Dubbo协议-服务暴露

针对 http:// 协议暴露实现,参考 Http协议-服务暴露

基于注册中心发布服务

基于注册中心发布服务需要使用到注册中心,Dubbo 通过封装 RegistryProtocol 来完成。这个过程包含以下四个核心步骤:

  • 服务暴露
  • 服务 Server 启动
  • 服务注册
  • 服务配置订阅

RegistryProtocol

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
public class RegistryProtocol implements Protocol {

private final static Logger logger = LoggerFactory.getLogger(RegistryProtocol.class);
/**
* 单例,在dubbo SPI中,被初始化,有且仅有一次。
*/
private static RegistryProtocol INSTANCE;

/**
* 订阅URL与监听器的映射关系
* key: 服务提供方订阅 URL
* value: 监听器
*/
private final Map<URL, NotifyListener> overrideListeners = new ConcurrentHashMap<URL, NotifyListener>();

/**
* 服务暴露映射关系
* key: 服务提供者的 URL 字符串形式
* value: 服务暴露器 Export
*/
private final Map<String, ExporterChangeableWrapper<?>> bounds = new ConcurrentHashMap<String, ExporterChangeableWrapper<?>>();

/**
* Cluster 自适应拓展实现类对象
*/
private Cluster cluster;

/**
* Protocol 自适应拓展实现类,通过Dubbo SPI自动注入 【Dubbo IOC ,Setter注入 】
*/
private Protocol protocol;
/**
* RegistryFactory 自适应拓展实现类,通过Dubbo SPI自动注入
*/
private RegistryFactory registryFactory;

/**
* 代理工厂
*/
private ProxyFactory proxyFactory;

public RegistryProtocol() {
INSTANCE = this;
}

public static RegistryProtocol getRegistryProtocol() {
if (INSTANCE == null) {
ExtensionLoader.getExtensionLoader(Protocol.class).getExtension(Constants.REGISTRY_PROTOCOL); // load
}
return INSTANCE;
}
}

暴露并启动服务

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
+--- RegistryProtocol
/**
* 暴露服务,并启动 Server
*
* @param originInvoker 封装服务的 AbstractProxyInvoker 对象
* @param <T>
* @return
*/
@SuppressWarnings("unchecked")
private <T> ExporterChangeableWrapper<T> doLocalExport(final Invoker<T> originInvoker) {

// 1 获得在 bounds 缓存中的key
// 其实是服务提供者暴露地址, 即从Invoker的URL中Map属性集合中获取key为'export'的服务提供者暴露地址然后去除不需要的信息,该地址要写到注册中心上。
String key = getCacheKey(originInvoker);

// 2 从 bounds 缓存中获得,是否存在已经暴露过的服务
ExporterChangeableWrapper<T> exporter = (ExporterChangeableWrapper<T>) bounds.get(key);

// 3 不存在则会进行服务暴露,并启动 Server
if (exporter == null) {
synchronized (bounds) {
exporter = (ExporterChangeableWrapper<T>) bounds.get(key);

// 未暴露过,进行暴露服务
if (exporter == null) {
/**
* 1 创建InvokerDelegete 对象
* 2 InvokerDelegete 继承了 InvokerWrapper类,增加了getInvoker方法,获取非InvokerDelegete的Invoker对象,
* 通过getInvoker方法可以看出来,可能会存在InvokerDelete.invoker也是InvokerDelegete类型的情况
* 3 todo InvokerDelegete 中的 URL 是服务提供者的URL,在使用 protocol.export 时,使用具体协议暴露服务
*/
final Invoker<?> invokerDelegete = new InvokerDelegete<T>(originInvoker, getProviderUrl(originInvoker));
/**
* 🌟使用服务提供者的协议将 InvokerDelegete 转换成 Exporter
* 1 使用Protocol协议暴露服务并创建ExporterChangeableWrapper 对象 (构造参数: Exporter,Invoker,这样Invoker和Exporter就形成了绑定关系)
* 2 具体调用哪个协议的export方法,看Dubbo SPI选择哪个,就调用对应协议的XXXProtocol#export(Invoker)
* - todo 注意这里使用 SPI 找具体的 Protocol 实现时的过程,该方法是一个标注 @Adaptive 方法,会根据 InvokerDelegete 获取 url ,而该 url 是 originInvoker 中的 Provider URL ,如对应的协议为 Dubbo
* 3 底层启动服务
*/
exporter = new ExporterChangeableWrapper<T>((Exporter<T>) protocol.export(invokerDelegete), originInvoker);

// 添加到 bounds 缓存
bounds.put(key, exporter);
}
}
}

// 4 返回 Export
return exporter;
}

以上方法做了以下两件事:

  • 通过具体的 Protocol 协议实现,如 DubboProtocol 暴露服务。其中,在通过具体 Protocol 协议暴露服务的过程中会启动 Server。
  • 缓存暴露的 Export,key 是服务提供方的 URL 剪枝后的 URL 串。

具体协议暴露服务和启动 Server 的细节可参考:

服务注册与订阅

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
+--- RegistryProtocol
@Override
public <T> Exporter<T> export(final Invoker<T> originInvoker) throws RpcException {


// 1 暴露服务并启动 Server 服务
// 此处Local指的是,本地启动服务(不同协议会启动不动的服务Server,如:NettyServer,tomcat等),打开端口,但是不包括向注册中心注册服务。
final ExporterChangeableWrapper<T> exporter = doLocalExport(originInvoker);

// 2 将 "registry://"协议转换成注册中心协议,以zk为例:zookeeper://127.0.0.1/com.xxx...XxxService?kev=value&kev=value...
URL registryUrl = getRegistryUrl(originInvoker);

// 3 根据 registryUrl 获得(创建)注册中心对象,如ZookeeperRegistry
final Registry registry = getRegistry(originInvoker);

// 4 获得真正要注册到注册中心的URL,其中会删除一些多余的参数信息
final URL registeredProviderUrl = getRegisteredProviderUrl(originInvoker);

// 服务提供者URL参数项 register , 服务提供者是否注册到配置中心。默认是true
boolean register = registeredProviderUrl.getParameter("register", true);

// 5 向本地注册表中记录服务提供者信息(包含服务对应的注册中心地址),该信息用于Dubbo QOS
ProviderConsumerRegTable.registerProvider(originInvoker, registryUrl, registeredProviderUrl);

/**
* 6 根据 register 的值决定是否注册服务
* 注意:
* 服务注册对于Dubbo 来说不是必需的,通过服务直连的方式就可以绕过注册中心。但是一般不这样做,直连方式不利于服务治理,仅推荐在测试环境测试服务时使用。
*/
if (register) {

// 将服务提供者地址写入到注册中心【如:使用zk会先创建服务提供者的节点路径】
register(registryUrl, registeredProviderUrl);

// 标记向本地注册表已经注册了服务提供者
ProviderConsumerRegTable.getProviderWrapper(originInvoker).setReg(true);
}

/** 7 使用OverrideListener 对象,服务暴露时会订阅配置规则 configurators [为了在服务配置发生变化时,重新导出服务。具体的使用场景应该当我们通过 Dubbo 管理后台修改了服务配置后,Dubbo 得到服务配置被修改的通知,然后重新导出服务] */

// 7.1 基于 registeredProviderUrl 构建服务提供方订阅URL,如 provider://...?...&category=configurators&check=false
final URL overrideSubscribeUrl = getSubscribedOverrideUrl(registeredProviderUrl);

// 7.2 创建 OverrideListener 监听器,用于监听订阅的URL映射的目录有没有发生变化
// 即 获取要监听的配置目录,这里会在ProviderURL的基础上添加category=configurators参数,并封装成对 OverrideListener 记录到 overrideListeners 集合中
final OverrideListener overrideSubscribeListener = new OverrideListener(overrideSubscribeUrl, originInvoker);

// 7.3 将订阅放入缓存
overrideListeners.put(overrideSubscribeUrl, overrideSubscribeListener);

// 7.4 向注册中心进行订阅,主要是监听该服务的configurators节点
registry.subscribe(overrideSubscribeUrl, overrideSubscribeListener);

// 8 创建并返回DestroyableExporter
return new DestroyableExporter<T>(exporter, originInvoker, overrideSubscribeUrl, registeredProviderUrl);
}

小结

本篇文章介绍了 Dubbo 远程暴露的实现。远程暴露需要实现 暴露服务启动 Server服务注册服务配置订阅。前两个是由具体 Protocol 实现的,如 DubboProtocol 会将传入的 Invoker 包装成 DubboExporter 对象,并以服务键作为缓存 key 缓存起来,接着启动服务,具体来说是 Netty 服务器。后两个是有 RegistryProtocol 逻辑实现,需要根据是否有有效的注册中心,决定是否注册服务和订阅服务配置,否则只暴露和启动Server,这样情况下只能直连。