fc fs筆記
213 subscribers
17 photos
43 files
282 links
Download Telegram
fc fs筆記
https://utcc.utoronto.ca/~cks/space/blog/tech/FilesystemPerfQuestionToday
總結:現代系統上,針對 SSD(SATA/SAS/nvme)而言,文件系統性能只要足夠,就不需要比較性能有多好了,別的方面的差異比性能更重要。
感想:實際上比較文件系統性能的 benchmark 也很容易誤導。文件系統按其功能定義是有狀態的,benchmark 為了“公平”起見通常會測量全新狀態下的性能表現,而這通常有別於使用一段時間之後的性能。
https://lore.kernel.org/lkml/YhIwUEpymVzmytdp@casper.infradead.org/ 內核在討論要不要移除 reiserfs ,RIP
關於文件系統如何保證斷電時的一致性:記錄日誌(journaling)、軟更新(soft updates)、日誌式結構(log-structured)、A/B更新、寫時拷貝(copy on write)
(先挖個坑,一會兒亂序慢慢填,記錄一下思緒,說不定以後會整理成博客)
介紹這些斷電保護機制之前可能需要先解釋一下為啥需要斷電保護,畢竟有些上古的文件系統(比如 fat 系)貌似完全沒有這方面設計似乎也活得好好的。之前解釋硬盤演化的坑裡說過,早期軟盤硬盤可以完全沒有控制芯片,純粹靠CPU“驅動”,那個年代尤其是 DOS 下沒有多程序並發的寫入,程序調用 DOS 終端寫一個文件,DOS 就調用對應 IO 的中斷,按文件系統的邏輯順序寫東西,CPU控制下寫的東西只有一個當前位置,所以如果寫入過程中突然斷電也只是最多有一個扇區存在寫入一半的狀況,導致文件系統的不一致性可能發生在一個扇區。所以這種時候 chkdsk 檢查和修復就足以應對了。Unix時代早期的 fs 也幾乎是這個思路,雖然 Unix 有並發多進程,不過內核部分還是不可重入的,所以寫入文件系統的代碼基本可以保證寫入順序,然後發生了突然斷電就依靠 fsck 修文件系統的不一致。
說到文件系統的不一致,可能很多地方提過 fsck/chkdsk 可以修它,但是沒說過具體不一致是指什麼意思,又是怎麼修的。比方說程序在 4K 簇大小的 fat32 上創建文件 c:\abc\def.txt 寫了兩個簇的數據,DOS接到這個文件系統的寫入調用,實際要做的事情是在 fat32 好幾個位置寫東西:
a. 找到 abc 這個文件夾所在的簇,往文件夾裡添加一個文件表項 def.txt 記錄下創建時間、文件名、文件長度之類的元數據
b. 掃描 fat 找兩個空簇,寫入 fat 文件分配表,把這倆簇號連成一個單鍊錶
c. 把文件數據實際寫入這倆簇裡
d. 把起始簇號寫到 abc 文件夾的 def.txt 文件表項裡
e. 調整可用簇計數,寫入到 fat 開頭的 vbr(superblock)
實際上述寫入的順序可能不是我列出來的順序,不過大體上是要按照某個順序在不同的偏移處寫入這些東西。考慮在上述寫入過程中斷電的話,那麼可能發生幾種不一致(不完全列表,只是臨時想到的):
1. 文件表項說 def.txt 長度有倆簇,但是起始地址卻是空的。
2. 文件表項說 def.txt 的起始簇號是 0x100 ,但是 fat 文件分配表中記錄的 0x100 狀態卻是未佔用
3. fat文件分配表中 0x100 和 0x104 倆簇形成單鍊錶已佔用的狀態了,但是未能成功寫入 def.txt 文件表項,整個文件夾樹中沒有文件的起始簇號是 0x100 。
4. 把兩簇號記錄為了已佔用的單鍊錶,但是 vbr 中的已佔用計數沒有減2。
等等情況,也就是一部分寫入成功了而另一部分寫入失敗了的話,散佈在文件系統中不同地方的記錄出現了偏差。
這時對 fat32 做 chkdsk ,它會去檢查 fat32 上述各個元數據是否合理,試圖修復一致性。比如:
1. 發現 def.txt 的起始簇號是 0x100 ,0x100 下一個簇是 0x104 ,0x104 是單鍊錶末尾。但是 def.txt 的文件長度是0字節:這時可以修改文件長度為 8K ,雖然正確的文件長度可能不到 8K ,於是文件末尾多了一些字節的垃圾。
2. 發現 0x100 和 0x104 構成一個單鍊錶,但是文件夾樹中沒有文件的起始簇號是 0x100 ,那麼只能創建一個新文件叫 RECOVER1 放在根目錄去,內容指向 0x100 。
3. 發現 vbr 中記錄的已用簇計數比 fat 文件分配表中實際佔用的簇少了 2 ,這時去調整 vbr 中的計數。
總之 chkdsk 依賴於知曉 fat32 寫入時修改元數據的具體順序,然後基於它看到的實際情況,做合理或不合理的猜測,假定突然斷電發生時,已經產生了哪些寫入,還未產生哪些寫入,猜測哪邊大概是正確的然後調整另一邊。
有的時候chkdsk發現的問題的修復方式不是那麼顯然的,比如如果 fat 中 0x100 和 0x102 記錄的下一簇都是 0x104 ,也就是說有兩個不同文件“共享”了同一個結尾,那麼必然需要犧牲一個文件的內容,砍斷尾巴,使所有文件在 fat 中的記錄都成為單鍊錶。
可見 chkdsk 修復文件系統一致性的方式,並不在意“正確性”,只在意不同位置的數據之間是否合乎邏輯,避免文件系統其餘地方的代碼遇到邏輯上不一致的 bug 。“修復一致性”這個操作,有時是可以搶救數據的,有時是會破壞數據的,目標只是還原到“一致”的狀態。
Unix 上的傳統 fs 其實也是類似。Unix分支較多,實現細節比較繁雜,就拿設計上很接近傳統 Unix fs 的 ext2 舉例吧。比如要在 4K塊大小的 ext2 中創建文件 /abc/def.txt 寫6K(2塊)數據,實際需要的讀寫大概是:
a. 掃描 block group info 找一個相對比較空的 block group
b. 掃描這個 block group 的 inode bitmap 找一個空 inode 槽,在 inode bitmap 記錄這個槽佔用了
c. 往 inode 槽裡寫入文件元數據(權限/用戶/時間戳)
d. 掃描這個 block group 的 block bitmap 找兩個空的 block ,在 block bitmap 記錄它們被佔用了
e. 往這倆 block 中實際寫入 6K 長度的數據
f. 在 inode 的 block map 記錄那倆 block 號
g. 找到 /abc 這個文件夾的 block ,插入一個叫 def.txt 的文件記錄,指向剛創建的 inode
h. 調整 block group info 中該 block group 的 inode 和 block 計數
i. 調整 superblock 中總體的 inode 和 block 計數
等等,在上述寫入操作中突然斷電的話,同樣有可能產生一部分寫入已完成而另一部分寫入還沒完成的狀態,於是下次掛載時 fsck 需要掃描 superblock / block group info / inode bitmap / block bitmap / 文件夾表項 / inode 表,尋找不一致性並且試圖修復。
Ted Ts'o 寫的 e2fsck 是這類傳統 Unix fs 設計的 fsck 中出名得非常健壯的 fsck 工具,在 fsck 過程中分成好幾個 pass ,每個 pass 掃描並確保特定某一類元數據的一致性,後面的 pass 依賴前面 pass 修好的一致的數據結構。
pass 的這種依存關係也基於 ext2 實際寫入時的順序的假設,靠 fsck 修 ext2 的時候通常假定當兩邊出現不一致的時候,早期 pass 先檢查到新落盤的數據,後期 pass 可以據此試圖修復後落盤的數據。
可見 fsck/chkdsk 修復斷電產生不一致的時候,基於“文件系統的寫入存在嚴格順序”這個假設。
理解了文件系統的一致性和 fsck 實際在做什麼,回到前面說的早期硬盤由CPU驅動的事情。隨著計算機更新換代,一個趨勢是 CPU 越來越快,而存儲設備容量越來越大,讀寫速度卻遠跟不上CPU速度增速的情況。換句話說讓 CPU 驅動硬盤緩慢的讀寫就很浪費 CPU 週期。同時 CHS 尋址方式到 LBA 尋址的轉變也使得硬盤逐漸加入越來越複雜的控制器,分擔CPU驅動的活。
硬件上的變化,同時催生出軟件上的變化:操作系統尤其是 Unix/Linux 的並發程度越來越高,同時讀寫的情況越來越多,於是 OS 內核中有了 IO 隊列這樣的東西。
脫離斷電保護的上下文,說 fs 讀寫 hdd 的時候,大概會假設 fs 產生的寫入請求立刻落到盤上。但是在斷電保護的上下文中,fs 的寫入請求和落盤的時機就變得關鍵了。
從內核的 fs 產生一個寫入請求的時候,實際上是先把要寫的數據寫在了內存中的緩存裡,然後 fs 把寫入請求提交給 IO 隊列,然後 fs 就去接著幹別的了。之後 IO 隊列拿著一系列請求,可能做一些“優化”,根據讀寫地址重新排列 IO ,調整讀寫發生的先後順序,然後再根據 IO 隊列的順序(而不是 fs 發出請求的順序)對設備讀寫。
而設備這邊拿到讀寫請求之後,在設備上也會被控制器進一步安排順序,可能先寫入盤上的緩存,再根據設備自己的知識(比如哪些 lba 在物理上更加接近)安排讀寫順序,最後實際完成寫入。
於是軟硬件層面發生的變化是,無論是OS內的IO隊列,或是硬盤控制器上的隊列,都可能會交換寫入請求的順序。在某一特定時刻,fs 發出的很多寫入請求可能都排在 OS 或者 hdd 的隊列中等待落盤。
於是上面所說的 fsck 修復一致性所依賴的兩個前提條件都不成立了:
1. 同一時刻可能有多個寫入請求處於隊列中
2. fs 實際上無法控制寫入請求落盤的順序
發生了突然斷電,fsck 發現元數據不一致了,但是它並不知道該怎麼修了,它不知道這不一致的兩邊,哪邊的數據是斷電前已經成功落盤的,哪邊的數據是還沒覆蓋的老數據。閉著眼睛想像hdd還是和以前一樣保證寫入順序當然可以修到一致的程度,但是“修復”本身可能造成更大的損害。從用戶角度看,就是 fs “不穩定”了,“丟數據”了,一斷電就壞了。
不過即便OS和hdd上都有緩存和隊列,其實還是有辦法控制寫入順序的。hdd 的接口比如 SCSI SATA,或者 SSD 的 nvme ,除了讀寫指令之外,都提供了某些特殊指令用於控制寫入隊列。SCSI 有 TCQ ,SATA 有 NCQ 。IDE/PATA 由於實現 ATA TCQ 是可選的,而且會產生大量中斷,所以好像還不普及 ,而到了 SATA 的年代 NCQ 已經普及了。
撇開各個接口的技術細節,大體上說,Linux的文件系統可以對IO隊列提兩種特殊命令:
FLUSH: 讓設備清空當前隊列中尚存的寫入。都落盤了之後才繼續接受後面的寫入。Linux 5.x 開始變成 PREFLUSH tag ,加在某個寫入請求上,意思是執行這個寫入前保證之前提及的寫入都被刷到盤上。
FUA(forced unit access):當設備確認寫入已完成的時候,要求該寫入落到了盤上,而不是只落在了緩存。
這些特殊指令會被轉換到具體接口上的特殊命令,控制寫入隊列,強制某些特定的寫入之間的順序。
(順便別的OS比如 Windows 和 Mac 上也有類似的東西,只是具體指令和語義可能有些差別)
有了這些特殊指令,Linux 實現 fs 的時候可以通過他們限制寫入順序從而拯救突然斷電的情景。
不過最蠢的做法,比如每個寫入都加上 FLUSH ,顯然可以讓文件系統的斷電保證回到完全沒有隊列時的表現,但是完全禁止掉隊列,又會嚴重影響到寫入性能。所以實現 fs 的時候需要有選擇的,在關鍵的寫入之間加入這些指令,只控制特定一些寫入的前後的順序,把別的無關緊要的寫入請求的順序交由IO隊列或者設備上隊列自由調控。
具體如何正確利用上述特殊指令控制IO隊列就成為了文件系統實現中各有不同的地方,這也是各個文件系統的斷電保護機制上和讀寫性能上有所差異的原因。想要理解為何突然斷電可能造成文件系統損壞、發生數據損壞時責任在文件系統還是在存儲設備硬件,重要的一點就在於理解這些斷電保護機制的實現細節。
繼續說斷電保護,上面描述 OS 的 IO 隊列和磁盤上的緩存隊列的時候,忽略了一個情況:從 FS 發出的 IO 請求,到達磁盤控制器之前,中間可以經過 SCSI/SATA/nvme/FC 等這些設備接口,也可以經過別的塊設備抽象層,比如 Linux 的 lvm/mdraid (底下是 device mapper ),比如跑在虛擬機內的 Linux 可能經過虛擬機虛擬的塊設備操作虛擬磁盤,比如通過 USB mass storage / USB Sttached SCSI 接入的“可移除存儲”的控制器,比如通過 iSCSI/NVMe-oF 接入的設備可能會有更複雜的塊設備控制器(本質上是另一台服務器實現了塊設備的讀寫接口)。這些林林總總的塊設備抽象層實現各有差別,有的非常智能,有的非常簡陋。它們都或多或少會做邏輯地址翻譯,把上層 FS 發出讀寫請求時用的地址,翻譯到下層實際設備的地址,可能產生不連續的地址映射關係(比如 FS 寫入 100 到 110 的 lba ,可能被翻譯成 230 到 235 接著 176 到 180 兩斷 lba ),也可以有各自的緩存和隊列實現。所以使用這些方式接入的設備也需要正確實現上述塊設備的特殊指令的語義,保證從 FS 發出的特殊請求能跨越一系列翻譯/緩存/重新排序,對應到底下實際存儲設備上的。換句話說就算 FS 和硬盤本身實現正確,如果中間的塊設備層沒能正確傳達這些特殊指令,那整體就難以保證斷電時數據的一致性了。
FS 實現斷電保護最常見的機制是寫日誌(journaling),由於這種機制也經常被用作 RDBMS 比如 MySQL/PostgreSQL 的異常恢復機制中,所以程序員們對它的習性相對來說比較熟悉。提到 journaling 的時候,大部分介紹經常把它描述成「寫兩遍」,確實寫日誌需要把被日誌保護的數據寫兩遍,但是寫兩遍本身不是實現日誌的關鍵,說成寫兩遍也容易和 A/B 更新那種斷電保護機制混淆(甚至會有人覺得 CoW 也是寫兩遍?)。日誌的關鍵特徵在於:
1. 日誌記錄在一段連續的(邏輯上環狀的)存儲空間,通常不會和別的存放元數據或者文件數據的存儲區域混在一起。
2. 對日誌的寫入操作只有在末尾添加(append-only)這一種模式,後續添加的記錄可能會讓之前的日誌記錄失效,但是日誌記錄覺不會覆蓋寫入(overwrite)尚未失效的之前的記錄。
3. 日誌中的多個連續的寫入記錄在邏輯上構成事務(transaction)單位,整個事務中的多個寫入看作一個整體,通常會有個事務編號或者事務UUID之類的東西標記。
4. 用上述塊設備層面提供的特殊指令,保證事務寫入的一致性。然後通過日誌的事務保證對日誌之外的文件系統寫入的原子性。
根據日誌中記錄的操作的方向,日誌記錄可以分爲重現(redo)日誌和撤銷(undo)日誌。重現日誌中記錄的是即將發生的寫入的新數據,這樣如果寫入被中斷,可以重放(replay)日誌中的記錄,重放後保證新數據一定會落在盤上。撤銷日誌中記錄的是即將被覆蓋的地址上記錄的老數據,這樣如果寫入已經發生而日誌需要回滾(rollback)可以把對應地址恢復到事務寫入發生前的狀態。大部分現代 FS 都採用重現(redo)日誌,包括 ext3/4 的 JBD ,XFS 這些都是記錄重現日誌。NTFS 的 $LogFile 中似乎同時記錄了 redo 和 undo 的操作,貌似是用來支持 TxF 特性的,具體細節我也沒看明白……
然後根據日誌記錄中實際記錄下的數據的形式,又可以分爲物理(physical)日誌和邏輯(logical)日誌。(熟悉關係數據庫的人可能可以想到基於行(row-based)的日誌和基於語句(statement-based)的日誌)在物理日誌這一類中,通常記錄的是即將寫入的地址(比如數據塊的 LBA),和將要寫入到地址上的數據本身(比如一整塊 4K 的實際數據);在邏輯日誌中記錄的是一系列操作,每個操作記錄操作類型(追加/刪除/改寫之類的)、操作的對象(inode號之類的)和操作的值,比如「把 inode 345 的所有者改成 1000 」之類的,這樣一條邏輯操作。
具體來說 ext3/4 (在 5.10 之前)只有通過 JBD 實現的物理日誌。JBD和JBD2是個相對獨立的組件,不光 ext3/4 還有別的 FS 也可以用(好像有個叫 ocfs2 的在用 jbd2 ?),換句話說 JBD 的實現機制完全不理解它記錄的內容,ext3/4 讓它記錄啥,它就忠實地記下「要寫入的地址」和「地址上要寫入的數據」,到開機需要做日誌回放的時候,它就從事務中讀出一列日誌記錄,按照地址往設備上覆蓋寫數據,僅此而已。JBD不理解也不需要理解 ext3/4 的佈局結構,不知道它記錄的是 inode 表還是文件夾內容還是塊位圖或是別的東西。
相反 XFS 實現的是邏輯日誌,在日誌中記錄下操作的類型和操作的對象,而非地址和數據塊。
ext4 5.10 開始有一個新特性叫 Fast Commit ,在 JBD2 所管的地址範圍內,又單開出一塊區域給 Fast Commit ,然後 Fast Commit 實現了記錄文件 fsync 時所需的操作。換句話說 ext4 在 5.10 開啓 Fast Commit 特性之後,對一部分操作可以採用邏輯日誌了。當 Fast Commit 記錄的事務中出現 Fast Commit 還不支持的操作的時候, ext4 會清除掉 Fast Commit 區域,回退到傳統的 JBD2 日誌。
ext3/4 使用物理日誌,優點是 JBD 代碼不需要理解 ext3/4 的結構就可以記錄任何種類的操作,畢竟所有操作都是在某個地址上寫入某塊。掛載選項的 data=journal|ordered|writeback 實際控制在日誌中記錄哪些塊以及記錄的順序。
data=journal 會在日誌中記錄所有寫入,包括文件數據,這是 JBD 的靈活性允許的:它不關心記錄的是元數據還是數據。
data=ordered 是默認行爲,會先等文件數據落盤(比如通過 FLUSH 刷入所有還在隊列中的寫入操作),然後再在日誌中記錄元數據變化。這意味着重啓回放日誌之後,新寫入的數據也可能出現在還沒提交(不會被回放)的事務中,也可能發生寫入截斷(寫入覆蓋的文件數據塊已經落在盤上,而新增加的文件數據塊還沒記錄在元數據中),這些都是 ext3/4 默認情況下允許出現的文件內容不一致。
data=writeback 不會等待文件數據落盤就開始寫入和提交日誌中的元數據,用這個的時候 ext3/4 完全不理會文件內容本身是否一致而只保證文件系統本身的一致性,用文件內容的一致性去換吞吐。
可見正因爲物理日誌使得 ext3/4 可以選擇是否在日誌中記錄文件內容,如果採用邏輯日誌就沒有了這種靈活性。不出意外的, ext4 在 5.10 內核加入的 Fast Commit 只能在 data=ordered 模式下開啓,不再兼容別的日誌模式。
物理日誌的缺點也很明顯:記錄元數據修改的粒度是一整塊(通常4K),而非 inode (ext4是256字節)之類的邏輯上的單位,於是提交 inode 可能會影響到別的 inode ,而用戶空間對這種影響不可控制。比如用戶空間的數據庫程序同時修改了兩個 inode ,一個很小(比如鎖文件)而另一個很大(比如上 GiB 容量的表文件),對很小的那個鎖文件做 fsync ,不湊巧那個鎖文件的 inode 和那個表文件的 inode 放在了同一個 4K 頁面中,那 fsync 必須等待包含兩個 inode 記錄的整個一塊都刷到盤上,在 data=ordered 上也意味着那個上 GiB 的表文件中所有隊列中緩存的寫入也必須全都刷入盤上才能開始提交 inode 寫入到日誌。這使得(沒有 Fast Commit 的) ext3/4 上 fsync 操作的延遲變得不可預期(內部取決於被 fsync 的 inode 保存在同一塊的別的 inode ,而用戶空間控制不了這個 inode 排列)。 Fast Commit 特性注重於部分解決物理日誌在這方面的缺陷。
上面說 XFS 採用邏輯日誌,其實 XFS 自己的文檔說它們用了一種混合了物理日誌和邏輯日誌的實現,並且在邏輯日誌基礎上,把多條邏輯日誌記錄合併在一次日誌寫入中,叫做 re-logging 。具體細節可參考上面 xfs filesystem structure 3.141952 文檔中的第3章 delayed logging (講設計和思想)和第13章 journaling log (講具體數據結構細節)。另一點實現上的區別在 XFS 的 logging 是延遲(delayed)和異步(asynchronous)的,試圖儘量合併日誌寫入操作直到固定大小的日誌緩存寫滿。
另一點不得不提的差異或者說常見誤解,(與其說是物理日誌和邏輯日誌的差異不如說是 ext4 和 xfs 實現上的差異)在於, XFS (和 btrfs 之類更現代的設計)斷電保護設施的正確性事實上依賴於上述IO隊列提供的特殊指令(FLUSH/FUA那些),xfs 從 4.19 內核開始不再支持 barrier/nobarrier 的掛載參數,更早之前就發出了廢棄警告。 btrfs 雖然仍然支持 barrier/nobarrier ,但是明確表示關閉 barrier 在突然斷電時產生文件系統損壞是幾乎無可避免的。這裏 ext4 仍然支持 nobarrier 並且文檔上對關閉 barrier 的操作並不那麼嚴厲這一點上還是有所差異。
考慮一下如果IO隊列提供的特殊指令實現不正確的情況下會發生什麼:
1. 如果IO隊列長度只有 1 (比如 USB mass storage 的時候,接口上的隊列長度爲 1 ,雖然設備內部可能有更長的寫入隊列),由於 journal 的地址空間連續而且 append-only ,大體可以假定 OS 內的 IO 隊列會按照 LBA 安排寫入順序,從而 JBD 的寫入大體上可能符合 ext4 的要求(除了 journal area wrap back 的情況),對 ext4 而言斷電保護機制的可靠性可能回退到了 data=writeback 同等的保護。
2. 如果還有 LVM / qemu qcow2 之類的塊映射層,那 IO 隊列安排的寫入順序就不一定是 ext4 發出的寫入順序了。
3. 如果不是 SATA/SCSI HDD 這種單通道設備,而是 nvme 之類支持多通道讀寫的設備接口,那寫入順序更是無法保證。
可見 「ext4 不強制要求 barrier 」只在最簡單的系統構成情況下可能會有點點保護作用(不通過地址映射層,直接連接 HDD ,不能是 SMR ,不能是 SSD ),在現在常見的 PC 上能滿足這些條件的系統越來越少了。幫助內核識別和排除掉那些不能正確實現特殊指令的設備應該是所有消費者共同的目標(而不是指責某些 FS 要求這些指令的正確性而固守更老舊的 FS)
上面的介紹裏說 ext3 的 JBD 和 ext4 的 JBD2 幾乎是一直放在一起的(除了 ext4 才有的 fast commit ),實際上 ext4 從 ext2/3 用的 block mapping 遷移到 extent based mapping 之後,很重要的一大改進還在於 delayed allocation ,也會影響到斷電保證 。ext2/3 的 allocation path 在用戶空間開始 write 那一刻就做 allocation 從而對這些 allocation 所做的元數據修改提交日誌,之後程序慢慢填充內存緩衝,而 pagecache 子系統會幾乎很快把這些 dirty page 刷到盤上。ext4 採用 extent 方式記錄地址映射之後,爲了讓 extent 儘量連續,會儘量延遲 allocation 發生的時機,於是 write 進緩存的數據在一段時間內還沒被分配地址於是不會被刷到盤上,影響是 ext4 表面上看起來比 ext3 更容易丟寫入。當年這一切換曾經引起 Linux 社區很大波瀾, Theodore Ts'o 當年就寫了幾篇博客介紹相關細節和澄清一些誤解:
Delayed allocation and the zero-length file problem
Don’t fear the fsync!
雖然有些歷史了,不過這些博客對理解 ext4 的斷電保護也很關鍵。