「一切皆文件」是 UNIX 的基本設(shè)計哲學(xué)。文件按照層級關(guān)系組織為樹形目錄,構(gòu)成了文件系統(tǒng) 的基本形態(tài)。用戶使用文件系統(tǒng)來保存數(shù)據(jù)時,不必關(guān)心數(shù)據(jù)底層的存儲方式,便可以按照約定的接口規(guī)范進(jìn)行訪問。
概念篇
關(guān)于文件系統(tǒng)的接口規(guī)范,應(yīng)用最為廣泛的莫過于 POSIX,源于 IEEE 委員會編寫的相關(guān)標(biāo)準(zhǔn),其中有些章節(jié)是關(guān)于文件及目錄操作的。標(biāo)準(zhǔn)本身比較冗長晦澀,在此不作深入探討。我們可以參考 Quora 上的一個問答 “What does POSIX conformance/compliance mean in the distributed systems world?” ,對此概括的比較全面。
POSIX 兼容要求文件系統(tǒng)具備以下幾項特征:
- 層級化的目錄結(jié)構(gòu),支持任意深度
- 文件通過
open(O_CREAT),目錄通過mkdir創(chuàng)建等等- 目錄可以通過
opendir/readdir遍歷- 路徑/命名空間可以通過
rename、link / unlink、symlink / readlink等修改- 數(shù)據(jù)通過
write或writev寫入,fsync時要求持久化,通過read或readv讀取- 其他一些接口如
stat,chmod / chown等- 與某些流行的說法相悖,擴(kuò)展屬性看起來并不是 POSIX 的一部分,參見The Open Group Base Specifications Issue 7, 2018 edition 里的函數(shù)列表
測試篇
一個文件系統(tǒng)是否真正滿足 POSIX 兼容性,我們可以通過測試工具來檢驗。比較流行的一個測試用例集是 pjdfstest,來源于 FreeBSD,也適用于 Linux 等系統(tǒng)。pjdfstest 的測試用例需要以 root 身份來運行,并且要求系統(tǒng)里安裝了 Perl 和 TAP::Harness(Perl 軟件包),測試過程如下:
cd /path/to/filesystem/under/test
sudo prove --recurse --verbose /path/to/pjdfstest/tests
我們選取了幾種云環(huán)境中的共享文件系統(tǒng)進(jìn)行測試,統(tǒng)計測試結(jié)果中的失敗用例如下:

因為 Amazon EFS 失敗的測試用例相比其他產(chǎn)品大了幾個數(shù)量級,為了方便比較,上圖的橫坐標(biāo)使用了對數(shù)坐標(biāo)。
我們還同時測試了 S3FS 和 Goofys,失敗的用例數(shù)均為數(shù)百項乃至上千項,其根本原因是這兩個項目并不是嚴(yán)格按照文件系統(tǒng)來設(shè)計的:
Goofys 可以將 S3 掛載為文件系統(tǒng),但僅僅是 “POSIX-ish” 接口的 “Filey” 系統(tǒng)(這兩個描述來自于官方項目介紹,翻譯成中文即“似是而非”或“貌合神離”)。Goofys 在設(shè)計理念上為了性能而犧牲了 POSIX 兼容性,所支持的文件操作極大地受限于 S3 等對象存儲本身。測試結(jié)果也驗證了這一點。建議在生產(chǎn)使用之前全面評審應(yīng)用的數(shù)據(jù)訪問方式,以免落入陷阱。
S3FS 盡管名為文件系統(tǒng),但實際上更接近于用文件系統(tǒng)視圖管理 S3 bucket 中對象的一種方法。盡管 S3FS 支持了 POSIX 的一個較大子集,但只是將系統(tǒng)調(diào)用一一映射為對象存儲請求,并不支持常規(guī)文件系統(tǒng)的語義及一致性(例如目錄的原子重命名,獨占模式打開時的互斥,附加文件內(nèi)容會導(dǎo)致重寫整個文件以及不支持硬連接等等)。這些缺陷導(dǎo)致 S3FS 并不能用于替代常規(guī)文件系統(tǒng)(即便不考慮性能問題),因為當(dāng)應(yīng)用訪問文件系統(tǒng)時,預(yù)期的行為應(yīng)該是符合 POSIX 規(guī)范的,而 S3FS 遠(yuǎn)遠(yuǎn)不能滿足這一點。
分析篇
下面我們將測試的失敗用例進(jìn)行分類統(tǒng)計,挑選幾類比較有代表性的來分析下會對應(yīng)用造成何種限制。
[圖片上傳失敗...(image-8410b3-1646300520186)]
總的來說,無論從數(shù)量還是類別來看,JuiceFS 的失敗用例都更少,有更好的兼容性。Amazon EFS 的失敗用例無論從總數(shù)及類別均大大超出其它幾種文件系統(tǒng),無法放入同一圖表對比,后面將單獨分析。
JuiceFS
JuiceFS 在本次測試中通過了8811項用例中的絕大多數(shù),僅在 utimensat 測試集上失敗了 3 項。對應(yīng)日志如下:
…
/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位整數(shù),故無法通過這三個測試(實際上本次測試涉及的所有文件系統(tǒng)都無法100%通過這個測試集)。如果您的應(yīng)用場景要求秒以下的時間精度或者更大范圍,歡迎聯(lián)系我們商討解決方案。
GCP Filestore
除了和 JuiceFS 一樣在 utimesat 測試集上存在若干失敗結(jié)果之外,GCP Filestore 還在 unlink 測試集中失敗了 1 項。這一項在其他所有文件系統(tǒng)中也都是失敗的。
/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)用于驗證一個文件在打開狀態(tài)下被刪除時的行為:
desc="An open file will not be immediately freed by unlink"
刪除文件的操作在系統(tǒng)層面實際對應(yīng)于 unlink,即移除該文件名到對應(yīng) inode 的鏈接,對應(yīng) nlink 的值減 1,這個測試用例就是要驗證這一點。
# A deleted file's link count should be 0
expect 0 open ${n0} O_RDONLY : unlink ${n0} : fstat 0 nlink
文件內(nèi)容只有在鏈接數(shù)(nlink)減少至 0 并且沒有打開的文件描述符(fd)指向該文件時才會被真正刪除。如果 nlink 沒有被正確更新,可能會導(dǎo)致本該刪除的文件仍然殘留在系統(tǒng)里。
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 用于驗證不具備寫權(quán)限時,應(yīng)該對 O_TRUNC 模式返回 EACCES 錯誤這一行為。
desc="open returns EACCES when O_TRUNC is specified and write permission is denied"
上面這三個失敗日志需要結(jié)合測試代碼來分析,分別對應(yīng) owner,group 和 other 三種情況。不失一般性,我們僅就 owner 情況進(jìn)行分析 :
expect 0 -u 65534 -g 65534 chmod ${n1} 0477
expect EACCES -u 65534 -g 65534 open ${n1} O_RDONLY,O_TRUNC
首先設(shè)置文件 owner 權(quán)限為 4,即 r-- 只讀,然后嘗試以 O_RDONLY,O_TRUNC 模式打開文件,預(yù)期應(yīng)該返回 EACCES,實際返回了 0。
根據(jù) 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 組合使用的結(jié)果是未知的,而且此用例的被測文件本身就是空文件,O_TRUNC 不會產(chǎn)生任何效果。
symlink 失敗用例
對應(yīng)測試日志如下:
/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"
失敗的用例對應(yīng)代碼如下:
n0=`namegen`
nx=`dirgen_max`
nxx="${nx}x"
mkdir -p "${nx%/*}"
expect 0 symlink ${nx} ${n0}
expect 0 unlink ${n0}
該測試用例是要創(chuàng)建長度為 PATH_MAX (包括結(jié)尾的0在內(nèi))的符號鏈接,通不過表明無法在 騰訊云 NAS 上創(chuàng)建長度為 PATH_MAX 的符號鏈接。
阿里云 NAS
相比騰訊云 NAS,阿里云 NAS 在 symlink 上表現(xiàn)正常,但未能通過 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個測試用例來詳細(xì)解釋一下,同時覆蓋了這兩個權(quán)限位
# 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 的權(quán)限創(chuàng)建目標(biāo)文件,然后修改文件內(nèi)容,檢查 SUID 和 SGID 是否被正確清除。文件權(quán)限里的 777 大家會比較熟悉,分別對應(yīng) owner,group和 other 的 rwx,即可讀、可寫、可執(zhí)行。最前面的 0 表示八進(jìn)制數(shù)。
第二位 6 需要著重解釋下,這個八位元組(octet)代表特殊權(quán)限位,其中前兩位分別對應(yīng) setuid/setgid(或稱 SUID/SGID),可以應(yīng)用于可執(zhí)行文件及公共目錄。該權(quán)限位被設(shè)置時,任何用戶都會以 owner (或 group)身份來運行該文件。這個特殊的屬性允許用戶獲取通常只對 owner 開放的文件和目錄訪問權(quán)限。例如 passwd 命令就設(shè)置了 setuid 權(quán)限,這允許普通用戶修改密碼,因為保存密碼的文件是只允許 root 訪問的,用戶不可直接修改。
setuid/setgid 設(shè)計的出發(fā)點是提供一種方法,讓用戶以限定的方式(指定可執(zhí)行文件)訪問受限文件(非當(dāng)前用戶所有)。因此,當(dāng)文件被非 owner 修改時應(yīng)自動清除此權(quán)限位,以避免用戶通過這個途徑獲取其他權(quán)限。
從測試結(jié)果中我們可以看到在阿里云 NAS 中,文件被非 owner 修改時,setuid/setgid 均未被清除,這樣實際上用戶可以通過修改文件內(nèi)容以該 owner 身份進(jìn)行任意操作,這將會是個安全隱患。
rename 失敗用例
阿里云 NAS 在這個測試集上失敗數(shù)量較多,達(dá)到了 24 項,全部出現(xiàn)在 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 位被設(shè)置時 rename 的行為:當(dāng)包含源對象的目錄設(shè)置了 sticky 權(quán)限位的時候,并且源對象和包含目錄的 owner 都與有效用戶ID(effective user ID)不同時,rename 應(yīng)該返回 EACCES 或 EPERM。(這樣的復(fù)雜邏輯令人聯(lián)想到三國殺的武將技能設(shè)定……)。
sticky 位的典型應(yīng)用是 /tmp 目錄,允許所有人創(chuàng)建內(nèi)容,但是只有 owner 才能刪除文件。FTP 里面的公共上傳目錄通常也是這種設(shè)置。
幾個失敗的測試用例表明阿里云 NAS 對 sticky 位的支持還不夠完善,非 owner 的 rename 操作沒有被拒絕,并且產(chǎn)生了實際的效果——源文件被重命名。這種行為越過了文件系統(tǒng)的訪問控制,對用戶文件的安全性造成了威脅。
Amazon EFS 中的失敗用例
Amazon Elastic File System (EFS) 在 pjdfstest 測試中的不僅失敗比例極高(8811個測試用例失敗了1533個),而且?guī)缀醺采w了所有類別,這比較令人意外。

EFS 支持以 NFS 方式掛載,但對 NFS 特性的支持并不完整。比如EFS 不支持塊設(shè)備和字符設(shè)備,這直接導(dǎo)致了 pjdfstest 中大量測試用例的失敗。排除這兩類文件之后,仍然有上百項不同類別的失敗,所以在復(fù)雜場景中應(yīng)用 EFS 必須慎之又慎。
總結(jié)篇
通過上面的對比分析,JuiceFS 在兼容性方面表現(xiàn)最好,像大多數(shù)網(wǎng)絡(luò)文件系統(tǒng)一樣,為了性能犧牲了秒以下的時間精度和范圍(1970 - 2106 年)。Google Filestore 和騰訊云 CFS 次之,有幾類未能通過。而阿里云 NAS 和 Amazon EFS 的兼容性最差,有大量的兼容性測試通不過,其中包括有嚴(yán)重安全隱患的若干個測試用例,使用前建議做安全性評估。
JuiceFS 一直非常重視對 POSIX 標(biāo)準(zhǔn)的高度兼容,我們把 pjdfstest 等兼容性測試工具同其他隨機和并發(fā)測試工具(比如 fsracer、fstool 等)一起作為集成測試工具,在持續(xù)完善功能、提高性能的同時,盡力保持最大程度的 POSIX 兼容性,避免用戶在使用過程中落入各種陷阱,從而更加專注于自身業(yè)務(wù)的發(fā)展。
如有幫助的話歡迎關(guān)注我們項目 Juicedata/JuiceFS 喲! (0?0?)