源码下载和编译
源码下载、编译和导入步骤如下:
-
dubbo的项目在github中的地址为: https://github.com/apache/dubbo
-
进入tag目录选择自己要下载的版本,本次我选用的是2.7.8-release
-
解压后进行本地编译,进入dubbo项目, 进行编译操作
mvn clean install -DskipTests
-
等待编译完成后,使用IDE引入项目。
架构整体设计
Dubbo调用关系说明
在这里主要由四部分组成:
- Provider: 暴露服务的服务提供方
- Protocol 负责提供者和消费者之间协议交互数据
- Service 真实的业务服务信息 可以理解成接口 和 实现
- Container Dubbo的运行环境
- Consumer: 调用远程服务的服务消费方
- Protocol 负责提供者和消费者之间协议交互数据
- Cluster 感知提供者端的列表信息
- Proxy 可以理解成 提供者的服务调用代理类 由它接管 Consumer中的接口调用逻辑
- Registry: 注册中心,用于作为服务发现和路由配置等工作,提供者和消费者都会在这里进行注册
- Monitor: 用于提供者和消费者中的数据统计,比如调用频次,成功失败次数等信息。
启动和执行流程说明:
- 提供者端启动 容器负责把Service信息加载 并通过Protocol 注册到注册中心
- 消费者端启动 通过监听提供者列表来感知提供者信息 并在提供者发生改变时 通过注册中心及时通知消费端
- 消费方发起 请求 通过Proxy模块
- 利用Cluster模块 来选择真实的要发送给的提供者信息
- 交由Consumer中的Protocol 把信息发送给提供者
- 提供者同样需要通过 Protocol 模块来处理消费者的信息
- 最后由真正的服务提供者 Service 来进行处理
整体的调用链路
说明:
淡绿色 代表了服务生产者的范围
淡蓝色 代表了服务消费者的范围
红色箭头 代表了调用的方向
整体链路调用的流程:
- 消费者通过Interface进行方法调用 统一交由消费者端的 Proxy 通过ProxyFactory 来进行代理对象的创建 使用到了 jdk动态代理或者 javassist技术
- 交给Filter 这个模块 做一个统一的过滤请求 在SPI案例中涉及过
- 接下来会进入最主要的Invoker调用逻辑
通过Directory 去配置中新读取信息 最终通过list方法获取所有的Invoker
通过Cluster模块 根据选择的具体路由规则 来选取Invoker列表
通过LoadBalance模块 根据负载均衡策略 选择一个具体的Invoker 来处理我们的请求
如果执行中出现错误 并且Consumer阶段配置了重试机制 则会重新尝试执行 - 继续经过Filter 进行执行功能的前后封装 Invoker 选择具体的执行协议
- 客户端 进行编码和序列化 然后发送数据
- 到达Consumer中的 Server 在这里进行解码 和 反序列化的接收数据
- 使用Exporter选择执行器
- 交给Filter 进行一个提供者端的过滤 到达 Invoker 执行器
- 通过Invoker 调用接口的具体实现 然后返回
Dubbo源码整体设计
官网地址
图例说明:
- 图中左边淡蓝背景的为服务消费方使用的接口,右边淡绿色背景的为服务提供方使用的接口,位于中轴线上的为双方都用到的接口。
- 图中从下至上分为十层,各层均为单向依赖,右边的黑色箭头代表层之间的依赖关系,每一层都可以剥离上层被复用,其中,Service 和 Config 层为 API,其它各层均为 SPI。
- 图中绿色小块的为扩展接口,蓝色小块为实现类,图中只显示用于关联各层的实现类。
- 图中蓝色虚线为初始化过程,即启动时组装链,红色实线为方法调用过程,即运行时调时链,紫色三角箭头为继承,可以把子类看作父类的同一个节点,线上的文字为调用的方法。
Dubbo源码整体设计与调用链路十分相似。只不过这里可以看到接口的一些具体实现以及左侧也有更为详细的层次划分,后面的源码解析时也会着重介绍其中比较重要的模块实现。
分层介绍:
- Business 业务逻辑层
- service 业务层 包括我们的业务代码 比如 接口 实现类 直接面向开发者
- RPC层 远程过程调用层
- config 配置层 对外提供配置 以
ServiceConfig
ReferenceConfig
为核心 可以直接初始化配置类 也可以解析配置文件生成 - proxy 服务接口透明代理,生成服务的客户端 Stub 和服务器端 Skeleton, 以
ServiceProxy
为中心,扩展接口为ProxyFactory
,服务代理层 无论是生产者还是消费者框架都会产生一个代理类 整个过程对上层透明就是业务层对远程调用无感 - registry 注册中心层 封装服务地址的注册与发现 以服务的URL为中心,扩展接口为
RegistryFactory
,Registry
,RegistryService
- cluster 路由层 (集群容错层) 提供了多个提供者的路由和负载均衡 并且它桥接注册中心 以
Invoker
为核心 - monitor 监控层 RPC调用相关的信息 如 调用次数 成功失败的情况 调用时间等 在这一层完成
- protocol 远程调用层 封装RPC调用 无论是服务的暴露,还是服务的引用 都是在Protocol中作为主功能入口 负责Invoker的整个生命周期 Dubbo中所有的模型都向Invoker靠拢,以
Invocation
,Result
为中心,扩展接口为Protocol
,Invoker
,Exporter
- config 配置层 对外提供配置 以
- Remoting层 远程数据传输层
- exchange 信息交换层 封装请求和响应的模式 如把请求由同步 转换成异步,以
Request
,Response
为中心,扩展接口为Exchanger
,ExchangeChannel
,ExchangeClient
,ExchangeServer
- transport 网络传输层 统一网络传输的接口 比如 netty 和 mina 统一为一个网络传输接口
- serialize 数据序列化层 负责管理整个框架中的数据传输的序列化 和反序列化
- exchange 信息交换层 封装请求和响应的模式 如把请求由同步 转换成异步,以
服务注册与消费源码剖析
3.1 注册中心Zookeeper剖析
注册中心是Dubbo的重要组成部分,主要用于服务的注册与发现,我们可以选择Redis、Nacos、Zookeeper作为Dubbo的注册中心,Dubbo推荐用户使用Zookeeper作为注册中心。
注册中心Zookeeper目录结构
我们使用一个最基本的服务的注册与消费的Demo来进行说明。
例如:只有一个提供者和消费者。com.elvis.service.HelloService
为我们所提供的服务。
public interface HelloService {
String helloService(String name);
}
则Zookeeper的目录结构如下:
+- dubbo
| +- com.lagou.service.HelloService
| | +- consumers
| | | +- consumer://192.168.31.52/com.elvis.service.HelloService?application=service-consumer&category=consumers&check=false&dubbo=2.0.2&init=false&interface=com.elvis.service.HelloService&metadata-type=remote&methods=helloService&pid=1260&qos.accept.foreign.ip=true&qos.enable=true&qos.port=33333&release=2.7.8&retries=2&side=consumer&sticky=false&timeout=4000×tamp=1623853262182
| | +- providers
| | | +- dubbo://169.254.194.245:20880/com.elvis.service.HelloService?anyhost=true&application=service-provider&bind.ip=169.254.194.245&bind.port=20880&deprecated=false&dubbo=2.0.2&dynamic=true&generic=false&interface=com.elvis.service.HelloService&metadata-type=remote&methods=helloService&pid=15236&release=2.7.8&side=provider×tamp=1623769434942
| | +- configuration
| | +- routers
通过debug可以看到最终添加到zookeeper中的节点数据,provider将url作为数据存入到zookeeper,将下面的信息url转换后存入zookeeper
dubbo://192.168.31.52:20880/com.elvis.service.HelloService?anyhost=true&application=service-provider&deprecated=false&dubbo=2.0.2&dynamic=true&generic=false&interface=com.elvis.service.HelloService&metadata-type=remote&methods=helloService&pid=16084&release=2.7.8&side=provider×tamp=1623852515015
转换后就是要存入的节点
/dubbo/com.elvis.service.HelloService/providers/dubbo%3A%2F%2F192.168.31.52%3A20880%2Fcom.elvis.service.HelloService%3Fanyhost%3Dtrue%26application%3Dservice-provider%26deprecated%3Dfalse%26dubbo%3D2.0.2%26dynamic%3Dtrue%26generic%3Dfalse%26interface%3Dcom.elvis.service.HelloService%26metadata-type%3Dremote%26methods%3DhelloService%26pid%3D16084%26release%3D2.7.8%26side%3Dprovider%26timestamp%3D1623852515015
可以通过ZK客户端查看节点
- 可以在这里看到所有的都是在dubbo层级下的
- dubbo跟节点下面是当前所拥有的接口名称,如果有多个接口,则会以多个子节点的形式展开
- 每个服务下面又分别有四个配置项
- consumers: 当前服务下面所有的消费者列表(URL)
- providers: 当前服务下面所有的提供者列表(URL)
- configuration: 当前服务下面的配置信息信息,provider或者consumer会通过读取这里的配置信息来获取配置
- routers: 当消费者在进行获取提供者的时,会通过这里配置好的路由来进行适配匹配规则。
- 可以看到,dubbo基本上很多时候都是通过URL的形式来进行交互获取数据的,在URL中也会保存很多的信息。后面也会对URL的规则做详细介绍。
通过这张图我们可以了解到如下信息: - 提供者会在providers 目录下进行自身的进行注册。
- 消费者会在consumers 目录下进行自身注册,并且监听provider 目录,以此通过监听提供者增加或者减少,实现服务发现。
- Monitor模块会对整个服务级别做监听,用来得知整体的服务情况。以此就能更多的对整体情况做监控。
服务的注册过程分析
官网实现细节
服务注册(暴露)过程
首先 ServiceConfig
类拿到对外提供服务的实际类 ref(如:HelloServiceImpl),然后通过ProxyFactory
接口实现类中的 getInvoker
方法使用 ref 生成一个 AbstractProxyInvoker
实例,到这一步就完成具体服务到 Invoker
的转化。接下来就是 Invoker
转换到 Exporter
的过程。
查看ServiceConfig 类
重点查看 ProxyFactory 和 Protocol 类型的属性 以及 ref;
ProxyFactory 和 Protocol实际都是通过ExtensionLoader类获取到的,ref则是来自父类是一个泛型实例
下面看一下Invoker 转换成 Exporter的过程
其中会涉及到 RegistryService接口 RegistryFactory
接口 和 注册provider到注册中心流程的过程
- RegistryService代码解读,这块儿的代码比较简单,主要是对指定的路径进行注册,解绑,监听和取消监听,查询操作。也是注册中心中最为基础的类。
public interface RegistryService {
/**
* 进行对URL的注册操作,比如provider,consumer,routers等
*/
void register(URL url);
/**
* 解除对指定URL的注册,比如provider,consumer,routers等
*/
void unregister(URL url);
/**
* 增加对指定URL的路径监听,当有变化的时候进行通知操作
*/
void subscribe(URL url, NotifyListener listener);
/**
* 解除对指定URL的路径监听,取消指定的listener
*/
void unsubscribe(URL url, NotifyListener listener);
/**
* 查询指定URL下面的URL列表,比如查询指定服务下面的consumer列表
*/
List<URL> lookup(URL url);
}
- 我们再来看RegistryFactory ,是通过他来生成真实的注册中心。通过这种方式,也可以保证一个应用中可以使用多个注册中心。可以看到这里也是通过不同的protocol参数,来选择不同的协议。
@SPI("dubbo")
public interface RegistryFactory {
/**
* 获取注册中心地址
*/
@Adaptive({"protocol"})
Registry getRegistry(URL url);
}
- 下面来跟踪一下,一个服务是如何注册到注册中心上去的。其中比较关键的一个类是RegistryProtocol ,他负责管理整个注册中心相关协议。并且统一对外提供服务。这里我们主要以RegistryProtocol.export 方法作为入口,这个方法主要的作用就是将我们需要执行的信息注册并且导出。
@Override
public <T> Exporter<T> export(final Invoker<T> originInvoker) throws RpcException {
// 获取注册中心的地址
// zookeeper://192.168.137.144:2181/org.apache.dubbo.registry.RegistryService?application=service-//provider&dubbo=2.0.2&export=dubbo%3A%2F%2F192.168.31.52%3A20880%2Fcom.elvis.service.HelloService%3Fanyhost%3Dtrue%26application%3Dservice-provider%26bind.ip%3D192.168.31.52%26bind.port%3D20880%26deprecated%3Dfalse%26dubbo%3D2.0.2%26dynamic%3Dtrue%26generic%3Dfalse%26interface%3Dcom.elvis.service.HelloService%26metadata-type%3Dremote%26methods%3DhelloService%26pid%3D3016%26release%3D2.7.8%26side%3Dprovider%26timestamp%3D1623854314822&pid=3016&release=2.7.8&timeout=20000×tamp=1623854314818
URL registryUrl = getRegistryUrl(originInvoker);
// url to export locally
// 获取当前提供者需要注册的地址
// dubbo://192.168.31.52:20880/com.elvis.service.HelloService?anyhost=true&application=service-provider&bind.ip=192.168.31.52&bind.port=20880&deprecated=false&dubbo=2.0.2&dynamic=true&generic=false&interface=com.elvis.service.HelloService&metadata-type=remote&methods=helloService&pid=3016&release=2.7.8&side=provider×tamp=1623854314822
URL providerUrl = getProviderUrl(originInvoker);
// 获取进行注册override协议的访问地址
// provider://192.168.31.52:20880/com.elvis.service.HelloService?anyhost=true&application=service-provider&bind.ip=192.168.31.52&bind.port=20880&category=configurators&check=false&deprecated=false&dubbo=2.0.2&dynamic=true&generic=false&interface=com.elvis.service.HelloService&metadata-type=remote&methods=helloService&pid=3016&release=2.7.8&side=provider×tamp=1623854314822
final URL overrideSubscribeUrl = getSubscribedOverrideUrl(providerUrl);
// 增加override的监听器
final OverrideListener overrideSubscribeListener = new OverrideListener(overrideSubscribeUrl, originInvoker);
overrideListeners.put(overrideSubscribeUrl, overrideSubscribeListener);
// 根据现有的override协议,对注册地址进行改写操作
providerUrl = overrideUrlWithConfig(providerUrl, overrideSubscribeListener);
//export invoker
// 对当前的服务进行本地导出
// 完成后即可在看到本地的20880端口号已经启动,并且暴露服务
final ExporterChangeableWrapper<T> exporter = doLocalExport(originInvoker, providerUrl);
// url to registry
// 获取真实的注册中心, 比如我们常用的ZookeeperRegistry
final Registry registry = getRegistry(originInvoker);
// 获取当前服务需要注册到注册中心的providerURL,主要用于去除一些没有必要的参数(比如在本地导出时所使用的qos参数等值)
// dubbo://192.168.31.52:20880/com.elvis.service.HelloService?anyhost=true&application=service-provider&deprecated=false&dubbo=2.0.2&dynamic=true&generic=false&interface=com.elvis.service.HelloService&metadata-type=remote&methods=helloService&pid=3016&release=2.7.8&side=provider×tamp=1623854314822
final URL registeredProviderUrl = getUrlToRegistry(providerUrl, registryUrl);
// decide if we need to delay publish
// 获取当前url是否需要进行注册参数
boolean register = providerUrl.getParameter(REGISTER_KEY, true);
if (register) {
// 将当前的提供者注册到注册中心上去
register(registryUrl, registeredProviderUrl);
}
// register stated url on provider model
// 在提供者模型上注册声明的url,主要存储与服务提供者相关的信息
registerStatedUrl(registryUrl, registeredProviderUrl, register);
// 设置当前导出中的相关信息
exporter.setRegisterUrl(registeredProviderUrl);
exporter.setSubscribeUrl(overrideSubscribeUrl);
// Deprecated! Subscribe to override rules in 2.6.x or before.
// 对override协议进行注册,用于在接收到override请求时做适配,这种方式用于适配2.6.x及之前的版本(混用)
registry.subscribe(overrideSubscribeUrl, overrideSubscribeListener);
// 通知RegistryProtocol监听者,有服务上线
notifyExport(exporter);
//Ensure that a new exporter instance is returned every time export
// 返回导出对象(对数据进行封装)
return new DestroyableExporter<>(exporter);
}
- 再来看看
register
和registerStatedUrl
方法, 这里面做的比较简单,主要是从RegistoryFactory 中获取注册中心,并且进行地址注册。
private void register(URL registryUrl, URL registeredProviderUrl) {
// 获取注册中心
Registry registry = registryFactory.getRegistry(registryUrl);
// 对当前的服务进行注册
registry.register(registeredProviderUrl);
}
private void registerStatedUrl(URL registryUrl, URL registeredProviderUrl, boolean registered) {
// ProviderModel 表示服务提供者模型,此对象中存储了与服务提供者相关的信息。
// 比如服务的配置信息,服务实例等。每个被导出的服务对应一个 ProviderModel。
ProviderModel model = ApplicationModel.getProviderModel(registeredProviderUrl.getServiceKey());
model.addStatedUrl(new ProviderModel.RegisterStatedURL(
registeredProviderUrl,
registryUrl,
registered));
}
- 看一下Registry中的类目录结构
+- RegistryService
| +- Registry
| | +- AbstractRegistry
| | | +- FailbackRegistry
| | | | +- ZookeeperRegistry
| | | | +- NacosRegistry
| | | | +- ...
目录结构描述如下:
- 在这里每个层级代表继承自父级
- 这里面RegistryService 就是我们之前所讲对外提供注册机制的接口。
- 其下面Registry 也同样是一个接口,是对RegistryService 的集成,并且继承了Node 接口,说明注册中心也是基于URL去做的。
- AbstractRegistry 是对注册中心的封装,其主要会对本地注册地址的封装,主要功能在于远程注册中心不可用的时候,可以采用本地的注册中心来使用。
- FailbackRegistry 从名字中可以看出来,失败自动恢复,后台记录失败请求,定时重发功能。
- 最深的一层则更多是真实的第三方渠道实现。
- 看一下在FailbackRegistry 中的实现, 可以在这里看到他的主要作用是调用第三方的实现方式,并且在出现错误时增加重试机制。
@Override
public void register(URL url) {
if (!acceptable(url)) {
logger.info("URL " + url + " will not be registered to Registry. Registry " + url + " does not accept service of this protocol type.");
return;
}
// 上层调用
// 主要用于保存已经注册的地址列表
super.register(url);
// 将一些错误的信息移除(确保当前地址可以在出现一些错误的地址时可以被删除)
removeFailedRegistered(url);
removeFailedUnregistered(url);
try {
// Sending a registration request to the server side
// 发送给第三方渠道进行注册操作,也就是实际的注册
doRegister(url);
} catch (Exception e) {
Throwable t = e;
// 记录日志
// If the startup detection is opened, the Exception is thrown directly.
boolean check = getUrl().getParameter(Constants.CHECK_KEY, true)
&& url.getParameter(Constants.CHECK_KEY, true)
&& !CONSUMER_PROTOCOL.equals(url.getProtocol());
boolean skipFailback = t instanceof SkipFailbackWrapperException;
if (check || skipFailback) {
if (skipFailback) {
t = t.getCause();
}
throw new IllegalStateException("Failed to register " + url + " to registry " + getUrl().getAddress() + ", cause: " + t.getMessage(), t);
} else {
logger.error("Failed to register " + url + ", waiting for retry, cause: " + t.getMessage(), t);
}
// Record a failed registration request to a failed list, retry regularly
// 后台异步进行重试,也是Failback比较关键的代码
addFailedRegistered(url);
}
}
- 再来看看Zookeeper中doRegister 方法的实现, 可以看到这里的实现也比较简单,关键在于toUrlPath 方法的实现。关于dynamic 的值,我们也在上面有看到,他的URL也是true的。
@Override
public void doRegister(URL url) {
try {
// 进行创建地址
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);
}
}
- 解读
toUrlPath
方法。可以看到这里的实现也是比较简单,也验证了我们之前的路径规则。
private String toServicePath(URL url) {
// 接口地址
String name = url.getServiceInterface();
if (ANY_VALUE.equals(name)) {
return toRootPath();
}
// 根节点 + 接口地址
return toRootDir() + URL.encode(name);
}
private String toCategoryPath(URL url) {
// 服务名称 + category(在当前的例子中是providers)
return toServicePath(url) + PATH_SEPARATOR + url.getParameter(CATEGORY_KEY, DEFAULT_CATEGORY);
}
private String toUrlPath(URL url) {
// 分类地址 + url字符串
return toCategoryPath(url) + PATH_SEPARATOR + URL.encode(url.toFullString());
}
URL规则详解 和 服务本地缓存
URL规则详解
URL地址如下:
protocol://host:port/path?key=value&key=value
dubbo://192.168.31.52:20880/com.elvis.service.HelloService?anyhost=true&application=service-provider&deprecated=false&dubbo=2.0.2&dynamic=true&generic=false&interface=com.elvis.service.HelloService&metadata-type=remote&methods=helloService&pid=3016&release=2.7.8&side=provider×tamp=1623854314822
URL主要有以下几部分组成:
- protocol: 协议,一般像我们的provider 或者consumer 在这里都是人为具体的协议
- host: 当前provider 或者其他协议所具体针对的地址,比较特殊的像override 协议所指定的host就是0.0.0.0 代表所有的机器都生效
- port: 和上面相同,代表所处理的端口号
- path: 服务路径,在provider 或者consumer 等其他中代表着我们真实的业务接口
- key=value: 这些则代表具体的参数,这里我们可以理解为对这个地址的配置。比如我们provider中需要具体机器的服务应用名,就可以是一个配置的方式设置上去。
注意:Dubbo中的URL与java中的URL是有一些区别的,如下:
- 这里提供了针对于参数的parameter 的增加和减少(支持动态更改)
- 提供缓存功能,对一些基础的数据做缓存.
服务本地缓存
在上面我们有讲到dubbo有对路径进行本地缓存操作。这里我们就对本地缓存进行讲解。
dubbo调用者需要通过注册中心(例如:ZK)注册信息,获取提供者,但是如果频繁往从ZK获取信息,肯定会存在单点故障问题,所以dubbo提供了将提供者信息缓存在本地的方法。
Dubbo在订阅注册中心的回调处理逻辑当中会保存服务提供者信息到本地缓存文件当中(同步/异步两种方式),以URL纬度进行全量保存。
Dubbo在服务引用过程中会创建registry对象并加载本地缓存文件,会优先订阅注册中心,订阅注册中心失败后会访问本地缓存文件内容获取服务提供信息。
- 首先从构造方法讲起, 这里方法比较简单,主要用于确定需要保存的文件信息。并且从系统中读取已有的配置信息。
public AbstractRegistry(URL url) {
setUrl(url);
if (url.getParameter(REGISTRY__LOCAL_FILE_CACHE_ENABLED, true)) {
// Start file save timer
syncSaveFile = url.getParameter(REGISTRY_FILESAVE_SYNC_KEY, false);
默认保存路径(home/.dubbo/dubbo-registry-appName-address-port.cache)
String defaultFilename = System.getProperty("user.home") + "/.dubbo/dubbo-registry-" + url.getParameter(APPLICATION_KEY) + "-" + url.getAddress().replaceAll(":", "-") + ".cache";
String filename = url.getParameter(FILE_KEY, defaultFilename);
// 创建文件
File file = null;
if (ConfigUtils.isNotEmpty(filename)) {
file = new File(filename);
if (!file.exists() && file.getParentFile() != null && !file.getParentFile().exists()) {
if (!file.getParentFile().mkdirs()) {
throw new IllegalArgumentException("Invalid registry cache file " + file + ", cause: Failed to create directory " + file.getParentFile() + "!");
}
}
}
this.file = file;
// When starting the subscription center,
// we need to read the local cache file for future Registry fault tolerance processing.
// 加载已有的配置文件
loadProperties();
notify(url.getBackupUrls());
}
}
- 我们可以看到这个类中最为关键的一个属性为properties ,我们可以通过寻找,得知这个属性的设置值只有在一个地方: saveProperties ,我们来看一下这个方法。这里也有一个我们值得关注的点,就是基于版本号的的更改。
private void saveProperties(URL url) {
if (file == null) {
return;
}
try {
StringBuilder buf = new StringBuilder();
// 获取所有通知到的地址
Map<String, List<URL>> categoryNotified = notified.get(url);
if (categoryNotified != null) {
for (List<URL> us : categoryNotified.values()) {
for (URL u : us) {
if (buf.length() > 0) {
// 多个地址进行拼接
buf.append(URL_SEPARATOR);
}
buf.append(u.toFullString());
}
}
}
// 保存数据
properties.setProperty(url.getServiceKey(), buf.toString());
// 保存为一个新的版本号
// 通过这种机制可以保证后面保存的记录,在重试的时候,不会重试之前的版本
long version = lastCacheChanged.incrementAndGet();
if (syncSaveFile) {
// 需要同步保存则进行保存
doSaveProperties(version);
} else {
// 否则则异步去进行处理
registryCacheExecutor.execute(new SaveProperties(version));
}
} catch (Throwable t) {
logger.warn(t.getMessage(), t);
}
}
- 再来看看是如何进行保存文件的。这里的实现也比较简单,主要比较关键的代码在于利用文件级锁来保证同一时间只会有一个线程执行。
public void doSaveProperties(long version) {
if (version < lastCacheChanged.get()) {
return;
}
if (file == null) {
return;
}
// Save
try {
// 使用文件级别所,来保证同一段时间只会有一个线程进行读取操作
File lockfile = new File(file.getAbsolutePath() + ".lock");
if (!lockfile.exists()) {
lockfile.createNewFile();
}
try (RandomAccessFile raf = new RandomAccessFile(lockfile, "rw");
FileChannel channel = raf.getChannel()) {
// 利用文件锁来保证并发的执行的情况下,只会有一个线程执行成功(原因在于可能是跨VM的)
FileLock lock = channel.tryLock();
if (lock == null) {
throw new IOException("Can not lock the registry cache file " + file.getAbsolutePath() + ", ignore and retry later, maybe multi java process use the file, please config: dubbo.registry.file=xxx.properties");
}
// Save
try {
if (!file.exists()) {
file.createNewFile();
}
// 将配置的文件信息保存到文件中
try (FileOutputStream outputFile = new FileOutputStream(file)) {
properties.store(outputFile, "Dubbo Registry Cache");
}
} finally {
// 解开文件锁
lock.release();
}
}
} catch (Throwable e) {
// 执行出现错误时,则交给专门的线程去进行重试
savePropertiesRetryTimes.incrementAndGet();
if (savePropertiesRetryTimes.get() >= MAX_RETRY_TIMES_SAVE_PROPERTIES) {
logger.warn("Failed to save registry cache file after retrying " + MAX_RETRY_TIMES_SAVE_PROPERTIES + " times, cause: " + e.getMessage(), e);
savePropertiesRetryTimes.set(0);
return;
}
if (version < lastCacheChanged.get()) {
savePropertiesRetryTimes.set(0);
return;
} else {
registryCacheExecutor.execute(new SaveProperties(lastCacheChanged.incrementAndGet()));
}
logger.warn("Failed to save registry cache file, will retry, cause: " + e.getMessage(), e);
}
}
Dubbo 消费过程分析
服务消费流程
首先 ReferenceConfig
类的 init
方法调用 createProxy() ,期间 使用Protocol
调用 refer
方法生成 Invoker
实例(如上图中的红色部分),这是服务消费的关键。接下来使用ProxyFactory把 Invoker
转换为客户端需要的接口(如:HelloService)。
- 首先看ReferenceConfig类中的几个关键成员,也都是通过ExtensionLoader获取到
- 再看createProxy方法,这里就是为消费端产生代理对象
private T createProxy(Map<String, String> map) {
// 判断是否要通过本地引用
if (shouldJvmRefer(map)) {
URL url = new URL(LOCAL_PROTOCOL, LOCALHOST_VALUE, 0, interfaceClass.getName()).addParameters(map);
invoker = REF_PROTOCOL.refer(interfaceClass, url);
if (logger.isInfoEnabled()) {
logger.info("Using injvm service " + interfaceClass.getName());
}
} else {
urls.clear();
if (url != null && url.length() > 0) { // user specified URL, could be peer-to-peer address, or register center's address.
String[] us = SEMICOLON_SPLIT_PATTERN.split(url);
if (us != null && us.length > 0) {
for (String u : us) {
URL url = URL.valueOf(u);
if (StringUtils.isEmpty(url.getPath())) {
url = url.setPath(interfaceName);
}
if (UrlUtils.isRegistry(url)) {
urls.add(url.addParameterAndEncoded(REFER_KEY, StringUtils.toQueryString(map)));
} else {
urls.add(ClusterUtils.mergeUrl(url, map));
}
}
}
} else { // assemble URL from register center's configuration
// if protocols not injvm checkRegistry
if (!LOCAL_PROTOCOL.equalsIgnoreCase(getProtocol())) {
checkRegistry();
// 获取全部需要注册的消费者url
List<URL> us = ConfigValidationUtils.loadRegistries(this, false);
if (CollectionUtils.isNotEmpty(us)) {
for (URL u : us) {
URL monitorUrl = ConfigValidationUtils.loadMonitor(this, u);
if (monitorUrl != null) {
map.put(MONITOR_KEY, URL.encode(monitorUrl.toFullString()));
}
//添加需要的参数并编码后放入url的注册队列中
urls.add(u.addParameterAndEncoded(REFER_KEY, StringUtils.toQueryString(map)));
}
}
if (urls.isEmpty()) {
throw new IllegalStateException("No such any registry to reference " + interfaceName + " on the consumer " + NetUtils.getLocalHost() + " use dubbo version " + Version.getVersion() + ", please config <dubbo:registry address=\"...\" /> to your spring config.");
}
}
}
if (urls.size() == 1) {
//通过protocol的refer获取到invoker对象
invoker = REF_PROTOCOL.refer(interfaceClass, urls.get(0));
} else {
List<Invoker<?>> invokers = new ArrayList<Invoker<?>>();
URL registryURL = null;
for (URL url : urls) {
invokers.add(REF_PROTOCOL.refer(interfaceClass, url));
if (UrlUtils.isRegistry(url)) {
registryURL = url; // use last registry url
}
}
if (registryURL != null) { // registry url is available
// for multi-subscription scenario, use 'zone-aware' policy by default
String cluster = registryURL.getParameter(CLUSTER_KEY, ZoneAwareCluster.NAME);
// The invoker wrap sequence would be: ZoneAwareClusterInvoker(StaticDirectory) -> FailoverClusterInvoker(RegistryDirectory, routing happens here) -> Invoker
invoker = Cluster.getCluster(cluster, false).join(new StaticDirectory(registryURL, invokers));
} else { // not a registry url, must be direct invoke.
String cluster = CollectionUtils.isNotEmpty(invokers)
? (invokers.get(0).getUrl() != null ? invokers.get(0).getUrl().getParameter(CLUSTER_KEY, ZoneAwareCluster.NAME) : Cluster.DEFAULT)
: Cluster.DEFAULT;
invoker = Cluster.getCluster(cluster).join(new StaticDirectory(invokers));
}
}
}
if (logger.isInfoEnabled()) {
logger.info("Refer dubbo service " + interfaceClass.getName() + " from url " + invoker.getUrl());
}
/**
* @since 2.7.0
* ServiceData Store
*/
String metadata = map.get(METADATA_KEY);
WritableMetadataService metadataService = WritableMetadataService.getExtension(metadata == null ? DEFAULT_METADATA_STORAGE_TYPE : metadata);
if (metadataService != null) {
URL consumerURL = new URL(CONSUMER_PROTOCOL, map.remove(REGISTER_IP_KEY), 0, map.get(INTERFACE_KEY), map);
metadataService.publishServiceDefinition(consumerURL);
}
// create service proxy
// 最终将invoker创建代理对象返回,
return (T) PROXY_FACTORY.getProxy(invoker, ProtocolUtils.isGeneric(generic));
}
- 进入
RegistryProtocol
中的refer
方法
@Override
@SuppressWarnings("unchecked")
public <T> Invoker<T> refer(Class<T> type, URL url) throws RpcException {
// 获取到注册中心url
//zookeeper://192.168.137.144:2181/org.apache.dubbo.registry.RegistryService?application=service-consumer&dubbo=2.0.2&pid=4636&qos.accept.foreign.ip=true&qos.enable=true&qos.port=33333&refer=application%3Dservice-consumer%26check%3Dfalse%26dubbo%3D2.0.2%26init%3Dfalse%26interface%3Dcom.elvis.service.HelloService%26metadata-type%3Dremote%26methods%3DhelloService%26pid%3D4636%26qos.accept.foreign.ip%3Dtrue%26qos.enable%3Dtrue%26qos.port%3D33333%26register.ip%3D192.168.31.52%26release%3D2.7.8%26retries%3D2%26side%3Dconsumer%26sticky%3Dfalse%26timeout%3D4000%26timestamp%3D1623857094198&release=2.7.8&timeout=20000×tamp=1623857114535
url = getRegistryUrl(url);
///org.apache.dubbo.registry.zookeeper.ZookeeperRegistry
Registry registry = registryFactory.getRegistry(url);
if (RegistryService.class.equals(type)) {
return proxyFactory.getInvoker((T) registry, type, url);
}
// group="a,b" or group="*", 这里实际获取到的就是url中的参数
Map<String, String> qs = StringUtils.parseQueryString(url.getParameterAndDecoded(REFER_KEY));
String group = qs.get(GROUP_KEY);
if (group != null && group.length() > 0) {
if ((COMMA_SPLIT_PATTERN.split(group)).length > 1 || "*".equals(group)) {
return doRefer(Cluster.getCluster(MergeableCluster.NAME), registry, type, url);
}
}
// 获取到路由对象
Cluster cluster = Cluster.getCluster(qs.get(CLUSTER_KEY));
return doRefer(cluster, registry, type, url);
}
- 进入doRefer
private <T> Invoker<T> doRefer(Cluster cluster, Registry registry, Class<T> type, URL url) {
RegistryDirectory<T> directory = new RegistryDirectory<T>(type, url);
directory.setRegistry(registry);
directory.setProtocol(protocol);
// all attributes of REFER_KEY
Map<String, String> parameters = new HashMap<String, String>(directory.getConsumerUrl().getParameters());
// 生成消费端的url
//consumer://192.168.31.52/com.elvis.service.HelloService?application=service-consumer&check=false&dubbo=2.0.2&init=false&interface=com.elvis.service.HelloService&metadata-type=remote&methods=helloService&pid=4636&qos.accept.foreign.ip=true&qos.enable=true&qos.port=33333&release=2.7.8&retries=2&side=consumer&sticky=false&timeout=4000×tamp=1623857094198
URL subscribeUrl = new URL(CONSUMER_PROTOCOL, parameters.remove(REGISTER_IP_KEY), 0, type.getName(), parameters);
if (directory.isShouldRegister()) {
directory.setRegisteredConsumerUrl(subscribeUrl);
// 向注册中心添加消费者节点
registry.register(directory.getRegisteredConsumerUrl());
}
directory.buildRouterChain(subscribeUrl);
// 订阅服务引用,实际就是订阅生产者,最终进入org.apache.dubbo.registry.zookeeper.ZookeeperRegistry#doSubscribe方法
directory.subscribe(toSubscribeUrl(subscribeUrl));
Invoker<T> invoker = cluster.join(directory);
List<RegistryProtocolListener> listeners = findRegistryProtocolListeners(url);
if (CollectionUtils.isEmpty(listeners)) {
return invoker;
}
RegistryInvokerWrapper<T> registryInvokerWrapper = new RegistryInvokerWrapper<>(directory, cluster, invoker);
for (RegistryProtocolListener listener : listeners) {
listener.onRefer(this, registryInvokerWrapper);
}
return registryInvokerWrapper;
}
- 进入doSubscribe,这里会对生产者节点进行订阅
@Override
public void doSubscribe(final URL url, final NotifyListener listener) {
try {
if (ANY_VALUE.equals(url.getServiceInterface())) {
String root = toRootPath();
ConcurrentMap<NotifyListener, ChildListener> listeners = zkListeners.computeIfAbsent(url, k -> new ConcurrentHashMap<>());
ChildListener zkListener = listeners.computeIfAbsent(listener, k -> (parentPath, currentChilds) -> {
for (String child : currentChilds) {
child = URL.decode(child);
if (!anyServices.contains(child)) {
anyServices.add(child);
subscribe(url.setPath(child).addParameters(INTERFACE_KEY, child,
Constants.CHECK_KEY, String.valueOf(false)), k);
}
}
});
zkClient.create(root, false);
List<String> services = zkClient.addChildListener(root, zkListener);
if (CollectionUtils.isNotEmpty(services)) {
for (String service : services) {
service = URL.decode(service);
anyServices.add(service);
subscribe(url.setPath(service).addParameters(INTERFACE_KEY, service,
Constants.CHECK_KEY, String.valueOf(false)), listener);
}
}
} else {
List<URL> urls = new ArrayList<>();
// 通过消费端的url反向推出服务提供者的节点路径进行订阅
for (String path : toCategoriesPath(url)) {
ConcurrentMap<NotifyListener, ChildListener> listeners = zkListeners.computeIfAbsent(url, k -> new ConcurrentHashMap<>());
ChildListener zkListener = listeners.computeIfAbsent(listener, k -> (parentPath, currentChilds) -> ZookeeperRegistry.this.notify(url, k, toUrlsWithEmpty(url, parentPath, currentChilds)));
// 如果消费者节点还没有创建则进行创建。
zkClient.create(path, false);
// 添加子节点监听器,并返回当前已经存在的服务提供者
List<String> children = zkClient.addChildListener(path, zkListener);
if (children != null) {
// 添加服务提供者的信息
urls.addAll(toUrlsWithEmpty(url, path, children));
}
}
// 通知服务提供者变化的信息
notify(url, listener, urls);
}
} catch (Throwable e) {
throw new RpcException("Failed to subscribe " + url + " to zookeeper " + getUrl() + ", cause: " + e.getMessage(), e);
}
}
6. 上面的notify最终会进入到org.apache.dubbo.registry.support.AbstractRegistry#notify(org.apache.dubbo.common.URL, org.apache.dubbo.registry.NotifyListener, java.util.List<org.apache.dubbo.common.URL>)
,这里会将获取的服务提供者信息缓存到文件中
/**
* Notify changes from the Provider side.
*
* @param url consumer side url
* @param listener listener
* @param urls provider latest urls
*/
protected void notify(URL url, NotifyListener listener, List<URL> urls) {
if (url == null) {
throw new IllegalArgumentException("notify url == null");
}
if (listener == null) {
throw new IllegalArgumentException("notify listener == null");
}
if ((CollectionUtils.isEmpty(urls))
&& !ANY_VALUE.equals(url.getServiceInterface())) {
logger.warn("Ignore empty notify urls for subscribe url " + url);
return;
}
if (logger.isInfoEnabled()) {
logger.info("Notify urls for subscribe url " + url + ", urls: " + urls);
}
// keep every provider's category.
Map<String, List<URL>> result = new HashMap<>();
for (URL u : urls) {
if (UrlUtils.isMatch(url, u)) {
String category = u.getParameter(CATEGORY_KEY, DEFAULT_CATEGORY);
List<URL> categoryList = result.computeIfAbsent(category, k -> new ArrayList<>());
categoryList.add(u);
}
}
if (result.size() == 0) {
return;
}
Map<String, List<URL>> categoryNotified = notified.computeIfAbsent(url, u -> new ConcurrentHashMap<>());
for (Map.Entry<String, List<URL>> entry : result.entrySet()) {
String category = entry.getKey();
List<URL> categoryList = entry.getValue();
categoryNotified.put(category, categoryList);
listener.notify(categoryList);
// We will update our cache file after each notification.
// When our Registry has a subscribe failure due to network jitter, we can return at least the existing cache URL.
// 这里讲获取到的provider的信息存储到文件中
saveProperties(url);
}
}