dubbo的日志中出现了这种信息:
[WARN ] 2017-11-03 15:15:20,988--DubboSaveRegistryCache-thread-1--[com.alibaba.dubbo.registry.zookeeper.ZookeeperRegistry] [DUBBO] Failed to save registry store file,cause: Can not lock the registry cache file /root/.dubbo/dubbo-registry-10.255.242.99.cache,ignore and retry later, maybe multi java process use the file, please config:dubbo.registry.file=xxx.properties, dubbo version: 2.8.3.2, current host:10.255.242.97
java.io.IOException: Can not lock theregistry cache file /root/.dubbo/dubbo-registry-10.255.242.99.cache, ignore andretry later, maybe multi java process use the file, please config:dubbo.registry.file=xxx.properties
atcom.alibaba.dubbo.registry.support.AbstractRegistry.doSaveProperties(AbstractRegistry.java:193)~[dubbo-2.8.3.2.jar:2.8.3.2]
atcom.alibaba.dubbo.registry.support.AbstractRegistry$SaveProperties.run(AbstractRegistry.java:150)[dubbo-2.8.3.2.jar:2.8.3.2]
atjava.util.concurrent.ThreadPoolExecutor.runWorker(ThreadPoolExecutor.java:1110)[na:1.7.0_09-icedtea]
atjava.util.concurrent.ThreadPoolExecutor$Worker.run(ThreadPoolExecutor.java:603)[na:1.7.0_09-icedtea]
atjava.lang.Thread.run(Thread.java:722) [na:1.7.0_09-icedtea]
在dubbo服务启动时有可能会有这样的警告信息,不管是provider还是consumer启动时都有可能,甚至在provider启动的时候相关的consumer也在报。
这是个什么玩意?
这段日志说的是dubbo服务需要锁定并保存缓冲文件,但是现在锁定失败了。
什么缓冲文件呢?为啥要锁定呢?
事情是这样的
- dubbo使用zookeeper作为注册中心,每一个provider和consumer都必须在zookeeper上注册在案。
- 每当有provider或者consumer启动或者停止服务,zookeeper会向记录在案的所有dubbo服务广播provider或者consumer列表的变化,每个dubbo服务都会把现在zookeeper注册在案的dubbo服务列表缓存在本地文件,自己不用的provider的地址也会缓存。
- dubbo有个帅气的功能,zookeeper挂了也不影响consumer调用provider,因为provider地址都缓存在本地了,zookeeper连不上还可以从缓存文件里找。
- 缓存文件默认在/{user.home}/.dubbo/ 文件夹下,文件名是dubbo-registry-{zookeeper地址}.cache,还有个给锁用的文件,文件名是dubbo-registry-{zookeeper地址}.cache.lock,内容一直是空的,只是用来当锁的。
- 我本地的缓存文件名字是dubbo-registry-10.255.242.99.cache和dubbo-registry-10.255.242.99.cache.lock。路径是默认的,/root/.dubbo
dubbo是怎么玩缓存的?
如日志中所说,日志出现在ZookeeperRegistry类,这个类是向zookeeper注册用的。
dubbo中的注册类还有好几个,他们的继承和实现关系关系大概是这样的:
这几个注册类dubbo是以工厂模式来使用的。
虽然日志说日志出现在ZookeeperRegistry类,实际上这个日志相关的代码在AbstractRegistry中,只不过每个注册类在构造方法里面的第一行永远都是super(url);
ZookeeperRegistry类的构造函数是这样的:
public ZookeeperRegistry(URL url, ZookeeperTransporterzookeeperTransporter) {
super(url);
if (url.isAnyHost()) {
throw new IllegalStateException("registry address == null");
}
String group = url.getParameter(Constants.GROUP_KEY, DEFAULT_ROOT);
if (!group.startsWith(Constants.PATH_SEPARATOR)) {
group = Constants.PATH_SEPARATOR + group;
}
this.root = group;
zkClient = zookeeperTransporter.connect(url);
zkClient.addStateListener(new StateListener() {
public void stateChanged(int state) {
if (state == RECONNECTED) {
try {
recover();
} catch (Exception e) {
logger.error(e.getMessage(), e);
}
}
}
});
}
ZookeeperRegistry的父类是FailbackRegistry,FailbackRegistry的构造函数也是上来就super,FailbackRegistry的父类是AbstractRegistry,直接看他的构造方法:
public AbstractRegistry(URL url) {
setUrl(url);
// 启动文件保存定时器
syncSaveFile =url.getParameter(Constants.REGISTRY_FILESAVE_SYNC_KEY, false);
String filename =url.getParameter(Constants.FILE_KEY, System.getProperty("user.home")+ "/.dubbo/dubbo-registry-" + url.getHost() + ".cache");
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 newIllegalArgumentException("Invalid registry store file " + file +", cause: Failed to create directory " + file.getParentFile() +"!");
}
}
}
this.file = file;
loadProperties();
notify(url.getBackupUrls());
}
从缓存文件里读取了信息,放到property属性里,然后调用notify方法
protected void notify(List<URL> urls) {
if (urls == null || urls.isEmpty())return;
for (Map.Entry<URL,Set<NotifyListener>> entry : getSubscribed().entrySet()) {
URL url = entry.getKey();
if (!UrlUtils.isMatch(url,urls.get(0))) {
continue;
}
Set<NotifyListener> listeners= entry.getValue();
if (listeners != null) {
for (NotifyListener listener :listeners) {
try {
notify(url, listener,filterEmpty(url, urls));
} catch (Throwable t) {
logger.error("Failed to notify registry event, urls: " + urls+ ", cause: " + t.getMessage(), t);
}
}
}
}
}
遍历所有的linstener,调用notify方法:
protected void notify(URL url,NotifyListener listener, List<URL> urls) {
if (url == null) {
throw newIllegalArgumentException("notify url == null");
}
if (listener == null) {
throw newIllegalArgumentException("notify listener == null");
}
if ((urls == null || urls.size() == 0)
&&!Constants.ANY_VALUE.equals(url.getServiceInterface())) {
logger.warn("Ignore emptynotify urls for subscribe url " + url);
return;
}
if (logger.isInfoEnabled()) {
logger.info("Notify urls forsubscribe url " + url + ", urls: " + urls);
}
Map<String, List<URL>>result = new HashMap<String, List<URL>>();
for (URL u : urls) {
if (UrlUtils.isMatch(url, u)) {
String category =u.getParameter(Constants.CATEGORY_KEY, Constants.DEFAULT_CATEGORY);
List<URL> categoryList =result.get(category);
if (categoryList == null) {
categoryList = newArrayList<URL>();
result.put(category,categoryList);
}
categoryList.add(u);
}
}
if (result.size() == 0) {
return;
}
Map<String, List<URL>>categoryNotified = notified.get(url);
if (categoryNotified == null) {
notified.putIfAbsent(url, newConcurrentHashMap<String, List<URL>>());
categoryNotified =notified.get(url);
}
for (Map.Entry<String,List<URL>> entry : result.entrySet()) {
String category = entry.getKey();
List<URL> categoryList =entry.getValue();
categoryNotified.put(category,categoryList);
saveProperties(url);
listener.notify(categoryList);
}
}
在最后linstener调用notify方法前,调用了saveProperties方法,保存缓存文件的代码就在这
private void saveProperties(URL url) {
if (file == null) {
return;
}
try {
StringBuilder buf = newStringBuilder();
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);
}
}
syncSaveFile代表是否同步保存文件,这个参数可以配置,默认是同步保存,直接执行doSaveProperties方法。如果选择异步,会建立一个有ExecutorService,类似定时任务,执行的也是doSaveProperties方法。
public void doSaveProperties(long version) {
if (version <lastCacheChanged.get()) {
return;
}
if (file == null) {
return;
}
Properties newProperties = newProperties();
// 保存之前先读取一遍,防止多个注册中心之间冲突
InputStream in = null;
try {
if (file.exists()) {
in = new FileInputStream(file);
newProperties.load(in);
}
} catch (Throwable e) {
logger.warn("Failed to loadregistry 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 = newFile(file.getAbsolutePath() + ".lock");
if (!lockfile.exists()) {
lockfile.createNewFile();
}
RandomAccessFile raf = newRandomAccessFile(lockfile, "rw");
try {
FileChannel channel =raf.getChannel();
try {
FileLock lock =channel.tryLock();
if (lock == null) {
throw newIOException("Can not lock the registry cache file " +file.getAbsolutePath() + ", ignore and retry later, maybe multi javaprocess use the file, please config: dubbo.registry.file=xxx.properties");
}
// 保存
try {
if (!file.exists()) {
file.createNewFile();
}
FileOutputStreamoutputFile = 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 saveregistry store file, cause: " + e.getMessage(), e);
}
}
可以看到,dubbo先把日志文件读出来,然后锁定.lock文件,把缓存写到文件里,最后释放锁。这里的锁用的是FileLock,这是进程级别的锁,也就是说,如果两个dubbo服务都要同时写缓存文件,就会有一个会因为无法获得锁而抛出本文最上面的IOException。
说白了就是不要把dubbo服务都扔在一个服务器上,扔在一个服务器上也最好改改缓存文件地址,各人用各人的。
贴一下位于注册类最顶端的Registry接口,定义了注册机制最基础的注册行为:
public interfaceRegistryService {
/**
* 注册数据,比如:提供者地址,消费者地址,路由规则,覆盖规则,等数据。
* <p>
* 注册需处理契约:<br>
* 1. 当URL设置了check=false时,注册失败后不报错,在后台定时重试,否则抛出异常。<br>
* 2. 当URL设置了dynamic=false参数,则需持久存储,否则,当注册者出现断电等情况异常退出时,需自动删除。<br>
* 3. 当URL设置了category=routers时,表示分类存储,缺省类别为providers,可按分类部分通知数据。<br>
* 4. 当注册中心重启,网络抖动,不能丢失数据,包括断线自动删除数据。<br>
* 5. 允许URI相同但参数不同的URL并存,不能覆盖。<br>
*
* @param url 注册信息,不允许为空,如:dubbo://10.20.153.10/com.alibaba.foo.BarService?version=1.0.0&application=kylin
*/
void register(URL url);
/**
* 取消注册.
* <p>
* 取消注册需处理契约:<br>
* 1. 如果是dynamic=false的持久存储数据,找不到注册数据,则抛IllegalStateException,否则忽略。<br>
* 2. 按全URL匹配取消注册。<br>
*
* @param url 注册信息,不允许为空,如:dubbo://10.20.153.10/com.alibaba.foo.BarService?version=1.0.0&application=kylin
*/
void unregister(URL url);
/**
* 订阅符合条件的已注册数据,当有注册数据变更时自动推送.
* <p>
* 订阅需处理契约:<br>
* 1. 当URL设置了check=false时,订阅失败后不报错,在后台定时重试。<br>
* 2. 当URL设置了category=routers,只通知指定分类的数据,多个分类用逗号分隔,并允许星号通配,表示订阅所有分类数据。<br>
* 3. 允许以interface,group,version,classifier作为条件查询,如:interface=com.alibaba.foo.BarService&version=1.0.0<br>
* 4. 并且查询条件允许星号通配,订阅所有接口的所有分组的所有版本,或:interface=*&group=*&version=*&classifier=*<br>
* 5. 当注册中心重启,网络抖动,需自动恢复订阅请求。<br>
* 6. 允许URI相同但参数不同的URL并存,不能覆盖。<br>
* 7. 必须阻塞订阅过程,等第一次通知完后再返回。<br>
*
*@param url 订阅条件,不允许为空,如:consumer://10.20.153.10/com.alibaba.foo.BarService?version=1.0.0&application=kylin
* @param listener 变更事件监听器,不允许为空
*/
void subscribe(URL url, NotifyListenerlistener);
/**
* 取消订阅.
* <p>
* 取消订阅需处理契约:<br>
* 1. 如果没有订阅,直接忽略。<br>
* 2. 按全URL匹配取消订阅。<br>
*
* @param url 订阅条件,不允许为空,如:consumer://10.20.153.10/com.alibaba.foo.BarService?version=1.0.0&application=kylin
* @param listener 变更事件监听器,不允许为空
*/
void unsubscribe(URL url, NotifyListenerlistener);
/**
* 查询符合条件的已注册数据,与订阅的推模式相对应,这里为拉模式,只返回一次结果。
*
* @param url 查询条件,不允许为空,如:consumer://10.20.153.10/com.alibaba.foo.BarService?version=1.0.0&application=kylin
* @return 已注册信息列表,可能为空,含义同{@linkcom.alibaba.dubbo.registry.NotifyListener#notify(List<URL>)}的参数。
* @seecom.alibaba.dubbo.registry.NotifyListener#notify(List)
*/
List<URL> lookup(URL url);
}
最终我没在代码中找到这个缓存文件是怎么影响consumer发起连接的,但是基本可以确认dubbo有时候大量的disconect日志跟缓存文件的缓存失败有关系。
有一次更是出现了一种神奇的现象,consumer出现了跨zookeeper的连接,当时的服务部署图如下:
测试服务器有个zookeeper,有一个provider和一个consumer注册到了这个zookeeper,相关的service只有ServiceA
我本地也起了个zookeeper,然后在我本地起了一个ServiceA的provider,注册到我本地的zookeeper上。
本地的provider启动之后,神奇的事情发生了,本地service居然不断提示说与测试服务器的consumer连接断开,测试服务器的consumer也在不断提示与我本地的连接断开(连接的是我电脑的ip地址)。可从这个部署情况来看,我本地的provider和测试服务器的consumer应该没有半毛钱关系才对。
我登录测试环境zookeeper,查看了ServiceA相关的节点,provider就只有测试服务器的provider一个,consumer也只有测试服务器consumer一个,没有我本机provider什么事。
然后突然想起来我本机的provider之前曾经在测试服务器的zookeeper注册过,后来服务停止之后才改成我本地zookeeper地址然后重启启动的。
zookeeper节点没问题,那么能让consumer没头脑的企图往我本地电脑上建立连接的,也只有因为这个缓存文件了,根据日志显示,测试服务器上的consumer启动的时候缓存失败了,日志中显示了文章开头那个警告。
重启consumer之后,这回缓存成功,我本地的provider和测试服务器consumer都清净了。
转自:https://cloud.tencent.com/developer/article/1706974