• dubbo的Failed to save registry store file问题


    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]
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7

    在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);
                        }
                    }
               }
           });
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23

    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());
        }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18

    从缓存文件里读取了信息,放到property属性里,然后调用notify方法

        protected void notify(List urls) {
            if (urls == null || urls.isEmpty())return;
     
            for (Map.Entry> entry : getSubscribed().entrySet()) {
                URL url = entry.getKey();
     
                if (!UrlUtils.isMatch(url,urls.get(0))) {
                    continue;
                }
     
                Set 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);
                        }
                    }
                }
            }
        }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22

    遍历所有的linstener,调用notify方法:

        protected void notify(URL url,NotifyListener listener, List 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>result = new HashMap>();
            for (URL u : urls) {
                if (UrlUtils.isMatch(url, u)) {
                    String category =u.getParameter(Constants.CATEGORY_KEY, Constants.DEFAULT_CATEGORY);
                    List categoryList =result.get(category);
                    if (categoryList == null) {
                        categoryList = newArrayList();
                        result.put(category,categoryList);
                    }
                    categoryList.add(u);
                }
            }
            if (result.size() == 0) {
                return;
            }
            Map>categoryNotified = notified.get(url);
            if (categoryNotified == null) {
                notified.putIfAbsent(url, newConcurrentHashMap>());
                categoryNotified =notified.get(url);
            }
            for (Map.Entry> entry : result.entrySet()) {
                String category = entry.getKey();
                List categoryList =entry.getValue();
                categoryNotified.put(category,categoryList);
                saveProperties(url);
                listener.notify(categoryList);
            }
        }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    • 40
    • 41
    • 42
    • 43

    在最后linstener调用notify方法前,调用了saveProperties方法,保存缓存文件的代码就在这

        private void saveProperties(URL url) {
            if (file == null) {
                return;
            }
     
            try {
                StringBuilder buf = newStringBuilder();
                Map>categoryNotified = notified.get(url);
                if (categoryNotified != null) {
                    for (List 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);
            }
        }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29

    syncSaveFile代表是否同步保存文件,这个参数可以配置,默认是同步保存,直接执行doSaveProperties方法。如果选择异步,会建立一个有ExecutorService,类似定时任务,执行的也是doSaveProperties方法。

        public void doSaveProperties(long version) {
            if (version 
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    • 40
    • 41
    • 42
    • 43
    • 44
    • 45
    • 46
    • 47
    • 48
    • 49
    • 50
    • 51
    • 52
    • 53
    • 54
    • 55
    • 56
    • 57
    • 58
    • 59
    • 60
    • 61
    • 62
    • 63
    • 64
    • 65
    • 66
    • 67
    • 68
    • 69
    • 70

    可以看到,dubbo先把日志文件读出来,然后锁定.lock文件,把缓存写到文件里,最后释放锁。这里的锁用的是FileLock,这是进程级别的锁,也就是说,如果两个dubbo服务都要同时写缓存文件,就会有一个会因为无法获得锁而抛出本文最上面的IOException。

    说白了就是不要把dubbo服务都扔在一个服务器上,扔在一个服务器上也最好改改缓存文件地址,各人用各人的。

    贴一下位于注册类最顶端的Registry接口,定义了注册机制最基础的注册行为:

    public interfaceRegistryService {
     
        /**
         * 注册数据,比如:提供者地址,消费者地址,路由规则,覆盖规则,等数据。
         * 

    * 注册需处理契约:
    * 1. 当URL设置了check=false时,注册失败后不报错,在后台定时重试,否则抛出异常。
    * 2. 当URL设置了dynamic=false参数,则需持久存储,否则,当注册者出现断电等情况异常退出时,需自动删除。
    * 3. 当URL设置了category=routers时,表示分类存储,缺省类别为providers,可按分类部分通知数据。
    * 4. 当注册中心重启,网络抖动,不能丢失数据,包括断线自动删除数据。
    * 5. 允许URI相同但参数不同的URL并存,不能覆盖。
    * * @param url 注册信息,不允许为空,如:dubbo://10.20.153.10/com.alibaba.foo.BarService?version=1.0.0&application=kylin */ void register(URL url); /** * 取消注册. *

    * 取消注册需处理契约:
    * 1. 如果是dynamic=false的持久存储数据,找不到注册数据,则抛IllegalStateException,否则忽略。
    * 2. 按全URL匹配取消注册。
    * * @param url 注册信息,不允许为空,如:dubbo://10.20.153.10/com.alibaba.foo.BarService?version=1.0.0&application=kylin */ void unregister(URL url); /** * 订阅符合条件的已注册数据,当有注册数据变更时自动推送. *

    * 订阅需处理契约:
    * 1. 当URL设置了check=false时,订阅失败后不报错,在后台定时重试。
    * 2. 当URL设置了category=routers,只通知指定分类的数据,多个分类用逗号分隔,并允许星号通配,表示订阅所有分类数据。
    * 3. 允许以interface,group,version,classifier作为条件查询,如:interface=com.alibaba.foo.BarService&version=1.0.0
    * 4. 并且查询条件允许星号通配,订阅所有接口的所有分组的所有版本,或:interface=*&group=*&version=*&classifier=*
    * 5. 当注册中心重启,网络抖动,需自动恢复订阅请求。
    * 6. 允许URI相同但参数不同的URL并存,不能覆盖。
    * 7. 必须阻塞订阅过程,等第一次通知完后再返回。
    * *@param url 订阅条件,不允许为空,如:consumer://10.20.153.10/com.alibaba.foo.BarService?version=1.0.0&application=kylin * @param listener 变更事件监听器,不允许为空 */ void subscribe(URL url, NotifyListenerlistener); /** * 取消订阅. *

    * 取消订阅需处理契约:
    * 1. 如果没有订阅,直接忽略。
    * 2. 按全URL匹配取消订阅。
    * * @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)}的参数。 * @seecom.alibaba.dubbo.registry.NotifyListener#notify(List) */ List lookup(URL url); }

    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    • 40
    • 41
    • 42
    • 43
    • 44
    • 45
    • 46
    • 47
    • 48
    • 49
    • 50
    • 51
    • 52
    • 53
    • 54
    • 55
    • 56
    • 57
    • 58
    • 59
    • 60
    • 61
    • 62
    • 63
    • 64
    • 65
    • 66

    最终我没在代码中找到这个缓存文件是怎么影响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都清净了。

  • 相关阅读:
    15:00面试,15:08就出来了,问的问题有点变态。。。
    vue路由 & nodeJS环境搭建
    基于javaweb的毕业设计毕业论文管理系统(java+ssm+jsp+tomcat+mysql)
    关于Arction的问题(语言-c#)
    Python接口自动化测试post请求和get请求,获取请求返回值
    调整SGA遇到ORA-00845错误
    Spring Boot 第三篇:理解 spring-boot-starter-parent
    Ubuntu Minkowski Engine安装
    linux配置文件共享
    wangEditor在Vue3中的使用
  • 原文地址:https://blog.csdn.net/m0_67390969/article/details/126656664