Dubbo的Provider,Consumer在启动时都会创建一个注册中心,注册中心可以选择Zookeeper,Redis。常用的是Zookeeper,我们这篇博客主要讲的就是Dubbo与Zookeeper的注册交互过程。
Dubbo里默认使用zkclient
来操作zookeeper服务器,其对zookeeper原始客户单做了一定的封装,操作zookeeper时能便捷一些,比如不需要手动处理session
超时,不需要重复注册watcher
等等。
分布式服务框架Dubbo中使用zookeeper来作为其命名服务,维护全局的服务地址列表。在Dubbo是实现中:服务提供者provider在启动的时候,向Zookeeper的指定节点:~/dubbo/${serviceName}/providers
目录下写入自己的URL地址,这个操作就会完成服务的发布。一般数据存放路径为 /zookeeper-xxx/data/version-xx/log.x
。
服务消费者启动的时候,订阅~/dubbo/${serviceName}/providers
目录下的提供者URL地址。注意:所有向ZK上注册的地址都是临时节点,这样就能够保证服务提供者和消费者能够自动感应资源的变化。Dubbo还有针对服务粒度的监控,方法是订阅/dubbo/${serviceName}目录下所有提供者和消费者的信息。
Dubbo在Zookeeper上注册的节点目录:假设接口名称是:com.bob.dubbo.service.CityDubboService
如果注册中心集群都挂掉,发布者和订阅者之间还能通信么?
可以通信,启动dubbo时,消费者会从zk拉去注册的生产者的地址接口作为数据,缓存在本地,每次调用安装本地的缓存地址进行调用。
在具体讲解ZookeeperRegistry的相关源码之前,先来分析下dubbo在zookeeper的目录结构以及dubbo如何利用这个特性:
针对每个接口节点会存在以下4个子节点:
节点名 | 作用 |
---|---|
consumers | 存储消费者节点url |
configuators | 存储override或者absent url,用于服务治理 |
routers | 用于设置路由url,用于服务治理 |
providers | 存储在线提供者url |
如图:
Dubbo启动时,Consumer和Provider都会把自身的URL格式化为字符串,然后注册到zookeeper相应节点下,作为一个临时节点,当连断开时,节点被删除。
Consumer在启动时,不仅仅会注册自身到 …/consumers/目录下,同时还会订阅…/providers目录,实时获取其上Provider的URL字符串信息。
zookeeper注册中心的源码为com.alibaba.dubbo.registry.zookeeper.ZookeeperRegistry
。
ZookeeperRegistry
类继承自 FailbackRegistry
,FailbackRegistry
又继承自 AbstractRegistry
,AbstractRegistry
实现了 RegistryService
接口。
因此我们阅读源码顺序为:RegistryService -> AbstractRegistry -> FailbackRegistry -> ZookeeperRegistry
RegistryService 接口
AbstractRegistry 抽象类
从构造函数可以看出 AbstractRegistry
抽象类主要是提供了对注册中心数据的文件缓存。
Dubbo会在用户目录创建
./dubbo
文件夹及缓存文件,以windows为例,生成的缓存文件为:C:\Users\你的登录用户名/.dubbo/dubbo-registry-127.0.0.1.cache
FailbackRegistry 抽象类
FailbackRegistry
顾名思义是主要提供的是失败自动恢复,同样看一下构造函数,在构造函数中会通过 ScheduledExecutorService
一直执行Retry
方法进行重试。
retry()方法主要的从各个操作中的失败列表取出失败的操作进行重试。
ZookeeperRegistry 类
同时提供了几个抽象方法
ZookeeperRegistry流程
服务提供者启动时
向/dubbo/com.foo.BarService/providers目录下写入自己的URL地址。
服务消费者启动时
订阅/dubbo/com.foo.BarService/providers目录下的提供者URL地址。
并向/dubbo/com.foo.BarService/consumers目录下写入自己的URL地址。
监控中心启动时
订阅/dubbo/com.foo.BarService目录下的所有提供者和消费者URL地址。
ZookeeperRegistry
主要是实现了FailbackRegistry
的那几个抽象方法。本次也主要分析 doRegister(),doSubscribe()这两个方法。
注册:
doRegister() 主要是调用zkClient创建一个节点。 create()以递归的方式创建节点,通过判断Url中dynamic=false 判断创建的是持久化节点还是临时节点。
主要做了以下几步:
1)记录注册注册地址 2) 注册节点到zookeeper上 3) 捕捉错误信息,出错则记录下来,等待定期器去重新执行
订阅
doSubscribe()
doSubscribe()
订阅Zookeeper节点是通过创建ChildListener
来实现的具体调用的方法是 addChildListener()
addChildListener()
又调用 AbstractZookeeperClient.addTargetChildListener()
然后调用subscribeChildChanges()
最后调用ZkclientZookeeperClient ZkClientd.watchForChilds()
消费者在引用服务时,会订阅接口下的providers的节点。一旦providers下的子节点发生改变(提供者的服务器增加或者删除),会通知到消费者。消费者会把提供者的集群地址缓存到本地。
主要做了以下几步操作(以具体接口为例)
1) 将订阅信息记录到集合中 `
2) 将路径转变成/dubbo/xxService/providers,
/dubbo/xxService/configurators
,/dubbo/xxService/routers 循环这三个路径
如果消费者的接口没有创建过子节点监听器,那么就创建子节点监听器
创建路径节点,并将子节点监听器放入到节点上。(一旦子节点发生改变,就通知)
获取到当前路径节点下的所有子节点(提供者),将这些子节点组装成集合;
如果没有节点,那么就将消费者的地址的协议变成empty、
empty://10.118.14.204/com.test.ITestService?
application\=testservice&category\=configurators
通知
3) 出现异常,根据url从本地缓存文件中获取到提供者的地址,通知
[站外图片上传中...(image-3598a1-1550486301310)]
ZookeeperRegistry
接口是* (对所有的接口进行订阅,有点类似于递归订阅)
`1) 如果在集合中没有创建过*的子节点监听器,那么就创建子节点监听器,一旦root下的子节点(service)发生改变,那么就对这个节点就行订阅NotifyListener 。(这时就有具体的接口了)
- 创建root节点,将子节点监听器放入到root上。并返回root下的所有的接口,对这些接口订阅NotifyListener`
接口是具体 (以providers为例)
`1)将接口名称转变成/dubbo/com.test.ITestService/providers,集合中没有没有providers的子节点监听器,就创建子节点监听器。一旦子节点发生改变,那么就通知
- 创建 /dubbo/com.test.ITestService/providers,并且将子节点监听器放入到这个节点上,并返回所有的子节点(提供者),通知。`
总结
当消费者要订阅接口中的提供者时
会监听/dubbo/xxService/providers下的所有提供者。一旦提供者的节点删除或增加时,都会通知到消费者的url(consumer://10.118.14.204/com.test.ITestService…….)
它会监听以下三个节点的子节点
1) /dubbo/xxService/providers 2)/dubbo/xxService/configurators 3)/dubbo/xxService/routers
组装的url集合(即提供者的子节点providers,configurators,routers下的子节点)。如果没有子节点(没有提供者),那么就将消费者的协议变成empty作为url。
protected void doSubscribe(final URL url, final NotifyListener listener) {
//接口名称(*代表需要监听root下面的所有节点)
if ("*".equals(url.getServiceInterface())) {
ConcurrentMap listeners = zkListeners.
get(url);
//如果listeners为空创建并放入到map中...
ChildListener zkListener = listeners.get(listener);
/**
root下的子节点是service接口
创建子节点监听器,对root下的子节点做监听,一旦有子节点发生改变,
那么就对这个节点进行订阅.
**/
if (zkListener == null) {
listeners.putIfAbsent(listener, new ChildListener() {
public void childChanged(String parentPath, List
currentChilds) {
for (String child : currentChilds) {
//如果不存在,才订阅
if (! anyServices.contains(child)) {
anyServices.add(child);
//订阅
subscribe(url.setPath(child).addParameters(
"interface", child,"check", "false"), listener);
}
}
}
});
zkListener = listeners.get(listener);
}
//创建root节点
zkClient.create(root, false);
//添加root节点的子节点监听器,并返回当前的services
List services = zkClient.addChildListener(root, zkListener);
if (services != null && services.size() > 0) {
//对root下的所有service节点进行订阅
for (String service : services) {
service = URL.decode(service);
anyServices.add(service);
subscribe(url.setPath(service).addParameters("interface",
service, "check", "false"), listener);
}
}
} else {
List urls = new ArrayList();
/**将url转变成
/dubbo/com.test.ITestService/providers
/dubbo/com.test.ITestService/configurators
/dubbo/com.test.ITestService/routers
**/
for (String path : toCategoriesPath(url)) {
ConcurrentMap listeners =
zkListeners.get(url);
//如果listeners为空就创建并放入盗map中
ChildListener zkListener = listeners.get(listener);
/**
对接口下的providers的子节点进行监听,一旦发生改变,就通知
**/
if (zkListener == null) {
listeners.putIfAbsent(listener, new ChildListener() {
public void childChanged(String parentPath, List
currentChilds) {
//通知
ZookeeperRegistry.this.notify(url, listener,
toUrlsWithEmpty(url, parentPath, currentChilds));
}
});
zkListener = listeners.get(listener);
}
//创建/dubbo/com.test.ITestService/providers
zkClient.create(path, false);
//获取到providers的所有子节点(提供者)
List children = zkClient.addChildListener(path,
zkListener);
//获取到所有的提供者,组装起来
if (children != null) {
//有子节点组装,没有那么就将消费者的协议变成empty作为url。
urls.addAll(toUrlsWithEmpty(url, path, children));
}
}
//通知/dubbo/com.test.ITestService/providers的所有子节点
notify(url, listener, urls);
}
}
/**
根据url获取到哪些类型
consumer://10.118.14.204/com.test.ITestService?application=testservice
&category=providers,configurators,routers&...
这里的category是重点
**/
private String[] toCategoriesPath(URL url) {
String[] categroies;
//如果是*
if ("*".equals(url.getParameter(Constants.CATEGORY_KEY)))
categroies = new String[] {"providers", "consumers",
"routers", "configurators"};
else
//从url获取到category的值,没有的话就默认providers
categroies = url.getParameter("category",
new String[] {"providers"});
String[] paths = new String[categroies.length];
//将格式转变成/dubbo/xxService/类型
for (int i = 0; i < categroies.length; i ++) {
paths[i] = toServicePath(url) + "/" + categroies[i];
}
return paths;
}
/**
组装providers、routers、configurators下的url。
如果有提供者那么就组装;没有的话,就将消费者的协议变成empty
**/
private List toUrlsWithEmpty(URL consumer, String path, List providers) {
List urls = toUrlsWithoutEmpty(consumer, providers);
if (urls.isEmpty()) {
int i = path.lastIndexOf('/');
String category = i < 0 ? path : path.substring(i + 1);
URL empty = consumer.setProtocol("empty").addParameter(
"category", category);
urls.add(empty);
}
return urls;
}
通知
有三个参数
url: 消费者的地址 consumer://10.118.14.204/com….. listener: 监听器 urls: providers,configurators和routers
FailbackRegistry
主要是铺捉到异常时放入到集合中,定时重试
AbstractRegistry
urls三种
1) providers(providers下的子节点) dubbo://10.118.22.29:20710/com.test.ITestService?anyhost=true&application=testservice&default.cluster=failfast…
2) configurators(configurators下的子节点为空,将消费者的url变成empty ) empty://10.118.14.204/com.test.ITestService?application=testservice&category=configurators&default.check=false..
3) routers(routers下的子节点为空,将消费者的url变成empty) empty://10.118.14.204/com.test.ITestService?application=testservice&category=routers&default.check=false..
保存到本地缓存文件
组装url保存到properties中,如果是同步,直接保存到本地缓存文件中,否则文件缓存定时写入
首先会有个dubbo-registry-10.118.22.25.cache.lock
,会获取这个文件的锁,然后保存dubbo-registry-10.118.22.25.cache
文件,再释放锁。
public void doSaveProperties(long version) {
if (version < lastCacheChanged.get()) {
return;
}
if (file == null) {
return;
}
Properties newProperties = new Properties();
// 保存之前先读取一遍,防止多个注册中心之间冲突
InputStream in = null;
try {
if (file.exists()) {
in = new FileInputStream(file);
newProperties.load(in);
}
} catch (Throwable e) {
logger.warn("Failed to load registry store file, cause: " + e.getMessage(), e);
} finally {
if (in != null) {
try {
in.close();
} catch (IOException e) {
logger.warn(e.getMessage(), e);
}
}
}
// 保存
try {
newProperties.putAll(properties);
File lockfile = new File(file.getAbsolutePath() + ".lock");
if (!lockfile.exists()) {
lockfile.createNewFile();
}
RandomAccessFile raf = new RandomAccessFile(lockfile, "rw");
try {
FileChannel channel = raf.getChannel();
try {
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");
}
// 保存
try {
if (!file.exists()) {
file.createNewFile();
}
FileOutputStream outputFile = new FileOutputStream(file);
try {
newProperties.store(outputFile, "Dubbo Registry Cache");
} finally {
outputFile.close();
}
} finally {
lock.release();
}
} finally {
channel.close();
}
} finally {
raf.close();
}
} catch (Throwable e) {
if (version < lastCacheChanged.get()) {
return;
} else {
registryCacheExecutor.execute(new SaveProperties(lastCacheChanged.incrementAndGet()));
}
logger.warn("Failed to save registry store file, cause: " + e.getMessage(), e);
}
}
监听器通知 (当收到服务变更通知时触发。)
当收到提供者的地址发生改变时,这时刷新缓存中的invoker,如果url不存在,那么重新refer(根据dubbo协议)
通知需处理契约:
1. 总是以服务接口和数据类型为维度全量通知,即不会通知一个服务的同类型的部分数据,用户不需要对比上一次通知结果。
2. 订阅时的第一次通知,必须是一个服务的所有类型数据的全量通知。
3. 中途变更时,允许不同类型的数据分开通知,比如:providers, consumers, routers, overrides,允许只通知其中一种类型,但该类型的数据必须是全量的,不是增量的。
4. 如果一种类型的数据为空,需通知一个empty协议并带category参数的标识性URL数据。
5. 通知者(即注册中心实现)需保证通知的顺序,比如:单线程推送,队列串行化,带版本对比。