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都清净了。

 

Logo

权威|前沿|技术|干货|国内首个API全生命周期开发者社区

更多推荐