太极限了,JDK的这个BUG都能被我踩到

捉虫大师
• 阅读 891

hello,大家好呀,我是小楼。

之前遇到个文件监听变更的问题,刚好这周末有空研究了一番,整理出来分享给大家。

从一次故障说起

我们还是从故障说起,这样更加贴近实际,也能让大家更快速理解背景。

有一个下发配置的服务,这个配置服务的实现有点特殊,服务端下发配置到各个服务的本地文件,当然中间经过了一个agent,如果没有agent也就无法写本地文件,然后由client端的程序监听这个配置文件,一旦文件有变更,就重新加载配置,画个架构图大概是这样: 太极限了,JDK的这个BUG都能被我踩到

今天的重点是文件的变更该如何监听(watch),我们当时的实现非常简单:

  • 单独起个线程,定时去获取文件的最后更新时间戳(毫秒级)
  • 记录每个文件的最后更新时间戳,根据这个时间戳是否变化来判断文件是否有变更

从上述简单的描述,我们能看出这样实现有一些缺点:

  • 无法实时感知文件的变更,感知误差在于轮询文件最后更新时间的间隔
  • 精确到毫秒级,如果同一毫秒内发生2次变更,且轮询时刚好落在这2次变更的中间时,后一次变更将无法感知,但这概率很小

还好,上述两个缺点几乎没有什么大的影响。

但后来还是发生了一次比较严重的线上故障,这是为什么呢?因为一个JDK的BUG,这里直接贴出罪魁祸首: 太极限了,JDK的这个BUG都能被我踩到

BUG详见:https://bugs.java.com/bugdatabase/view_bug.do?bug_id=8177809

在某些JDK版本下,获取文件的最后更新时间戳会丢失毫秒精度,总是返回整秒的时间戳,为了直观感受,写了个demo分别在jdk1.8.0_261jdk_11.0.6测试(均为MacOs):

  • jdk_1.8.0_261 太极限了,JDK的这个BUG都能被我踩到
  • jdk_11.0.6 太极限了,JDK的这个BUG都能被我踩到

如果是在这个BUG的影响下,只要同一秒内有2次变更,且读取文件最后时间戳位于这2次变更之间的时间,第2次变更就无法被程序感知了,同1秒这个概率比同一毫秒大的多的多,所以当然就被触发了,导致了一次线上故障。

这就好比之前是沧海一粟,现在变成了大海里摸到某条鱼的概率。这也能被我们碰到,真是有点极限~

WatchService—JDK内置的文件变更监听

当了解到之前的实现存在BUG后,我就去搜了一下Java下如何监听文件变更,果然被我找到了WatchService太极限了,JDK的这个BUG都能被我踩到

说是WatchService可以监听一个目录,对目录下的文件新增、变更、删除进行监听。于是我很快就写了个demo进行测试:

public static void watchDir(String dir) {
    Path path = Paths.get(dir);
    try (WatchService watchService = FileSystems.getDefault().newWatchService()) {
        path.register(watchService, StandardWatchEventKinds.ENTRY_CREATE, StandardWatchEventKinds.ENTRY_MODIFY, StandardWatchEventKinds.ENTRY_DELETE, StandardWatchEventKinds.OVERFLOW);
        while (true) {
            WatchKey key = watchService.take();
            for (WatchEvent<?> watchEvent : key.pollEvents()) {
                if (watchEvent.kind() == StandardWatchEventKinds.ENTRY_CREATE) {
                    System.out.println("create..." + System.currentTimeMillis());
                } else if (watchEvent.kind() == StandardWatchEventKinds.ENTRY_MODIFY) {
                    System.out.println("modify..." + System.currentTimeMillis());
                } else if (watchEvent.kind() == StandardWatchEventKinds.ENTRY_DELETE) {
                    System.out.println("delete..." + System.currentTimeMillis());
                } else if (watchEvent.kind() == StandardWatchEventKinds.OVERFLOW) {
                    System.out.println("overflow..." + System.currentTimeMillis());
                }
            }
            if (!key.reset()) {
                System.out.println("reset false");
                return;
            }
        }
    } catch (Exception e) {
        e.printStackTrace();
    }
}

太极限了,JDK的这个BUG都能被我踩到

先对/tmp/file_test目录进行监听,然后每隔5毫秒往文件写数据,理论上来说,应该能收到3次事件,但实际上很奇怪,仔细看接收到modify事件的时间大概是第一次文件修改后的9.5s左右,很奇怪,先记着,我们读一下WatchService源码

>>> 1652076266609 - 1652076257097
9512

WatchService原理

WatchService watchService = FileSystems.getDefault().newWatchService()

通过debug发现,这里的watchService实际上是PollingWatchService的实例,直接看PollingWatchService的实现: 太极限了,JDK的这个BUG都能被我踩到

PollingWatchService上来就起了个线程,这让我隐隐不安。再找一下这个scheduledExecutor在哪里用到: 太极限了,JDK的这个BUG都能被我踩到

每隔一段时间(默认为10s)去poll下,这个poll干了什么?代码太长,我截出关键部分: 太极限了,JDK的这个BUG都能被我踩到

果然,和我们的实现类似,也是去读文件的最后更新时间,根据时间的变化来发出变更事件。

换句话说,在某些JDK版本下,他也是有BUG的! 太极限了,JDK的这个BUG都能被我踩到

这也就解释了上文提到的事件监听为什么是在第一个9.5s之后才发出,因为监听注册后,sleep了500ms后修改文件,10s轮询,刚好9.5s后拿到第一轮事件。

inotify—Linux内核提供的文件监听机制

至此,我想起了linux上的tail命令,tail 是在文件有变更的情况下输出文件的末尾,理论上也是监听了文件变更,这块刚好在很久之前听过一个技术大佬分享如何自己实现tail命令,用到的底层技术就是inotify 太极限了,JDK的这个BUG都能被我踩到

简单来说,inotify是linux内核提供的一种监控文件变更事件的系统调用。如果基于此来实现,不就可以规避JDK的BUG了吗?

但奇怪的是为什么Java没有用这个来实现呢?于是我又搜了搜,发现谷歌似乎有一个库,但被删了,看不到代码: 太极限了,JDK的这个BUG都能被我踩到

github上又搜到一个:https://github.com/sunmingshi/Jinotify 太极限了,JDK的这个BUG都能被我踩到

看起来是一个native的实现,需要自己编译.so文件,这样就比较蛋疼了。

记得上次这么蛋疼还是在折腾Java的unix domain socket,也是找到了一个google的库,测试没问题,放到线上就崩了不得不说google还是厉害,JDK提供不了的库,我们来提供

太极限了,JDK的这个BUG都能被我踩到

于是我带着这个疑问去问了一个搞JVM开发的朋友,结果他告诉我,Java也可以使用inotify!

太极限了,JDK的这个BUG都能被我踩到

瞬间斗志来了,难道是我测试的姿势不对?

我又去翻了一遍Java文档,发现在角落隐藏了这么一段话: 太极限了,JDK的这个BUG都能被我踩到

也就是说,不同的平台下会使用不同的实现,PollingWatchService是在系统不支持inotify的情况下使用的兜底策略。

于是将watchService的类型打印出来,在Mac上打印为:

class sun.nio.fs.PollingWatchService

在Linux上是:

class sun.nio.fs.LinuxWatchService

LinuxWatchService在Mac上是找不到这个类,我猜测应该是Mac版的JDK压根没把这块代码打包进来。

原来我本地测试都走了兜底策略,看来是测了个寂寞。 太极限了,JDK的这个BUG都能被我踩到

于是我写了个demo再测试一把:

public static void main(String[] args) throws Exception {
    Thread thread = new Thread(() -> watchDir("/tmp/file_test"));
    thread.setDaemon(false);
    thread.start();

    Thread.sleep(500L);

    for (int i = 0; i < 3; i++) {
        String path = "/tmp/file_test/test";
        FileWriter fileWriter = new FileWriter(path);
        fileWriter.write(i);
        fileWriter.close();
        File file = new File(path);
        System.out.println(file.lastModified());
        Thread.sleep(5);
    }
}
  • 本地Mac 太极限了,JDK的这个BUG都能被我踩到

  • Linux 太极限了,JDK的这个BUG都能被我踩到

可以看出,Linux上能收到的事件比本地多的多,而且接收事件的时间明显实时多了。

为了更加准确的验证是inotify,用strace抓一下系统调用,由于JVM fork出的子进程较多,所以要加-f命令,输出太多可以存入文件再分析:

strace -f -o s.txt java FileTime

太极限了,JDK的这个BUG都能被我踩到

果然是用到了inotify系统调用的,再次验证了我们的猜想。

故障是如何修复的?

再次回到开头的故障,我们是如何修复的呢?由于下发的文件和读取文件的程序都是我们可控的,所以我们绕过了这个BUG,给每个文件写一个version,可以用文件内容md5值作为version,写入一个特殊文件,读取时先读version,当version变化时再重新载入文件。

可能你要问了,为什么不用WatchService呢?

我也问了负责人,据说inotify在docker上运行的不是很好,经常会丢失事件,不是Java的问题,所有语言都存在这个问题,所以一直没有使用。不过这块找不到相关的资料,也无法证明,所以暂时搁置。

最后说几句

有些BUG,不踩过就很难避免,代码只要存在BUG的可能性,就一定会暴露出来,只是时间问题。

我们要在技术上深入探究,小心求证,但产品上不必执着,可另辟蹊径。

另外解决不了的问题时可以找这个领域的资深人士,所以平时没事认识几个大牛很有必要。我们今天的分析到此结束,我是小楼,下期再见~


搜索关注微信公众号"捉虫大师",后端技术分享,架构设计、性能优化、源码阅读、问题排查、踩坑实践。 太极限了,JDK的这个BUG都能被我踩到

点赞
收藏
评论区
推荐文章
Python进阶者 Python进阶者
9个月前
只用3行Python代码,获取星期几?
大家好,我是皮皮。一、前言前几天在Python最强王者交流群分享了一个只用3行Python代码,获取星期几的问题,这里拿出来给大家分享下,一起学习下。看到这个代码,我当时的第一反应是,这个人基础学的还是可以的,input用到位了。对于初学者来说,能写成这样,也是不错的了,起码功能确实是实现了,虽然说起来确实有点说不上来的味道,应缺斯汀。二、解决过程有一说
技术小男生 技术小男生
4个月前
linux环境jdk环境变量配置
1:编辑系统配置文件vi/etc/profile2:按字母键i进入编辑模式,在最底部添加内容:JAVAHOME/opt/jdk1.8.0152CLASSPATH.:$JAVAHOME/lib/dt.jar:$JAVAHOME/lib/tools.jarPATH$JAVAHOME/bin:$PATH3:生效配置
光头强的博客 光头强的博客
4个月前
Java面向对象试题
1、请创建一个Animal动物类,要求有方法eat()方法,方法输出一条语句“吃东西”。创建一个接口A,接口里有一个抽象方法fly()。创建一个Bird类继承Animal类并实现接口A里的方法输出一条有语句“鸟儿飞翔”,重写eat()方法输出一条语句“鸟儿吃虫”。在Test类中向上转型创建b对象,调用eat方法。然后向下转型调用eat()方
捉虫大师 捉虫大师
11个月前
大厂偏爱的Agent技术究竟是个啥
搜索关注微信公众号"捉虫大师",后端技术分享,架构设计、性能优化、源码阅读、问题排查、踩坑实践。hello大家好,我是小楼,今天给大家分享一个关于Agent技术的话题,也是后端启示录的第3篇文章。通过本文你可以了解到如下内容:什么是Agent技术为了解释什么是Agent技术,我在网上搜了一圈,但没有找到想要的结果。反倒是搜到了不少JavaAgent技术,
九路 九路
1年前
HTTPS 原理详解
摘要:本文尝试一步步还原HTTPS的设计过程,以理解为什么HTTPS最终会是这副模样。但是这并不代表HTTPS的真实设计过程。在阅读本文时,你可以尝试放下已有的对HTTPS的理解,这样更利于“还原”过程。我们先不了聊HTTP,HTTPS,我们先从一个聊天软件说起,我们要实现A能发一个hello消息给B:如果我们要实现这个聊天软件,本文只考虑安全性问题,要实现
Easter79 Easter79
1年前
SpringCloud学习(3)——Eureka服务注册中心及服务发现
Eureka概述Eureka是Netflix的一个子模块,也是核心模块之一。Eureka是一个基于REST的服务,用于定位服务,以实现云端中间层服务发现和故障转移。服务注册与发现对于微服务框架来说非常重要,有了服务注册和发现,只需要使用服务的标识符,就可以访问到服务,而不需要修改服务调用的配置文件了。功能类似于
捉虫大师 捉虫大师
5个月前
抓到Dubbo异步调用的小BUG,再送你一个贡献开源代码的机会
hello,大家好呀,我是小楼。最近一个技术群有同学at我,问我是否熟悉Dubbo,这我熟啊他说遇到了一个Dubbo异步调用的问题,怀疑是个BUG,提到BUG我可就不困了,说不定可以水,哦不...写一篇文章。问题复现遇到问题,尤其不是自己遇到的,必须要复现出来才好排查,截一个当时的聊天记录:他的问题原话是:今天发现一个问题有一个dubbo接口返回类型是
捉虫大师 捉虫大师
7个月前
这不会又是一个Go的BUG吧?
hello,大家好呀,我是小楼。最近我又双叒叕写了个BUG,一个线上服务死锁了,不过幸亏是个新服务,没有什么大影响。出问题的是Go的读写锁,如果你是写Java的,不必划走,更要看看本文,本文的重点在于Java和Go的读写锁对比,甚至看完后你会有一个隐隐的感觉:Go的读写锁是不是有BUG?故障回放背景简单抽象一下:一个server服务(Go语言实现),提供了
捉虫大师 捉虫大师
9个月前
踩了个DNS解析的坑,但我还是没想通
hello大家好,我是小楼。最近踩了个DNS解析的小坑,虽然问题解决了,但排查过程比较曲折,最后还是有一点没有想通,整个过程分享给大家。背景最近负责的服务要置换机器。置换机器可能很多小伙伴不知道是干啥,因为大家平时接触不到,我简单解释一下什么是机器置换以及为什么需要机器置换。机器置换通俗地讲就是更换机器,把服务从一台机器迁移到另一台上去。为什么要机器置换呢
捉虫大师 捉虫大师
7个月前
对不起,我错了,这代码不好写
hello,大家好呀,我是小楼。前几天不是写了这篇文章嘛。文章介绍了Sentinl的自适应缓存时间戳算法,从原理到实现都手把手解读了,而且还发现SentinelGo还未实现这个自适应算法,于是我就觉得,这简单啊,把Java代码翻译成Go不就可以混个PR?甚至在文章初稿中把这个描述为:「有手就可以」,感觉不太妥当,后来被我删掉了。过了几天,我想去看看有没有人看