「一切皆文件」是 UNIX 的基本設計哲學。文件按照層級關系組織為樹形目錄,構成了文件系統 的基本形態。用戶使用文件系統來保存數據時,不必關心數據底層的存儲方式,便可以按照約定的接口規范進行訪問。
概念篇
關於文件系統的接口規范,應用最為廣泛的莫過於 POSIX,源於 IEEE 委員會編寫的相關標准,其中有些章節是關於文件及目錄操作的。標准本身比較冗長晦澀,在此不作深入探討。我們可以參考 Quora 上的一個問答 “What does POSIX conformance/compliance mean in the distributed systems world?” ,對此概括的比較全面。
POSIX 兼容要求文件系統具備以下幾項特征:
- 層級化的目錄結構,支持任意深度
- 文件通過
open(O_CREAT)
,目錄通過mkdir
創建等等- 目錄可以通過
opendir/readdir
遍歷- 路徑/命名空間可以通過
rename
、link / unlink
、symlink / readlink
等修改- 數據通過
write
或writev
寫入,fsync
時要求持久化,通過read
或readv
讀取- 其他一些接口如
stat
,chmod / chown
等- 與某些流行的說法相悖,擴展屬性看起來並不是 POSIX 的一部分,參見The Open Group Base Specifications Issue 7, 2018 edition 里的函數列表
測試篇
一個文件系統是否真正滿足 POSIX 兼容性,我們可以通過測試工具來檢驗。比較流行的一個測試用例集是 pjdfstest,來源於 FreeBSD,也適用於 Linux 等系統。pjdfstest 的測試用例需要以 root 身份來運行,並且要求系統里安裝了 Perl 和 TAP::Harness(Perl 軟件包),測試過程如下:
cd /path/to/filesystem/under/test
sudo prove --recurse --verbose /path/to/pjdfstest/tests
我們選取了幾種雲環境中的共享文件系統進行測試,統計測試結果中的失敗用例如下:
因為 Amazon EFS 失敗的測試用例相比其他產品大了幾個數量級,為了方便比較,上圖的橫坐標使用了對數坐標。
我們還同時測試了 S3FS 和 Goofys,失敗的用例數均為數百項乃至上千項,其根本原因是這兩個項目並不是嚴格按照文件系統來設計的:
-
Goofys 可以將 S3 掛載為文件系統,但僅僅是 “POSIX-ish” 接口的 “Filey” 系統(這兩個描述來自於官方項目介紹,翻譯成中文即“似是而非”或“貌合神離”)。Goofys 在設計理念上為了性能而犧牲了 POSIX 兼容性,所支持的文件操作極大地受限於 S3 等對象存儲本身。測試結果也驗證了這一點。建議在生產使用之前全面評審應用的數據訪問方式,以免落入陷阱。
-
S3FS 盡管名為文件系統,但實際上更接近於用文件系統視圖管理 S3 bucket 中對象的一種方法。盡管 S3FS 支持了 POSIX 的一個較大子集,但只是將系統調用一一映射為對象存儲請求,並不支持常規文件系統的語義及一致性(例如目錄的原子重命名,獨占模式打開時的互斥,附加文件內容會導致重寫整個文件以及不支持硬連接等等)。這些缺陷導致 S3FS 並不能用於替代常規文件系統(即便不考慮性能問題),因為當應用訪問文件系統時,預期的行為應該是符合 POSIX 規范的,而 S3FS 遠遠不能滿足這一點。
分析篇
下面我們將測試的失敗用例進行分類統計,挑選幾類比較有代表性的來分析下會對應用造成何種限制。
總的來說,無論從數量還是類別來看,JuiceFS 的失敗用例都更少,有更好的兼容性。Amazon EFS 的失敗用例無論從總數及類別均大大超出其它幾種文件系統,無法放入同一圖表對比,后面將單獨分析。
JuiceFS
JuiceFS 在本次測試中通過了8811項用例中的絕大多數,僅在 utimensat 測試集上失敗了 3 項。對應日志如下:
…
/root/pjdfstest/tests/utimensat/08.t ........
not ok 5 - tried 'lstat pjdfstest_bfaee1fc7f2c1f80768e30f203f41627 atime_ns', expected 100000000, got 0
not ok 6 - tried 'lstat pjdfstest_bfaee1fc7f2c1f80768e30f203f41627 mtime_ns', expected 200000000, got 0
Failed 2/9 subtests
/root/pjdfstest/tests/utimensat/09.t ........
not ok 5 - tried 'lstat pjdfstest_7911595d91adcf915009f551ac48e1f2 mtime', expected 4294967296, got 0
這幾個測試用例出自utimensat/08.t和utimensat/09.t。其中 08.t 是測試亞秒級的文件訪問時間和修改時間精度,09.t 則是要求支持64位時間戳。
JuiceFS 目前只支持秒,時間戳保存為32位整數,故無法通過這三個測試(實際上本次測試涉及的所有文件系統都無法100%通過這個測試集)。如果您的應用場景要求秒以下的時間精度或者更大范圍,歡迎聯系我們商討解決方案。
GCP Filestore
除了和 JuiceFS 一樣在 utimesat 測試集上存在若干失敗結果之外,GCP Filestore 還在 unlink 測試集中失敗了 1 項。這一項在其他所有文件系統中也都是失敗的。
/root/pjdfstest/tests/unlink/14.t ...........
not ok 4 - tried 'open pjdfstest_b03f52249a0c653a3f382dfe1237caa1 O_RDONLY : unlink pjdfstest_b03f52249a0c653a3f382dfe1237caa1 : fstat 0 nlink', expected 0, got 1
該測試集(unlink/14.t)用於驗證一個文件在打開狀態下被刪除時的行為:
desc="An open file will not be immediately freed by unlink"
刪除文件的操作在系統層面實際對應於 unlink,即移除該文件名到對應 inode 的鏈接,對應 nlink 的值減 1,這個測試用例就是要驗證這一點。
# A deleted file's link count should be 0
expect 0 open ${n0} O_RDONLY : unlink ${n0} : fstat 0 nlink
文件內容只有在鏈接數(nlink)減少至 0 並且沒有打開的文件描述符(fd)指向該文件時才會被真正刪除。如果 nlink 沒有被正確更新,可能會導致本該刪除的文件仍然殘留在系統里。
CFS
CFS 相比 Google Filestore,還未能通過 open 和 symlink 的幾項測試。
open 失敗用例
選取其中一部分失敗日志如下:
/root/pjdfstest/tests/open/07.t .............
not ok 5 - tried '-u 65534 -g 65534 open pjdfstest_f24a42815d59c16a4bde54e6559d0390 O_RDONLY,O_TRUNC', expected EACCES, got 0
not ok 7 - tried '-u 65533 -g 65534 open pjdfstest_f24a42815d59c16a4bde54e6559d0390 O_RDONLY,O_TRUNC', expected EACCES, got 0
not ok 9 - tried '-u 65533 -g 65533 open pjdfstest_f24a42815d59c16a4bde54e6559d0390 O_RDONLY,O_TRUNC', expected EACCES, got 0
Failed 3/23 subtests
此測試集 open/07.t 用於驗證不具備寫權限時,應該對 O_TRUNC 模式返回 EACCES 錯誤這一行為。
desc="open returns EACCES when O_TRUNC is specified and write permission is denied"
上面這三個失敗日志需要結合測試代碼來分析,分別對應 owner,group 和 other 三種情況。不失一般性,我們僅就 owner 情況進行分析 :
expect 0 -u 65534 -g 65534 chmod ${n1} 0477
expect EACCES -u 65534 -g 65534 open ${n1} O_RDONLY,O_TRUNC
首先設置文件 owner 權限為 4,即 r--
只讀,然后嘗試以 O_RDONLY,O_TRUNC 模式打開文件,預期應該返回 EACCES,實際返回了 0。
根據 The Single UNIX ® Specification, Version 2 中對 O_TRUNC 的說明
O_TRUNC
If the file exists and is a regular file, and the file is successfully opened O_RDWR or O_WRONLY, its length is truncated to 0 and the mode and owner are unchanged. It will have no effect on FIFO special files or terminal device files. Its effect on other file types is implementation-dependent. The result of using O_TRUNC with O_RDONLY is undefined.
O_TRUNC 與 O_RDONLY 組合使用的結果是未知的,而且此用例的被測文件本身就是空文件,O_TRUNC 不會產生任何效果。
symlink 失敗用例
對應測試日志如下:
/root/pjdfstest/tests/symlink/03.t ..........
not ok 1 - tried 'symlink 7ea12171c487d234bef89d9d77ac8dc2929ea8ce264150140f02a77fc6dcad7c3b2b36b5ed19666f8b57ad861861c69cb63a7b23bcc58ad68e132a94c0939d5/.../... pjdfstest_57517a47d0388e0c84fa1915bf11fe4a', expected 0, got EINVAL
not ok 2 - tried 'unlink pjdfstest_57517a47d0388e0c84fa1915bf11fe4a', expected 0, got ENOENT
Failed 2/6 subtests
該測試集(symlink/03.t)用於測試路徑超出 PATH_MAX 長度時 symblink 的行為
desc="symlink returns ENAMETOOLONG if an entire length of either path name exceeded {PATH_MAX} characters"
失敗的用例對應代碼如下:
n0=`namegen`
nx=`dirgen_max`
nxx="${nx}x"
mkdir -p "${nx%/*}"
expect 0 symlink ${nx} ${n0}
expect 0 unlink ${n0}
該測試用例是要創建長度為 PATH_MAX (包括結尾的0在內)的符號鏈接,通不過表明無法在 騰訊雲 NAS 上創建長度為 PATH_MAX 的符號鏈接。
阿里雲 NAS
相比騰訊雲 NAS,阿里雲 NAS 在 symlink 上表現正常,但未能通過 chmod 和 rename 上的幾項測試用例。
chmod 失敗用例
在這個測試集中,阿里雲 NAS 失敗了以下幾個項目
/root/pjdfstest/tests/chmod/12.t ............
not ok 3 - tried '-u 65534 -g 65534 open pjdfstest_db85e6a66130518db172a8b6ce6d53da O_WRONLY : write 0 x : fstat 0 mode', expected 0777, got 04777
not ok 4 - tried 'stat pjdfstest_db85e6a66130518db172a8b6ce6d53da mode', expected 0777, got 04777
not ok 7 - tried '-u 65534 -g 65534 open pjdfstest_db85e6a66130518db172a8b6ce6d53da O_RDWR : write 0 x : fstat 0 mode', expected 0777, got 02777
not ok 8 - tried 'stat pjdfstest_db85e6a66130518db172a8b6ce6d53da mode', expected 0777, got 02777
not ok 11 - tried '-u 65534 -g 65534 open pjdfstest_db85e6a66130518db172a8b6ce6d53da O_RDWR : write 0 x : fstat 0 mode', expected 0777, got 06777
not ok 12 - tried 'stat pjdfstest_db85e6a66130518db172a8b6ce6d53da mode', expected 0777, got 06777
Failed 6/14 subtests
該測試集(chmod/12.t)用於測試 SUID/SGID 位的行為
desc="verify SUID/SGID bit behaviour"
我們選取其中的第11和12個測試用例來詳細解釋一下,同時覆蓋了這兩個權限位
# Check whether writing to the file by non-owner clears the SUID+SGID.
expect 0 create ${n0} 06777
expect 0777 -u 65534 -g 65534 open ${n0} O_RDWR : write 0 x : fstat 0 mode
expect 0777 stat ${n0} mode
expect 0 unlink ${n0}
此處,我們先以 06777 的權限創建目標文件,然后修改文件內容,檢查 SUID 和 SGID 是否被正確清除。文件權限里的 777 大家會比較熟悉,分別對應 owner,group和 other 的 rwx,即可讀、可寫、可執行。最前面的 0 表示八進制數。
第二位 6 需要着重解釋下,這個八位元組(octet)代表特殊權限位,其中前兩位分別對應 setuid/setgid(或稱 SUID/SGID),可以應用於可執行文件及公共目錄。該權限位被設置時,任何用戶都會以 owner (或 group)身份來運行該文件。這個特殊的屬性允許用戶獲取通常只對 owner 開放的文件和目錄訪問權限。例如 passwd 命令就設置了 setuid 權限,這允許普通用戶修改密碼,因為保存密碼的文件是只允許 root 訪問的,用戶不可直接修改。
setuid/setgid 設計的出發點是提供一種方法,讓用戶以限定的方式(指定可執行文件)訪問受限文件(非當前用戶所有)。因此,當文件被非 owner 修改時應自動清除此權限位,以避免用戶通過這個途徑獲取其他權限。
從測試結果中我們可以看到在阿里雲 NAS 中,文件被非 owner 修改時,setuid/setgid 均未被清除,這樣實際上用戶可以通過修改文件內容以該 owner 身份進行任意操作,這將會是個安全隱患。
rename 失敗用例
阿里雲 NAS 在這個測試集上失敗數量較多,達到了 24 項,全部出現在 rename/09.t 中:
desc="rename returns EACCES or EPERM if the directory containing 'from' is marked sticky, and neither the containing directory nor 'from' are owned by the effective user ID"
這個測試集用於檢驗 sticky 位被設置時 rename 的行為:當包含源對象的目錄設置了 sticky 權限位的時候,並且源對象和包含目錄的 owner 都與有效用戶ID(effective user ID)不同時,rename 應該返回 EACCES 或 EPERM。(這樣的復雜邏輯令人聯想到三國殺的武將技能設定……)。
sticky 位的典型應用是 /tmp 目錄,允許所有人創建內容,但是只有 owner 才能刪除文件。FTP 里面的公共上傳目錄通常也是這種設置。
幾個失敗的測試用例表明阿里雲 NAS 對 sticky 位的支持還不夠完善,非 owner 的 rename 操作沒有被拒絕,並且產生了實際的效果——源文件被重命名。這種行為越過了文件系統的訪問控制,對用戶文件的安全性造成了威脅。
Amazon EFS 中的失敗用例
Amazon Elastic File System (EFS) 在 pjdfstest 測試中的不僅失敗比例極高(8811個測試用例失敗了1533個),而且幾乎覆蓋了所有類別,這比較令人意外。
EFS 支持以 NFS 方式掛載,但對 NFS 特性的支持並不完整。比如EFS 不支持塊設備和字符設備,這直接導致了 pjdfstest 中大量測試用例的失敗。排除這兩類文件之后,仍然有上百項不同類別的失敗,所以在復雜場景中應用 EFS 必須慎之又慎。
總結篇
通過上面的對比分析,JuiceFS 在兼容性方面表現最好,像大多數網絡文件系統一樣,為了性能犧牲了秒以下的時間精度和范圍(1970 - 2106 年)。Google Filestore 和騰訊雲 CFS 次之,有幾類未能通過。而阿里雲 NAS 和 Amazon EFS 的兼容性最差,有大量的兼容性測試通不過,其中包括有嚴重安全隱患的若干個測試用例,使用前建議做安全性評估。
JuiceFS 一直非常重視對 POSIX 標准的高度兼容,我們把 pjdfstest 等兼容性測試工具同其他隨機和並發測試工具(比如 fsracer、fstool 等)一起作為集成測試工具,在持續完善功能、提高性能的同時,盡力保持最大程度的 POSIX 兼容性,避免用戶在使用過程中落入各種陷阱,從而更加專注於自身業務的發展。
如有幫助的話歡迎關注我們項目 Juicedata/JuiceFS 喲! (0ᴗ0✿)