一般來說,從檔案系統中獲得檔案變化資訊,呼叫作業系統提供的 API 即可。Windows 作業系統上有個名為 ReadDirectoryChangesW 的 API 介面,只要監視一個目錄路徑就可以獲得包括其子目錄下的所有檔案變化資訊,簡單高效;介面的支援度也很廣,現有主流的 Windows 作業系統都支援,往前還可以追溯到 Windows 2000。對碼農來說,能提供穩定有效且好用的 API 的系統就是好系統。而本文將討論 iGuard 網頁防篡改系統在 Linux 上獲取檔案變化資訊的方法及從 NFS 網路檔案系統中獲取檔案變化時遇到的困難和心得。
在 Linux 系統上獲取檔案變更資訊,就沒有這樣的好運了,想要一個像 Windows 上一樣提供 ReadDirectoryChangesW 功能的 API,似乎是一種奢望。Linux 核心版本 2.4.0 (2001) 中引入了一個叫 dnotify 的目錄檢測機制,不怎麼好用;核心 2.6.13 (2005) 引入了新方法 inotfiy,但它與 ReadDirectoryChangesW 相比還是差一大截,一次呼叫只能監視當前目錄下的檔案變更,子目錄裡的變更則是無法感知的。如果要獲取整個目錄下的所有檔案變化,應用程式需要遍歷整個目錄,並把所有的目錄監視起來。通過 inotify 介面獲得一個目錄建立事件時,需要把這個新建的目錄及時新增到監視列表,才有可能獲得新目錄下的檔案變化。應用程式處理變化資訊較慢時,在把新建目錄新增到監視列表前,新目錄下的檔案事件是極有可能丟失的。對於一個巨型檔案系統來說,遍歷出所有的目錄也是件費事耗資源的任務。如此看來,inotify 機制並不完善,小規模檔案數量的場景尚能勝任,像集約化平臺,檔案規模太龐大,就難以滿足了。
隨著 Linux 系統的演化,獲取檔案變化資訊的手段也在發展,但一直不完善,inotify 的缺陷同樣表現在 NFS 系統上。NFS 系統是天存資訊的使用者中一種常見的應用場景,它共享海量檔案。我們的 iGuard 網頁防篡改系統在 NFS 上需要一種可靠的獲取檔案變化的手段,來保障安全業務的 7×24h 運轉。鑑於 Linux 系統公開的 API 似乎不能滿足我們的要求,只有另闢蹊徑。幸好 Linux 是開源的,沒有現成的就改一個出來。
我們的改造目標指向了 NFS 系統的服務模組 nfsd。在 Linux 核心原始碼樹下的檔案系統 fs 目錄中很容易找到 nfsd 模組的同名目錄。在 Linux 系統中,NFS 服務透過虛擬檔案系統 VFS 介面來訪問真實的檔案系統,檔案的新建、改寫、改名和刪除等動作是非常清晰的。我們很快就把這些檔案更改相關的事件傳遞出來併為我所用。
最初,我們的這種 nfsd 解決方案和 iGuard 網頁防篡改系統看起來是可以一起工作的,在使用者生產環境下可以穩定地跑上幾周幾個月,基本上沒有問題。隨著集約化平臺的興起,大量網站集中到統一的管理平臺下進行內容編輯和運維,這樣的單一管理平臺釋出檔案的規模每天可達百萬級別。我們的 iGuard 系統在超大規模的檔案釋出量下也暴露出一些問題,檔案同步任務阻塞、滯後或者遺漏等;這些問題以前可能沒有出現或缺少關注,隨著規模變大,這些問題現今被放大了。這些問題中,最難排解的就是檔案遺漏,明明磁碟檔案已經更新,但系統就是沒有把檔案內容同步到遠端。後來追查發現,在某些情況下,我們無法獲得 NFS 服務所寫檔案物件的完整檔案路徑,進而無法輸出對應檔案的變更訊息。
在 Linux 檔案系統中,inode 和 dentry 是兩個重要的資料結構 。前者對應於磁碟檔案的後設資料 (型別、尺寸、許可權等,但不包括檔案路徑) 和檔案資料塊索引,每個 inode 都有一個編號,在檔案系統中是唯一的;後者是檔案系統執行過程中建立的記憶體物件,組合成目錄項快取記憶體 dcache,每個 dentry 對應檔案路徑上的一個節點並和一個 inode 相關聯,目錄樹由這些 dentry 組成,可以通過遍歷目錄樹來獲取檔案路徑,dentry 可以被視作某種快取資訊,讓檔案系統執行得更快更高效。
通過 NFS 對外提供檔案訪問的系統需要符合檔案系統可匯出規範,這個規範在 Linux 的核心文件 Making Filesystems Exportable 中有簡要說明,其中提到 NFS 通訊協議中使用檔案控制程式碼來標識檔案,而不是平時所想象的按檔案路徑來定位檔案。這個控制程式碼資訊跟符合可匯出規範的檔案系統相關,包含 inode 的編號、檔案系統標識等資訊。這裡可以看出,我們需要的檔案變化訊息是基於檔案路徑,而 NFS 操作檔案是基於這種檔案控制程式碼,這裡就存在從檔案控制程式碼到檔案路徑的轉譯過程。
在一般情況下,這個轉譯過程是正常的,每一個 NFS 檔案控制程式碼都可以在 dcache 中找到對應的檔案。文件 Making Filesystems Exportable 中還提到 dcache 構建中的 2 個注意事項,大致是:
- dcache 包含的物件有時候是沒有合適字首的節點 (可以理解為孤立的),該節點沒有與根節點相連。
- 新遍歷出來的節點可能是已存在於 dcache 的孤立節點,這種情況需要將孤立節點移動到合適的位置 (可以理解為孤立節點回歸到大目錄樹下)。
這就解釋了我們在 NFS 系統中遇到的問題原因——無法獲取變更檔案的完整路徑,因為它沒有和根節點相連。我們也能重現問題,在 NFS 服務和客戶端工作了一段時間後,重啟 NFS 伺服器,當 NFS 客戶端繼續讀寫曾經訪問過的檔案時,由於 NFS 伺服器上的 dcache 已經復位,客戶端請求過來的檔案控制程式碼是合法的,並在伺服器端形成一個沒有合適字首的節點,這樣的節點是無法解析出完整路徑的。dcache 畢竟是一個快取系統,不可能把磁碟上的目錄樹全部儲存到記憶體,當記憶體不夠用時,dcache 會釋放一部分資料並進行記憶體回收。
NFS 服務的這個問題看似無解,是 NFS 工作模式引發的。為了解決問題,我們嘗試採用一種看似笨拙但有效的方法,創造性地構建一張持久化的超大表來跟蹤所有的 NFS 檔案控制程式碼,記錄它們的檔案路徑資訊,通過這張大表轉譯出那些沒頭腦的節點。用磁碟空間來換取 NFS 檔案控制程式碼的路徑檢索。方法雖不完美,但我們盡力讓 iGauard 網頁防篡改系統執行更加完美。(徐品華 | 天存資訊)
Ref
- Filesystem notification series by Michael Kerrisk
- Making Filesystems Exportable