本文基于 Dubbo 2.6.1 版本,望知悉。
1. 概述
在 《精尽 Dubbo 源码分析 —— 服务引用(一)之本地引用(Injvm)》 一文中,我们已经分享了本地引用服务。在本文中,我们来分享远程引用服务。在 Dubbo 中提供多种协议( Protocol ) 的实现,大体流程一致,本文以 Dubbo Protocol 为例子,这也是 Dubbo 的默认协议。
如果不熟悉该协议的同学,可以先看看 《Dubbo 使用指南 —— dubbo://》 ,简单了解即可。
特性
缺省协议,使用基于 mina
1.1.7
和 hessian3.2.1
的 remoting 交互。
- 连接个数:单连接
- 连接方式:长连接
- 传输协议:TCP
- 传输方式:NIO 异步传输
- 序列化:Hessian 二进制序列化
- 适用范围:传入传出参数数据包较小(建议小于100K),消费者比提供者个数多,单一消费者无法压满提供者,尽量不要用 dubbo 协议传输大文件或超大字符串。
- 适用场景:常规远程服务方法调用
相比本地引用,远程引用会多做如下几件事情:
- 向注册中心订阅,从而发现服务提供者列表。
- 启动通信客户端,通过它进行远程调用。
2. 远程引用
远程暴露服务的顺序图如下:
在 #createProxy(map)
方法中,涉及远程引用服务的代码如下:
/** |
- 第 11 行:省略是否本地引用的代码,在 《精尽 Dubbo 源码分析 —— 服务引用(一)之本地引用(Injvm)》 已经有分享。
- 第 13 至 15 行:省略本地引用的代码,在 《精尽 Dubbo 源码分析 —— 服务引用(一)之本地引用(Injvm)》 已经有分享。
- 第 16 至 90 行:正常流程,一般为远程引用。
- 第 18 至 38 行:
url
配置项,定义直连地址,可以是服务提供者的地址,也可以是注册中心的地址。- 第 20 行:拆分地址成数组,使用 “;” 分隔。
- 第 22 至 23 行:循环数组
us
,创建 URL 对象后,添加到urls
中。 - 第 25 行:创建 URL 对象。
- 第 26 至 29 行:路径属性
url.path
未设置时,缺省使用接口全名interfaceName
。 - 第 30 至 32 行:若
url.protocol = registry
时,注册中心的地址,在参数url.parameters.refer
上,设置上服务引用的配置参数集合map
。 - 第 33 至 36 行:服务提供者的地址。
- 从逻辑上类似【第 53 行】的代码。
- 一般情况下,不建议这样在
url
配置注册中心,而是在registry
配置。如果要配置,格式为registry://host:port?registry=
,例如registry://127.0.0.1?registry=zookeeper
。 - TODO ClusterUtils.mergeUrl
- 第 39 至 59 行:
protocol
配置项,注册中心。- 第 42 行:调用
#loadRegistries(provider)
方法,加载注册中心的 com.alibaba.dubbo.common.URL` 数组。 - 第 43 至 58 行:循环数组
us
,创建 URL 对象后,添加到urls
中。- 第 47 行:调用
#loadMonitor(registryURL)
方法,获得监控中心 URL 。 - 第 49 至 51 行:服务引用配置对象
map
,带上监控中心的 URL 。具体用途,我们在后面分享监控中心会看到。 - 第 53 行:调用
URL#addParameterAndEncoded(key, value)
方法,将服务引用配置对象参数集合map
,作为"refer"
参数添加到注册中心的 URL 中,并且需要编码。通过这样的方式,注册中心的 URL 中,包含了服务引用的配置。
- 第 47 行:调用
- 第 42 行:调用
第 61 至 64 行:单
urls
时,直接调用Protocol#refer(type, url)
方法,引用服务,返回 Invoker 对象。- 此处 Dubbo SPI 自适应的特性的好处就出来了,可以自动根据 URL 参数,获得对应的拓展实现。例如,
invoker
传入后,根据invoker.url
自动获得对应 Protocol 拓展实现为 DubboProtocol 。 实际上,Protocol 有两个 Wrapper 拓展实现类: ProtocolFilterWrapper、ProtocolListenerWrapper 。所以,
#export(...)
方法的调用顺序是:- Protocol$Adaptive => ProtocolFilterWrapper => ProtocolListenerWrapper => RegistryProtocol
- =>
- Protocol$Adaptive => ProtocolFilterWrapper => ProtocolListenerWrapper => DubboProtocol
- 也就是说,这一条大的调用链,包含两条小的调用链。原因是:
- 首先,传入的是注册中心的 URL ,通过 Protocol$Adaptive 获取到的是 RegistryProtocol 对象。
- 其次,RegistryProtocol 会在其
#refer(...)
方法中,使用服务提供者的 URL ( 即注册中心的 URL 的refer
参数值),再次调用 Protocol$Adaptive 获取到的是 DubboProtocol 对象,进行服务暴露。
为什么是这样的顺序?通过这样的顺序,可以实现类似 AOP 的效果,在获取服务提供者列表后,再创建连接服务提供者的客户端。伪代码如下:
RegistryProtocol#refer(...) {
// 1. 获取服务提供者列表 【并且订阅】
// 2. 创建调用连接服务提供者的客户端
DubboProtocol#refer(...);
// ps:实际这个过程中,还有别的代码,详细见下文。
}- x
- 此处 Dubbo SPI 自适应的特性的好处就出来了,可以自动根据 URL 参数,获得对应的拓展实现。例如,
第 65 至 89 行:多
urls
时,循环调用Protocol#refer(type, url)
方法,引用服务,返回 Invoker 对象。此时,会有多个 Invoker 对象,需要进行合并。- 什么时候会出现多个
urls
呢?例如:《Dubbo 用户指南 —— 多注册中心注册》 。 - 第 66 至 76 行:循环
urls
,引用服务。- 第 71 行:调用
Protocol#refer(type, url)
方法,引用服务,返回 Invoker 对象。然后,添加到invokers
中。 - 第 72 会 75 行:使用最后一个注册中心的 URL ,赋值到
registryURL
。
- 第 71 行:调用
- 第 77 至 88 行:详细解析,见 《精尽 Dubbo 源码解析 —— 集群容错(三)之 Directory 实现》 。
- 什么时候会出现多个
- 第 92 行:省略启动时检查的代码,在 《精尽 Dubbo 源码分析 —— 服务引用(一)之本地引用(Injvm)》 已经有分享。
- 第 96 行:省略创建 Service 代理对象的代码,在 《精尽 Dubbo 源码分析 —— 服务引用(一)之本地引用(Injvm)》 已经有分享。
3. Protocol
服务引用与暴露的 Protocol 很多类似点,本文就不重复叙述了。
建议不熟悉的胖友,请点击 《精尽 Dubbo 源码分析 —— 服务暴露(一)之本地暴露(Injvm)》「3. Protocol」 查看。
本文涉及的 Protocol 类图如下:
3.1 ProtocolFilterWrapper
接 《精尽 Dubbo 源码分析 —— 服务引用(一)之本地引用(Injvm)》「 3.1 ProtocolFilterWrapper」 小节。
本文涉及的 #refer(type, url)
方法,代码如下:
1: public <T> Invoker<T> refer(Class<T> type, URL url) throws RpcException { |
- 第 2 至 5 行:当
invoker.url.protocl = registry
,注册中心的 URL ,无需创建 Filter 过滤链。 - 第 8 行:调用
protocol#refer(type, url)
方法,继续引用服务,最终返回 Invoker 。 - 第 8 行:在引用服务完成后,调用
#buildInvokerChain(invoker, key, group)
方法,创建带有 Filter 过滤链的 Invoker 对象。
3.2 RegistryProtocol
3.2.1 refer
本文涉及的 #refer(type, url)
方法,代码如下:
/** |
- 第 3 行:获得真实的注册中心的 URL 。该过程是我们在 《精尽 Dubbo 源码分析 —— 服务暴露(一)之本地暴露(Injvm)》「2.1 loadRegistries」 的那张图的反向流程,即红线部分 :
getRegistryUrl
- 第 5 行:获得注册中心 Registry 对象。
- 第 7至 9 行:【TODO 8018】RegistryService.class
- 第 13 行:获得服务引用配置参数集合
qs
。 - 第 16 至 22 行:分组聚合,参见 《Dubbo 用户指南 —— 分组聚合》 文档。
第 24 行:调用
#doRefer(cluster, registry, type, url)
方法,执行服务引用。不同于【第 20 行】的代码,后者调用#getMergeableCluster()
方法,获得可合并的 Cluster 对象,代码如下:private Cluster getMergeableCluster() {
return ExtensionLoader.getExtensionLoader(Cluster.class).getExtension("mergeable");
}
3.2.2 doRefer
#doRefer(cluster, registry, type, url)
方法,执行服务引用的逻辑。代码如下:
1: /** |
- 第 12 至 15 行,创建 RegistryDirectory 对象,并设置注册中心到它的属性。
- 第 18 行:获得服务引用配置集合
parameters
。注意,url
传入 RegistryDirectory 后,经过处理并重新创建,所以url != directory.url
,所以获得的是服务引用配置集合。如下图所示:parameters
- 第 19 行:创建订阅 URL 对象。
- 第 20 至 25 行:调用
RegistryService#register(url)
方法,向注册中心注册自己(服务消费者)。 - 第 26 终 30 行:调用
Directory#subscribe(url)
方法,向注册中心订阅服务提供者 + 路由规则 + 配置规则。- 在该方法中,会循环获得到的服务体用这列表,调用
Protocol#refer(type, url)
方法,创建每个调用服务的 Invoker 对象。
- 在该方法中,会循环获得到的服务体用这列表,调用
- 第 33 行:创建 Invoker 对象。详细解析,见 《精尽 Dubbo 源码解析 —— 集群容错(一)之抽象 API》 。
- 第 35 行:调用
ProviderConsumerRegTable#registerConsuemr(invoker, url, subscribeUrl, directory)
方法,向本地注册表,注册消费者。 - 第 36 行:返回 Invoker 对象。
3.3 DubboProtocol
3.3.1 refer
本文涉及的 #refer(type, url)
方法,代码如下:
// AbstractProtocol.java 父类 |
invokers
属性,Invoker 集合。- 第 3 行:调用
#optimizeSerialization(url)
方法,初始化序列化优化器。在 《精尽 Dubbo 源码分析 —— 序列化(一)之总体实现》 中,详细解析。 - 第 7 行:调用
#getClients(url)
方法,创建远程通信客户端数组。 - 第 7 行:创建 DubboInvoker 对象。
- 第 9 行:添加到
invokers
。 - 第 10 行:返回 Invoker 对象。
3.3.2 getClients
友情提示,涉及 Client 的内容,胖友先看过 《精尽 Dubbo 源码分析 —— NIO 服务器》 所有的文章。
#getClients(url)
方法,获得连接服务提供者的远程通信客户端数组。代码如下:
1: /** |
- 第 8 至 16 行:是否共享连接。
- 第 18 至 26 行:创建连接服务提供者的 ExchangeClient 对象数组。
- 注意,若开启共享连接,基于 URL 为维度共享。
- 第 21 至 22 行:共享连接,调用
#getSharedClient(url)
方法,获得 ExchangeClient 对象。 - 第 23 至 25 行:不共享连接,调用
#initClient(url)
方法,直接创建 ExchangeClient 对象。
connections
配置项。- 默认 0 。即,对同一个远程服务器,共用同一个连接。
- 大于 0 。即,每个服务引用,独立每一个连接。
- 《Dubbo 用户指南 —— 连接控制》
- 《Dubbo 用户指南 —— dubbo:reference》
3.3.3 getSharedClient
#getClients(url)
方法,获得连接服务提供者的远程通信客户端数组。代码如下:
/** |
referenceClientMap
属性,通信客户端集合。在我们创建好 Client 对象,“连接”服务器后,会添加到这个集合中,用于后续的 Client 的共享。- ReferenceCountExchangeClient ,顾名思义,带有指向数量计数的 Client 封装。
- “连接” ,打引号的原因,因为有 LazyConnectExchangeClient ,还是顾名思义,延迟连接的 Client 封装。
- 🙂 ReferenceCountExchangeClient 和 LazyConnectExchangeClient 的具体实现,在 「5. Client」 详细解析。
ghostClientMap
属性,幽灵客户端集合。TODO 8030 ,这个是什么用途啊。- 【添加】每次 ReferenceCountExchangeClient 彻底关闭( 指向归零 ) ,其内部的
client
会替换成重新创建的 LazyConnectExchangeClient 对象,此时叫这个对象为幽灵客户端,添加到ghostClientMap
中。 - 【移除】当幽灵客户端,对应的 URL 的服务器被重新连接上后,会被移除。
- 注意,在幽灵客户端被移除之前,
referenceClientMap
中,依然保留着对应的 URL 的 ReferenceCountExchangeClient 对象。所以,ghostClientMap
相当于标记referenceClientMap
中,哪些 LazyConnectExchangeClient 对象,是幽灵状态。👻
- 【添加】每次 ReferenceCountExchangeClient 彻底关闭( 指向归零 ) ,其内部的
- 第 2 至 4 行:从集合
referenceClientMap
中,查找 ReferenceCountExchangeClient 对象。 - 第 5 至 14 行:查找到客户端。
- 第 6 至 9 行:若未关闭,调用
ReferenceCountExchangeClient#incrementAndGetCount()
方法,增加指向该客户端的数量,并返回。 - 第 11 至 13 行:若已关闭,适用于幽灵状态的 ReferenceCountExchangeClient 对象,从
referenceClientMap
中移除,准备下面的代码,创建新的 ReferenceCountExchangeClient 对象。
- 第 6 至 9 行:若未关闭,调用
- 第 15 至 26 行:同步(
synchronized
) ,创建新的 ReferenceCountExchangeClient 对象。- 第 18 行:调用
#initClient(url)
方法,创建 ExchangeClient 对象。 - 第 20 行:将 ExchangeClient 对象,封装创建成 ReferenceCountExchangeClient 独享。
- 第 22 行:添加到集合
referenceClientMap
。 - 第 24 行:移除出集合
ghostClientMap
,因为不再是幽灵状态啦。
- 第 18 行:调用
3.3.4 initClient
#initClient(url)
方法,创建 ExchangeClient 对象,”连接”服务器。
1: private ExchangeClient initClient(URL url) { |
- 第 2 至 9 行:校验配置的 Client 的 Dubbo SPI 拓展是否存在。若不存在,抛出 RpcException 异常。
- 第 12 行:设置编解码器为
"Dubbo"
协议,即 DubboCountCodec 。 - 第 16 行:默认开启心跳功能。
- 第 19 至 31 行:连接服务器,创建客户端。
- 第 21 至 24 行:懒加载,创建 LazyConnectExchangeClient 对象。
- 第 25 至 28 行:直接连接,创建 HeaderExchangeClient 对象。
4. Invoker
本文涉及的 Invoker 类图如下:
4.1 DubboInvoker
com.alibaba.dubbo.rpc.protocol.dubbo.DubboInvoker
,实现 AbstractExporter 抽象类,Dubbo Invoker 实现类。代码如下:
1: /** |
- 胖友,请看属性上的代码注释。
- 第 29 行:调用父类构造方法。该方法中,会将
interface
group
version
token
timeout
添加到公用的隐式传参AbstractInvoker.attachment
属性。- 🙂 代码比较简单,胖友请自己阅读。
5. Client
友情提示,涉及 Client 的内容,胖友先看过 《精尽 Dubbo 源码分析 —— NIO 服务器》 所有的文章。
5.1 ReferenceCountExchangeClient
com.alibaba.dubbo.rpc.protocol.dubbo.ReferenceCountExchangeClient
,实现 ExchangeClient 接口,支持指向计数的信息交换客户端实现类。
构造方法
1: /** |
refenceCount
属性,指向计数。- 【初始】构造方法,【第 21 行】,计数加一。
- 【引用】每次引用,计数加一。
ghostClientMap
属性,幽灵客户端集合,和Protocol.ghostClientMap
参数,一致。client
属性,客户端。- 【创建】构造方法,传入
client
属性,指向它。 - 【关闭】关闭方法,创建 LazyConnectExchangeClient 对象,指向该幽灵客户端。
- 【创建】构造方法,传入
装饰器模式
基于装饰器模式,所以,每个实现方法,都是调用 client
的对应的方法。例如:
|
计数
public void incrementAndGetCount() { |
关闭
1: |
- 第 3 行:计数减一。若无指向,进行真正的关闭。
- 第 4 至 9 行:调用
client
的关闭方法,进行关闭。 第 11 行:调用
#replaceWithLazyClient()
方法,替换client
为 LazyConnectExchangeClient 对象。代码如下:1: private LazyConnectExchangeClient replaceWithLazyClient() {
2: // this is a defensive operation to avoid client is closed by accident, the initial state of the client is false
3: URL lazyUrl = url.addParameter(Constants.LAZY_CONNECT_INITIAL_STATE_KEY, Boolean.FALSE)
4: .addParameter(Constants.RECONNECT_KEY, Boolean.FALSE) // 不重连
5: .addParameter(Constants.SEND_RECONNECT_KEY, Boolean.TRUE.toString())
6: .addParameter("warning", Boolean.TRUE.toString())
7: .addParameter(LazyConnectExchangeClient.REQUEST_WITH_WARNING_KEY, true)
8: .addParameter("_client_memo", "referencecounthandler.replacewithlazyclient"); // 备注
9:
10: // 创建 LazyConnectExchangeClient 对象,若不存在。
11: String key = url.getAddress();
12: // in worst case there's only one ghost connection.
13: LazyConnectExchangeClient gclient = ghostClientMap.get(key);
14: if (gclient == null || gclient.isClosed()) {
15: gclient = new LazyConnectExchangeClient(lazyUrl, client.getExchangeHandler());
16: ghostClientMap.put(key, gclient);
17: }
18: return gclient;
19: }- 第 3 至 8 行:基于
url
,创建 LazyConnectExchangeClient 的 URL 链接。设置的一些参数,结合 「5.2 LazyConnectExchangeClient」 一起看。 - 第 10 至 17 行:创建 LazyConnectExchangeClient 对象,若不存在。
- 第 3 至 8 行:基于
5.2 LazyConnectExchangeClient
com.alibaba.dubbo.rpc.protocol.dubbo.LazyConnectExchangeClient
,实现 ExchangeClient 接口,支持懒连接服务器的信息交换客户端实现类。
构造方法
1: static final String REQUEST_WITH_WARNING_KEY = "lazyclient_request_with_warning"; |
initialState
属性,如果没有初始化客户端时的链接状态。有点绕,看#isConnected()
方法,代码如下:
public boolean isConnected() {
if (client == null) { // 客户端未初始化
return initialState;
} else {
return client.isConnected();
}
}- 所以,我们可以看到 ReferenceCountExchangeClient 关闭创建的 LazyConnectExchangeClient 对象的
initialState = false
,未连接。 - 默认值,
DEFAULT_LAZY_CONNECT_INITIAL_STATE = true
。
- 所以,我们可以看到 ReferenceCountExchangeClient 关闭创建的 LazyConnectExchangeClient 对象的
requestWithWarning
属性,请求时,是否检查告警。- 所以,我们可以看到 ReferenceCountExchangeClient 关闭创建的 LazyConnectExchangeClient 对象的
initialState = false
,未连接。 - 默认值,
false
。
- 所以,我们可以看到 ReferenceCountExchangeClient 关闭创建的 LazyConnectExchangeClient 对象的
warningcount
属性,警告计数器。每超过一定次数,打印告警日志。每次发送请求时,会调用#warning(request)
方法,根据情况,打印告警日志。代码如下:private void warning(Object request) {
if (requestWithWarning) { // 开启
if (warningcount.get() % 5000 == 0) { // 5000 次
logger.warn(new IllegalStateException("safe guard client , should not be called ,must have a bug."));
}
warningcount.incrementAndGet(); // 增加计数
}
}- 理论来说,不会被调用。如果被调用,那么就是一个 BUG 咯。
装饰器模式
基于装饰器模式,所以,每个实现方法,都是调用 client
的对应的方法。例如:
|
初始化客户端
private void initClient() throws RemotingException { |
- 发送消息/请求前,都会调用该方法,保证客户端已经初始化。代码如下:
public void send(Object message, boolean sent) throws RemotingException { |
666. 彩蛋
写的有点迷糊,主要是集群和注册中心,看的不是特别细致。
不过咋说呢?
读源码的过程,就像剥洋葱,一层一层拨开它的心。