<kbd id="afajh"><form id="afajh"></form></kbd>
<strong id="afajh"><dl id="afajh"></dl></strong>
    <del id="afajh"><form id="afajh"></form></del>
        1. <th id="afajh"><progress id="afajh"></progress></th>
          <b id="afajh"><abbr id="afajh"></abbr></b>
          <th id="afajh"><progress id="afajh"></progress></th>

          驚!發(fā)現(xiàn)了 JDK 的一個 bug

          共 4045字,需瀏覽 9分鐘

           ·

          2022-05-19 21:58

          hello,大家好呀,我是二哥呀。

          之前遇到個文件監(jiān)聽變更的問題,剛好這昨天晚上有空研究了一番,整理出來分享給大家。

          從一次故障說起

          我們還是從故障說起,這樣更加貼近實際,也能讓大家更快速理解背景。

          有一個下發(fā)配置的服務(wù),這個配置服務(wù)的實現(xiàn)有點特殊,服務(wù)端下發(fā)配置到各個服務(wù)的本地文件,當然中間經(jīng)過了一個 agent,如果沒有 agent 也就無法寫本地文件,然后由 client 端的程序監(jiān)聽這個配置文件,一旦文件有變更,就重新加載配置,畫個架構(gòu)圖大概是這樣:

          今天的重點是文件的變更該如何監(jiān)聽(watch),我們當時的實現(xiàn)非常簡單:

          • 單獨起個線程,定時去獲取文件的最后更新時間戳(毫秒級)
          • 記錄每個文件的最后更新時間戳,根據(jù)這個時間戳是否變化來判斷文件是否有變更

          從上述簡單的描述,我們能看出這樣實現(xiàn)有一些缺點:

          • 無法實時感知文件的變更,感知誤差在于輪詢文件最后更新時間的間隔
          • 精確到毫秒級,如果同一毫秒內(nèi)發(fā)生 2 次變更,且輪詢時剛好落在這 2 次變更的中間時,后一次變更將無法感知,但這概率很小

          還好,上述兩個缺點幾乎沒有什么大的影響。

          但后來還是發(fā)生了一次比較嚴重的線上故障,這是為什么呢?因為一個 JDK 的 BUG,這里直接貼出罪魁禍首:

          BUG 詳見:https://bugs.java.com/bugdatabase/view_bug.do?bug_id=8177809

          在某些 JDK 版本下,獲取文件的最后更新時間戳?xí)G失毫秒精度,總是返回整秒的時間戳,為了直觀感受,寫了個 demo 分別在jdk1.8.0_261jdk_11.0.6測試(均為 MacOs):

          • jdk_1.8.0_261
          • jdk_11.0.6

          如果是在這個 BUG 的影響下,只要同一秒內(nèi)有 2 次變更,且讀取文件最后時間戳位于這 2 次變更之間的時間,第 2 次變更就無法被程序感知了,同 1 秒這個概率比同一毫秒大的多的多,所以當然就被觸發(fā)了,導(dǎo)致了一次線上故障。

          這就好比之前是滄海一粟,現(xiàn)在變成了大海里摸到某條魚的概率。這也能被我們碰到,真是有點極限~

          WatchService—JDK 內(nèi)置的文件變更監(jiān)聽

          當了解到之前的實現(xiàn)存在 BUG 后,我就去搜了一下 Java 下如何監(jiān)聽文件變更,果然被我找到了WatchService。

          說是 WatchService 可以監(jiān)聽一個目錄,對目錄下的文件新增、變更、刪除進行監(jiān)聽。于是我很快就寫了個 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();
          ????}
          }

          先對/tmp/file_test目錄進行監(jiān)聽,然后每隔 5 毫秒往文件寫數(shù)據(jù),理論上來說,應(yīng)該能收到 3 次事件,但實際上很奇怪,仔細看接收到modify事件的時間大概是第一次文件修改后的9.5s左右,很奇怪,先記著,我們讀一下 WatchService 源碼

          >>>?1652076266609?-?1652076257097
          9512

          WatchService 原理

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

          通過 debug 發(fā)現(xiàn),這里的 watchService 實際上是 PollingWatchService 的實例,直接看 PollingWatchService 的實現(xiàn):

          PollingWatchService 上來就起了個線程,這讓我隱隱不安。再找一下這個 scheduledExecutor 在哪里用到:

          每隔一段時間(默認為 10s)去 poll 下,這個 poll 干了什么?代碼太長,我截出關(guān)鍵部分:

          果然,和我們的實現(xiàn)類似,也是去讀文件的最后更新時間,根據(jù)時間的變化來發(fā)出變更事件。

          換句話說,在某些 JDK 版本下,他也是有 BUG 的!

          這也就解釋了上文提到的事件監(jiān)聽為什么是在第一個 9.5s 之后才發(fā)出,因為監(jiān)聽注冊后,sleep 了 500ms 后修改文件,10s 輪詢,剛好 9.5s 后拿到第一輪事件。

          inotify—Linux 內(nèi)核提供的文件監(jiān)聽機制

          至此,我想起了 linux 上的tail命令,tail 是在文件有變更的情況下輸出文件的末尾,理論上也是監(jiān)聽了文件變更,這塊剛好在很久之前聽過一個技術(shù)大佬分享如何自己實現(xiàn)tail命令,用到的底層技術(shù)就是inotify

          簡單來說,inotify 是 linux 內(nèi)核提供的一種監(jiān)控文件變更事件的系統(tǒng)調(diào)用。如果基于此來實現(xiàn),不就可以規(guī)避 JDK 的 BUG 了嗎?

          但奇怪的是為什么 Java 沒有用這個來實現(xiàn)呢?于是我又搜了搜,發(fā)現(xiàn)谷歌似乎有一個庫,但被刪了,看不到代碼:

          github 上又搜到一個:https://github.com/sunmingshi/Jinotify

          看起來是一個 native 的實現(xiàn),需要自己編譯.so 文件,這樣就比較蛋疼了。

          記得上次這么蛋疼還是在折騰 Java 的 unix domain socket,也是找到了一個 google 的庫,測試沒問題,放到線上就崩了~不得不說 google 還是厲害,JDK 提供不了的庫,我們來提供~

          于是我?guī)е@個疑問去問了一個搞 JVM 開發(fā)的朋友,結(jié)果他告訴我,Java 也可以使用 inotify!

          瞬間斗志來了,難道是我測試的姿勢不對?

          我又去翻了一遍 Java 文檔,發(fā)現(xiàn)在角落隱藏了這么一段話:

          也就是說,不同的平臺下會使用不同的實現(xiàn),PollingWatchService 是在系統(tǒng)不支持 inotify 的情況下使用的兜底策略。

          于是將 watchService 的類型打印出來,在 Mac 上打印為:

          class?sun.nio.fs.PollingWatchService

          在 Linux 上是:

          class?sun.nio.fs.LinuxWatchService

          LinuxWatchService 在 Mac 上是找不到這個類,我猜測應(yīng)該是 Mac 版的 JDK 壓根沒把這塊代碼打包進來。

          原來我本地測試都走了兜底策略,看來是測了個寂寞。

          于是我寫了個 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

          • Linux

          可以看出,Linux 上能收到的事件比本地多的多,而且接收事件的時間明顯實時多了。

          為了更加準確的驗證是 inotify,用strace抓一下系統(tǒng)調(diào)用,由于 JVM fork 出的子進程較多,所以要加-f命令,輸出太多可以存入文件再分析:

          strace?-f?-o?s.txt?java?FileTime

          果然是用到了 inotify 系統(tǒng)調(diào)用的,再次驗證了我們的猜想。

          故障是如何修復(fù)的?

          再次回到開頭的故障,我們是如何修復(fù)的呢?由于下發(fā)的文件和讀取文件的程序都是我們可控的,所以我們繞過了這個 BUG,給每個文件寫一個version,可以用文件內(nèi)容 md5 值作為 version,寫入一個特殊文件,讀取時先讀 version,當 version 變化時再重新載入文件。

          可能你要問了,為什么不用 WatchService 呢?

          我也問了負責人,據(jù)說 inotify 在 docker 上運行的不是很好,經(jīng)常會丟失事件,不是 Java 的問題,所有語言都存在這個問題,所以一直沒有使用。不過這塊找不到相關(guān)的資料,也無法證明,所以暫時擱置。

          最后說幾句

          有些 BUG,不踩過就很難避免,代碼只要存在 BUG 的可能性,就一定會暴露出來,只是時間問題。

          我們要在技術(shù)上深入探究,小心求證,但產(chǎn)品上不必執(zhí)著,可另辟蹊徑。

          另外解決不了的問題時可以找這個領(lǐng)域的資深人士,所以平時沒事認識幾個大牛很有必要。我們今天的分析到此結(jié)束,我是二哥,下期再見~


          最后,如果你感興趣的話,歡迎加入 二哥的編程知識星球 (點擊了解詳情),和 160 多名 小伙伴一起交流學(xué)習(xí),這是一個 Java 學(xué)習(xí)指南 + 編程實戰(zhàn)的私密圈子,你可以向二哥提問、幫你制定學(xué)習(xí)計劃、跟著二哥一起做實戰(zhàn)項目。

          沒有什么使我停留——除了目的,縱然岸旁有玫瑰、有綠蔭、有寧靜的港灣,我是不系之舟。

          推薦閱讀

          瀏覽 30
          點贊
          評論
          收藏
          分享

          手機掃一掃分享

          分享
          舉報
          評論
          圖片
          表情
          推薦
          點贊
          評論
          收藏
          分享

          手機掃一掃分享

          分享
          舉報
          <kbd id="afajh"><form id="afajh"></form></kbd>
          <strong id="afajh"><dl id="afajh"></dl></strong>
            <del id="afajh"><form id="afajh"></form></del>
                1. <th id="afajh"><progress id="afajh"></progress></th>
                  <b id="afajh"><abbr id="afajh"></abbr></b>
                  <th id="afajh"><progress id="afajh"></progress></th>
                  看片亚洲啊啊啊啊啊啊啊啊 | 亚洲黄色电影精品 | 婷婷亚洲丁香色五月 | 国产精品无码在线看 | 操B的网站 |