使用 inotify 踩過的坑

Inotify 用于監(jiān)聽文件系統(tǒng)的變更,我們有個場景需要監(jiān)聽一個根目錄下的所有文件,包括不同深度子目錄下的文件,如果文件發(fā)生變化,增量讀取新增的內(nèi)容。

之前我們使用輪詢的模式,周期性遞歸遍歷根目錄,對比文件屬性信息選擇要讀取的文件,這種方式有一個問題就是周期很難選擇:

  1. 如果選擇較小的周期,讀文件的磁盤 I/O 會相對均勻,但是頻繁的遞歸遍歷會造成較高的 CPU 占用
  2. 如果選擇較大的周期,遞歸遍歷占用的資源將會下降,但是周期變大造成增量數(shù)據(jù)變大,結(jié)果每次讀取將會造成瞬間較高的磁盤 I/O

所以我們考慮用系統(tǒng)級別的 inotify 替換掉輪詢的邏輯,結(jié)果踩了不少坑。。。

1. JDK 封裝的問題 —— poller

jdk 將 inotify 的功能封裝為 WatchService,使用起來非常簡單,sample 中還提供了一個遞歸注冊目錄的例子 WatchDir.java。

由于 inotify 只支持 1.監(jiān)聽文件,2.監(jiān)聽目錄,以及目錄下的目錄和文件,不能監(jiān)聽子目錄下的變更,所以需要遞歸注冊目錄。

基于官方的 sample 我們很快就用上了 inotify,效果很不錯,不僅 CPU 占用下降了,而且文件讀取特別均勻。

直到有一天 CPU 使用率開始飚高了,通過 top 和 jstack 發(fā)現(xiàn)占用 CPU 最多的線程不是讀文件的線程,而是 LinuxWatchService 中的 poller 線程。

poller 線程的作用如上圖所示:

  1. poll requestList,處理業(yè)務(wù)代碼中的目錄注冊、取消和關(guān)閉等請求;
  2. poll inotify 文件描述符獲取 inoitfy_event, 然后按照 WatchKey (也就是 watch descriptor)放到不同的隊列,隊列的長度默認(rèn)為 512,如果隊列滿了,那么清空隊列并且放入 overflow 事件。

由于我們監(jiān)聽的目錄比較多,當(dāng)大量文件頻繁修改的時候是會產(chǎn)生大量的事件,而 poller 線程會盡最大能力讀取這些事件,所以造成了很高的 CPU 占用。

然后這是一種無用功,因?yàn)?1.事件數(shù)量超出 WatchEvent 隊列長度,上層業(yè)務(wù)邏輯也就取不到事件;2. inotify 的文件隊列也有簡單的事件去重邏輯,沒必要特別快地讀取事件然后再去重。

實(shí)際上 poller 的速度應(yīng)該受制于下游的處理能力,如果下游已經(jīng)不能處理,那么暫時放在中轉(zhuǎn)隊列也是意義不大的,我們只需要依賴 inotify 的文件隊列即可。

順便提一下 inotify 相關(guān)的幾個系統(tǒng)參數(shù):

  1. /proc/sys/fs/inotify/max_user_instances 初始化 ifd 的數(shù)量限制
  2. /proc/sys/fs/inotify/max_queued_events ifd 文件隊列長度限制
  3. /proc/sys/fs/inotify/max_user_watches 注冊監(jiān)聽目錄的數(shù)量限制
  4. 既然是文件描述符,當(dāng)然也受 /etc/security/limits.conf 和 /proc/sys/fs/file-max 限制

所以我們通過 JNI 調(diào)用重新實(shí)現(xiàn)了 WatchService 的邏輯,去掉了 poller 線程,由業(yè)務(wù)線程控制 poll 的頻率。

2. 莫名其妙的 MODIFY 事件 —— deleted

然后一切又變正常了,只是偶爾有幾條莫名其妙的 MODIFY 事件。

讀到 MODIFY 事件,首先需要讀取文件屬性,然后走不同的邏輯,但是此刻卻拋出 NoSuchFileException。

這個問題一開始也毫無頭緒,甚至懷疑是不是文件瞬間被刪除了。。。

直到有一天想起了 lsof 這個命令,驗(yàn)證了一下:

REG                8,6        16    6161077 /xxx/xxx/xxx/xxx (deleted)

果然是文件被刪除了,但是打開的文件描述符沒有關(guān)閉,而且還偶爾在寫數(shù)據(jù)。

3. 有目錄脫離監(jiān)聽 —— overflow

然后一切又變正常了,文件不存在的 MODIFY 事件歸咎于業(yè)務(wù)方程序的問題。

但是又遇到了新的問題,偶爾發(fā)現(xiàn)存在沒有讀取的文件,而且是整個子目錄下的所有文件都沒有讀。

由于我們監(jiān)聽的目錄比較多,有些機(jī)器上有約五千個監(jiān)聽目錄,所以偶爾會發(fā)生事件 overflow,我們猜測是目錄創(chuàng)建的事件被 overflow 了,所以沒有監(jiān)聽到這個目錄。

針對這個問題,我們添加了一個周期性重新遞歸注冊的機(jī)制。

由于 inotify 是基于 inode 的,所以重復(fù)注冊目錄獲得的 wd (watch descriptor) 是一樣的,但是如果目錄被移除然后重建了,那么就是一個新的 wd。

此處由于一個 bug 使得問題更嚴(yán)重,由于需要構(gòu)建文件的絕對路徑,我們采用 guava 的 BiMap 存儲 wd 與 path 之間的映射,完成 inotify_add_watch 之后需要更新 wdToPath

調(diào)用 BiMap 的 put 方法,如果存在遺留的 path,那么是會拋出 IllegalArgumentException 的,所以首先需要處理這種情況,然后調(diào)用 forcePut 覆蓋舊的映射。

4. 隱藏最深的 mv

移動監(jiān)聽目錄這個場景,由于寫代碼的時候沒有考慮,第一次發(fā)現(xiàn)這個問題著實(shí)被嚇了一下。

有些業(yè)務(wù)方的重啟腳本有保存歷史日志的邏輯,也就是 mv 舊的日志根目錄,然后創(chuàng)建一個新的日志根目錄。

由于 inotify 是基于 inode 的,所以 mv 后的目錄還在監(jiān)聽中,并且 wd 沒有變化,所以目錄下文件的改動還是會觸發(fā)事件。觸發(fā)事件不可怕,關(guān)鍵是通過 wdToPath 映射還原的絕對路徑已經(jīng)不對了。

在我們的使用場景中,將 mv 作為刪除處理即可,所以注冊目錄時默認(rèn)加上 IN_MOVE_SELF 事件監(jiān)聽,如果收到該事件,那么 forceUnregister 這個 wd,如果事件 overflow 了,那么還是依賴周期性重新注冊的機(jī)制。

參考

本文只是粗略記一下遇過的問題,inotify 的詳細(xì)介紹可以參考下述文章。

  1. Filesystem notification series by Michael Kerrisk
  2. Monitor Linux file system events with inotify


如果覺得我的文章對您有用,請隨意打賞。

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時請結(jié)合常識與多方信息審慎甄別。
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡書系信息發(fā)布平臺,僅提供信息存儲服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

  • 1.ios高性能編程 (1).內(nèi)層 最小的內(nèi)層平均值和峰值(2).耗電量 高效的算法和數(shù)據(jù)結(jié)構(gòu)(3).初始化時...
    歐辰_OSR閱讀 30,227評論 8 265
  • Swift1> Swift和OC的區(qū)別1.1> Swift沒有地址/指針的概念1.2> 泛型1.3> 類型嚴(yán)謹(jǐn) 對...
    cosWriter閱讀 11,641評論 1 32
  • 高度臨在閱讀 129評論 0 0
  • 100天可以養(yǎng)成一種習(xí)慣,100天可以有驚人的蛻變,100天的時間,100天的陪伴,100天的成長...........
    嬌之語閱讀 241評論 0 0
  • 文/靜仁 我突然想攤開一張紅白相間的信紙 寫一封信給你 像小時候母親要我寫信給她在外打工的其他兒女 帶上橡皮和字典...
    趙靜仁閱讀 439評論 0 0

友情鏈接更多精彩內(nèi)容