前往小程序,Get更优阅读体验!
立即前往
首页
学习
活动
专区
工具
TVP
发布
社区首页 >专栏 >dubbo的Failed to save registry store file问题

dubbo的Failed to save registry store file问题

作者头像
MickyInvQ
发布2020-09-27 17:49:11
2.8K0
发布2020-09-27 17:49:11
举报
文章被收录于专栏:InvQ的专栏InvQ的专栏InvQ的专栏

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服务需要锁定并保存缓冲文件,但是现在锁定失败了。

什么缓冲文件呢?为啥要锁定呢?

事情是这样的

  1. dubbo使用zookeeper作为注册中心,每一个provider和consumer都必须在zookeeper上注册在案。
  2. 每当有provider或者consumer启动或者停止服务,zookeeper会向记录在案的所有dubbo服务广播provider或者consumer列表的变化,每个dubbo服务都会把现在zookeeper注册在案的dubbo服务列表缓存在本地文件,自己不用的provider的地址也会缓存。
  3. dubbo有个帅气的功能,zookeeper挂了也不影响consumer调用provider,因为provider地址都缓存在本地了,zookeeper连不上还可以从缓存文件里找。
  4. 缓存文件默认在/{user.home}/.dubbo/ 文件夹下,文件名是dubbo-registry-{zookeeper地址}.cache,还有个给锁用的文件,文件名是dubbo-registry-{zookeeper地址}.cache.lock,内容一直是空的,只是用来当锁的。
  5. 我本地的缓存文件名字是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都清净了。

本文参与 腾讯云自媒体分享计划,分享自作者个人站点/博客。
原始发表:2017-11-15 ,如有侵权请联系 cloudcommunity@tencent.com 删除

本文分享自 作者个人站点/博客 前往查看

如有侵权,请联系 cloudcommunity@tencent.com 删除。

本文参与 腾讯云自媒体分享计划  ,欢迎热爱写作的你一起参与!

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
相关产品与服务
测试服务
测试服务 WeTest 包括标准兼容测试、专家兼容测试、手游安全测试、远程调试等多款产品,服务于海量腾讯精品游戏,涵盖兼容测试、压力测试、性能测试、安全测试、远程调试等多个方向,立体化安全防护体系,保卫您的信息安全。
领券
问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档