注册中心是Dubbo
的重要组成部分,主要用于服务的注册与发现,我们可以选择Redis
、Nacos
、 Zookeeper
作为Dubbo
的注册中心,Dubbo
推荐用户使用Zookeeper
作为注册中心。
1、注册中心Zookeeper目录结构
我们使用一个最基本的服务的注册与消费的Demo来进行说明。
例如:只有一个提供者和消费者。 com.riemann.service.HelloService
为我们所提供的服务。
public interface HelloService {
String sayHello(String name);
}
则Zookeeper
的目录结构如下:
+- dubbo
| +- com.riemann.service.HelloService
| | +- consumers
| | | +- consumer://192.168.1.102/com.riemann.service.HelloService?application=dubbo-demo-annotation- consumer&category=consumers&check=false&dubbo=2.0.2&init=false&interface=com.riemann.service.HelloService&methods=sayHello,sayHelloWithPrint,sayHelloWithTransmiss ion,sayHelloWithException&pid=25923&release=2.7.5&side=consumer&sticky=false×tamp=1583896043650
| | +- providers
| | | +- dubbo://192.168.1.102:20880/com.riemann.service.HelloService?anyhost=true&application=dubbo-demo-annotation-provider&deprecated=false&dubbo=2.0.2&dynamic=true&generic=false&interface=com.riemann.service.HelloService&methods=sayHello,sayHelloWithPrint,sayHelloWithTransmi ssion,sayHelloWithException&pid=25917&release=2.7.5&side=provider&telnet=clear,exit,help,status,log,ls,ps,cd,pwd,invoke,trace,count,select,shutdown×tamp=1583896023597
| | +- configuration
| | +- routers
dubbo
根节点下面是当前所拥有的接口名称,如果有多个接口,则会以多个子节点的形式展开consumers
: 当前服务下面所有的消费者列表(URL)providers
: 当前服务下面所有的提供者列表(URL)configuration
: 当前服务下面的配置信息信息,provider或者consumer会通过读取这里的配 置信息来获取配置routers
: 当消费者在进行获取提供者的时,会通过这里配置好的路由来进行适配匹配规则。dubbo
基本上很多时候都是通过URL的形式来进行交互获取数据的,在URL
中也会保存很多的信息。后面也会对URL
的规则做详细介绍。通过这张图我们可以了解到如下信息:
consumers
目录下进行自身注册,并且监听 provider
目录,以此通过监听提供者增 加或者减少,实现服务发现。Monitor
模块会对整个服务级别做监听,用来得知整体的服务情况。以此就能更多的对整体情况做监控。1、服务注册(暴露)过程
首先 ServiceConfig
类拿到对外提供服务的实际类 ref
(如:HelloServiceImpl
),然后通过 ProxyFactory
接口实现类中的 getInvoker
方法使用 ref
生成一个 AbstractProxyInvoker
实例,
到这一步就完成具体服务到 Invoker
的转化。接下来就是 Invoker
转换到 Exporter
的过程。
查看ServiceConfig
类
重点查看 ProxyFactory
和 Protocol
类型的属性 以及 ref
下面我们就看一下Invoker
转换成 Exporter
的过程
其中会涉及到 RegistryService
接口 RegistryFactory
接口 和 注册provider
到注册中心流程的过程
(1)、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);
}
(2)、我们再来看 RegistryFactory
,是通过他来生成真实的注册中心。通过这种方式,也可以保证一 个应用中可以使用多个注册中心。可以看到这里也是通过不同的protocol
参数,来选择不同的协议。
@SPI("dubbo")
public interface RegistryFactory {
/**
* 获取注册中心地址
*/
@Adaptive({"protocol"})
Registry getRegistry(URL url);
}
(3)、下面我们就来跟踪一下,一个服务是如何注册到注册中心上去的。其中比较关键的一个类是 RegistryProtocol
,他负责管理整个注册中心相关协议。并且统一对外提供服务。这里我们主要以 RegistryProtocol.export
方法作为入口,这个方法主要的作用就是将我们需要执行的信息注册并且
导出。
public <T> Exporter<T> export(Invoker<T> originInvoker) throws RpcException {
// 获取注册中心的地址
// zookeeper://127.0.0.1:2181/org.apache.dubbo.registry.RegistryService? application=dubbo-demo-annotation- provider&dubbo=2.0.2&export=dubbo%3A%2F%2F192.168.1.102%3A20880%2Fcom.lagou.serv ice.HelloService%3Fanyhost%3Dtrue%26application%3Ddubbo-demo-annotation- provider%26bind.ip%3D192.168.1.102%26bind.port%3D20880%26deprecated%3Dfalse%26du bbo%3D2.0.2%26dynamic%3Dtrue%26generic%3Dfalse%26interface%3Dcom.lagou.service.H elloService%26methods%3DsayHello%2CsayHelloWithPrint%2CsayHelloWithTransmission% 2CsayHelloWithException%26pid%3D30998%26release%3D2.7.5%26side%3Dprovider%26teln et%3Dclear%2Cexit%2Chelp%2Cstatus%2Clog%2Cls%2Cps%2Ccd%2Cpwd%2Cinvoke%2Ctrace%2C count%2Cselect%2Cshutdown%26timestamp%3D1583906801486&pid=30998&release=2.7.5&ti mestamp=1583906801477
URL registryUrl = this.getRegistryUrl(originInvoker);
// 获取当前提供者需要注册的地址
// dubbo://192.168.1.102:20880/com.riemann.service.HelloService? anyhost=true&application=dubbo-demo-annotation- provider&bind.ip=192.168.1.102&bind.port=20880&deprecated=false&dubbo=2.0.2&dyna mic=true&generic=false&interface=com.riemann.service.HelloService&methods=sayHello ,sayHelloWithPrint,sayHelloWithTransmission,sayHelloWithException&pid=30998&rele ase=2.7.5&side=provider&telnet=clear,exit,help,status,log,ls,ps,cd,pwd,invoke,tr ace,count,select,shutdown×tamp=1583906801486
URL providerUrl = this.getProviderUrl(originInvoker);
// 获取进行注册override协议的访问地址
// provider://192.168.1.102:20880/com.riemann.service.HelloService? anyhost=true&application=dubbo-demo-annotation- provider&bind.ip=192.168.1.102&bind.port=20880&category=configurators&check=fals e&deprecated=false&dubbo=2.0.2&dynamic=true&generic=false&interface=com.riemann.se rvice.HelloService&methods=sayHello,sayHelloWithPrint,sayHelloWithTransmission,s ayHelloWithException&pid=30998&release=2.7.5&side=provider&telnet=clear,exit,hel p,status,log,ls,ps,cd,pwd,invoke,trace,count,select,shutdown×tamp=158390680 1486
URL overrideSubscribeUrl = this.getSubscribedOverrideUrl(providerUrl);
// 增加override的监听器
RegistryProtocol.OverrideListener overrideSubscribeListener = new RegistryProtocol.OverrideListener(overrideSubscribeUrl, originInvoker);
this.overrideListeners.put(overrideSubscribeUrl, overrideSubscribeListener);
// 根据现有的override协议,对注册地址进行改写操作
providerUrl = this.overrideUrlWithConfig(providerUrl, overrideSubscribeListener);
// 对当前的服务进行本地导出
// 完成后即可在看到本地的20880端口号已经启动,并且暴露服务
RegistryProtocol.ExporterChangeableWrapper<T> exporter = this.doLocalExport(originInvoker, providerUrl);
// 获取真实的注册中心, 比如我们常用的ZookeeperRegistry
Registry registry = this.getRegistry(originInvoker);
// 获取当前服务需要注册到注册中心的providerURL,主要用于去除一些没有必要的参数(比如在本地导出时所使用的qos参数等值)
// dubbo://192.168.1.102:20880/com.riemann.service.HelloService?anyhost=true&application=dubbo-demo-annotation- provider&deprecated=false&dubbo=2.0.2&dynamic=true&generic=false&interface=com.riemann.service.HelloService&methods=sayHello,sayHelloWithPrint,sayHelloWithTransmi ssion,sayHelloWithException&pid=30998&release=2.7.5&side=provider&telnet=clear,e xit,help,status,log,ls,ps,cd,pwd,invoke,trace,count,select,shutdown×tamp=15 83906801486
URL registeredProviderUrl = this.getUrlToRegistry(providerUrl, registryUrl);
// 获取当前url是否需要进行注册参数
boolean register = providerUrl.getParameter("register", true);
if (register) {
// 将当前的提供者注册到注册中心上去
this.register(registryUrl, registeredProviderUrl);
}
// 对override协议进行注册,用于在接收到override请求时做适配,这种方式用于适配2.6.x及之 前的版本(混用)
registry.subscribe(overrideSubscribeUrl, overrideSubscribeListener);
// 设置当前导出中的相关信息
exporter.setRegisterUrl(registeredProviderUrl);
exporter.setSubscribeUrl(overrideSubscribeUrl);
// 返回导出对象(对数据进行封装)
return new RegistryProtocol.DestroyableExporter(exporter);
}
(4)、下面我们再来看看 register
方法, 这里面做的比较简单,主要是从 RegistoryFactory
中获取注册中心,并且进行地址注册。
public void register(URL registryUrl, URL registeredProviderUrl) {
// 获取注册中心
Registry registry = this.registryFactory.getRegistry(registryUrl);
// 对当前的服务进行注册
registry.register(registeredProviderUrl);
// ProviderModel 表示服务提供者模型,此对象中存储了与服务提供者相关的信息。
// 比如服务的配置信息,服务实例等。每个被导出的服务对应一个 ProviderModel。
ProviderModel model = ApplicationModel.getProviderModel(registeredProviderUrl.getServiceKey());
model.addStatedUrl(new RegisterStatedURL(registeredProviderUrl, registryUrl, true));
}
(5)、这里我们再跟里面的register
方法之前,先来介绍一下Registry
中的类目录结构
+- RegistryService
| +- Registry
| | +- AbstractRegistry
| | | +- FailbackRegistry
| | | | +- ZookeeperRegistry | | | | +- NacosRegistry
| | | | +- ...
目录结构描述如下:
RegistryService
就是我们之前所讲对外提供注册机制的接口。Registry
也同样是一个接口,是对 RegistryService
的集成,并且继承了 Node
接口, 说明注册中心也是基于URL
去做的。AbstractRegistry
是对注册中心的封装,其主要会对本地注册地址的封装,主要功能在于远程注册中心不可用的时候,可以采用本地的注册中心来使用。FailbackRegistry
从名字中可以看出来,失败自动恢复,后台记录失败请求,定时重发功能。(6)、下面我们来看一下在 FailbackRegistry
中的实现, 可以在这里看到他的主要作用是调用第三方的 实现方式,并且在出现错误时增加重试机制。
public void register(URL url) {
if (!this.acceptable(url)) {
this.logger.info("URL " + url + " will not be registered to Registry. Registry " + url + " does not accept service of this protocol type.");
} else {
// 上层调用
// 主要用于保存已经注册的地址列表
super.register(url);
// 将一些错误的信息移除(确保当前地址可以在出现一些错误的地址时可以被删除)
this.removeFailedRegistered(url);
this.removeFailedUnregistered(url);
try {
// 发送给第三方渠道进行注册操作
this.doRegister(url);
} catch (Exception var6) {
Throwable t = var6;
// 记录日志
boolean check = this.getUrl().getParameter("check", true) && url.getParameter("check", true) && !"consumer".equals(url.getProtocol());
boolean skipFailback = var6 instanceof SkipFailbackWrapperException;
if (check || skipFailback) {
if (skipFailback) {
t = var6.getCause();
}
throw new IllegalStateException("Failed to register " + url + " to registry " + this.getUrl().getAddress() + ", cause: " + ((Throwable)t).getMessage(), (Throwable)t);
}
this.logger.error("Failed to register " + url + ", waiting for retry, cause: " + var6.getMessage(), var6);
// 后台异步进行重试,也是Failback比较关键的代码
this.addFailedRegistered(url);
}
}
}
(7)、下面我们再来看看Zookeeper
中 doRegister
方法的实现, 可以看到这里的实现也比较简单,关键在于 toUrlPath
方法的实现。关于 dynamic
的值,我们也在上面有看到,他的URL
也是true
的。
public void doRegister(URL url) {
try {
// 进行创建地址
this.zkClient.create(this.toUrlPath(url), url.getParameter("dynamic", true));
} catch (Throwable var3) {
throw new RpcException("Failed to register " + url + " to zookeeper " + this.getUrl() + ", cause: " + var3.getMessage(), var3);
}
}
(8)、解读 toUrlPath
方法。可以看到这里的实现也是比较简单,也验证了我们之前的路径规则。
private String toUrlPath(URL url) {
// 分类地址 + url字符串
return this.toCategoryPath(url) + "/" + URL.encode(url.toFullString());
}
private String toCategoryPath(URL url) {
// 服务名称 + category(在当前的例子中是providers)
return this.toServicePath(url) + "/" + url.getParameter("category", "providers");
}
private String toServicePath(URL url) {
// 接口地址
String name = url.getServiceInterface();
// 根节点 + 接口地址
return "*".equals(name) ? this.toRootPath() : this.toRootDir() + URL.encode(name);
}
1、URL规则详解
URL地址如下:
protocol://host:port/path?key=value&key=value
provider://192.168.20.1:20883/com.riemann.service.HelloService?anyhost=true&application=service-provider2&bind.ip=192.168.20.1&bind.port=20883&category=configurators&check=false&deprecated=false&dubbo=2.0.2&dynamic=true&generic=false&interface=com.riemann.service
URL
主要有以下几部分组成:
protocol
: 协议,一般像我们的 provider
或者 consumer
在这里都是人为具体的协议host
: 当前 provider
或者其他协议所具体针对的地址,比较特殊的像 override
协议所指定的 host
就是 0.0.0.0
代表所有的机器都生效port
: 和上面相同,代表所处理的端口号path
: 服务路径,在 provider
或者 consumer
等其他中代表着我们真实的业务接口key=value
: 这些则代表具体的参数,这里我们可以理解为对这个地址的配置。比如我们 provider
中需要具体机器的服务应用名,就可以是一个配置的方式设置上去。注意:Dubbo
中的URL
与java
中的URL
是有一些区别的,如下:
parameter
的增加和减少(支持动态更改)2、服务本地缓存
在上面我们有讲到dubbo
有对路径进行本地缓存操作。这里我们就对本地缓存进行讲解。
dubbo
调用者需要通过注册中心(例如:ZK
)注册信息,获取提供者,但是如果频繁往从ZK
获取信息,肯定会存在单点故障问题,所以dubbo
提供了将提供者信息缓存在本地的方法。
Dubbo
在订阅注册中心的回调处理逻辑当中会保存服务提供者信息到本地缓存文件当中(同步/异步两 种方式),以URL
纬度进行全量保存。
Dubbo
在服务引用过程中会创建registry
对象并加载本地缓存文件,会优先订阅注册中心,订阅注册中心失败后会访问本地缓存文件内容获取服务提供信息。
(1)、首先从构造方法讲起, 这里方法比较简单,主要用于确定需要保存的文件信息。并且从系统中读取 已有的配置信息。
public AbstractRegistry(URL url) {
this.setUrl(url);
// Start file save timer
this.syncSaveFile = url.getParameter("save.file", false);
// 默认保存路径(home/.dubbo/dubbo-registry-appName-address-port.cache)
String defaultFilename = System.getProperty("user.home") + "/.dubbo/dubbo-registry-" + url.getParameter("application") + "-" + url.getAddress().replaceAll(":", "-") + ".cache";
String filename = url.getParameter("file", defaultFilename);
// 创建文件
File file = null;
if (ConfigUtils.isNotEmpty(filename)) {
file = new File(filename);
if (!file.exists() && file.getParentFile() != null && !file.getParentFile().exists() && !file.getParentFile().mkdirs()) {
throw new IllegalArgumentException("Invalid registry cache file " + file + ", cause: Failed to create directory " + file.getParentFile() + "!");
}
}
this.file = file;
// 加载已有的配置文件
this.loadProperties();
this.notify(url.getBackupUrls());
}
(2)、我们可以看到这个类中最为关键的一个属性为 properties
,我们可以通过寻找,得知这个属性的设置值只有在一个地方: saveProperties
,我们来看一下这个方法。这里也有一个我们值得关注的点,就是基于版本号的的更改。
private void saveProperties(URL url) {
if (this.file != null) {
try {
StringBuilder buf = new StringBuilder();
// 获取所有通知到的地址
Map<String, List<URL>> categoryNotified = (Map)this.notified.get(url);
if (categoryNotified != null) {
Iterator var4 = categoryNotified.values().iterator();
while(var4.hasNext()) {
List<URL> us = (List)var4.next();
URL u;
for(Iterator var6 = us.iterator(); var6.hasNext(); buf.append(u.toFullString())) {
u = (URL)var6.next();
// 多个地址进行拼接
if (buf.length() > 0) {
buf.append(' ');
}
}
}
}
// 保存数据
this.properties.setProperty(url.getServiceKey(), buf.toString());
// 保存为一个新的版本号
// 通过这种机制可以保证后面保存的记录,在重试的时候,不会重试之前的版本
long version = this.lastCacheChanged.incrementAndGet();
// 需要同步保存则进行保存
if (this.syncSaveFile) {
this.doSaveProperties(version);
} else {
// 否则则异步去进行处理
this.registryCacheExecutor.execute(new AbstractRegistry.SaveProperties(version));
}
} catch (Throwable var8) {
this.logger.warn(var8.getMessage(), var8);
}
}
}
(3)、下面我们再来看看是如何进行保存文件的。这里的实现也比较简单,主要比较关键的代码在于利用文件级锁来保证同一时间只会有一个线程执行。
public void doSaveProperties(long version) {
if (version >= this.lastCacheChanged.get()) {
if (this.file != null) {
// Save
try {
// 使用文件级别所,来保证同一段时间只会有一个线程进行读取操作
File lockfile = new File(this.file.getAbsolutePath() + ".lock");
if (!lockfile.exists()) {
lockfile.createNewFile();
}
RandomAccessFile raf = new RandomAccessFile(lockfile, "rw");
Throwable var5 = null;
try {
FileChannel channel = raf.getChannel();
Throwable var7 = null;
try {
// 利用文件锁来保证并发的执行的情况下,只会有一个线程执行成功(原因在于可能是跨VM的)
FileLock lock = channel.tryLock();
if (lock == null) {
throw new IOException("Can not lock the registry cache file " + this.file.getAbsolutePath() + ", ignore and retry later, maybe multi java process use the file, please config: dubbo.registry.file=xxx.properties");
}
// Save
try {
if (!this.file.exists()) {
this.file.createNewFile();
}
// 将配置的文件信息保存到文件中
FileOutputStream outputFile = new FileOutputStream(this.file);
Throwable var10 = null;
try {
this.properties.store(outputFile, "Dubbo Registry Cache");
} catch (Throwable var73) {
var10 = var73;
throw var73;
} finally {
if (outputFile != null) {
if (var10 != null) {
try {
outputFile.close();
} catch (Throwable var72) {
var10.addSuppressed(var72);
}
} else {
outputFile.close();
}
}
}
} finally {
// 解开文件锁
lock.release();
}
} catch (Throwable var76) {
var7 = var76;
throw var76;
} finally {
if (channel != null) {
if (var7 != null) {
try {
channel.close();
} catch (Throwable var71) {
var7.addSuppressed(var71);
}
} else {
channel.close();
}
}
}
} catch (Throwable var78) {
var5 = var78;
throw var78;
} finally {
if (raf != null) {
if (var5 != null) {
try {
raf.close();
} catch (Throwable var70) {
var5.addSuppressed(var70);
}
} else {
raf.close();
}
}
}
} catch (Throwable var80) {
// 执行出现错误时,则交给专门的线程去进行重试
this.savePropertiesRetryTimes.incrementAndGet();
if (this.savePropertiesRetryTimes.get() >= 3) {
this.logger.warn("Failed to save registry cache file after retrying 3 times, cause: " + var80.getMessage(), var80);
this.savePropertiesRetryTimes.set(0);
return;
}
if (version < this.lastCacheChanged.get()) {
this.savePropertiesRetryTimes.set(0);
return;
}
this.registryCacheExecutor.execute(new AbstractRegistry.SaveProperties(this.lastCacheChanged.incrementAndGet()));
this.logger.warn("Failed to save registry cache file, will retry, cause: " + var80.getMessage(), var80);
}
}
}
}
服务消费流程
首先 ReferenceConfig
类的 init
方法调用 createProxy()
,期间使用 Protocol
调用 refer
方法生成 Invoker
实例(如上图中的红色部分),这是服务消费的关键。接下来使用ProxyFactory
把 Invoker
转换为客户端需要的接口(如:HelloService
)。