fc fs筆記
86 subscribers
16 photos
42 files
268 links
Download Telegram
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 的斷電保護也很關鍵。
另一方面所有更現代的 FS 設計(包括 xfs 和 btrfs )都在一開始就採用了 extent based mapping 和 delayed allocation ,這方面 ext4 雖然是後來的實踐者,確實也是把 delayed allocation 推向大衆的最大推動者。
Forwarded from farseerfc 😂
對 fs 做 benchmark 這事情,我覺得換個領域類比一下大概能描述我現在的感受。就像假設有個博客隔幾個月拉來 3dmark glmark 分別在各個發行版的默認安裝的 gnome kde xfce 上跑一下,每個 mark 出個幀數柱狀圖,坐標軸是 gnome kde xfce 。仔細看 footnote 還會發現這博客跑的時候有的在 wayland 有的在 xorg ,有的全屏有的窗口,有的開著混成有的沒開,博主給的理由是為了衡量各個發行版開箱即用得到的體驗。看到這種博客是不是會感覺這博主啥都不懂在瞎測?結果時間久了經常有朋友跑來問 gnome kde xfce 哪個 3d 性能好,是不會產生“為啥會想用 3d 性能評價桌面環境?”的疑惑。再時間久了經常有人拿著那個博客跑來說 gnome 3d 性能不行啊,是不是會感覺很無奈想要反駁一下這個視角錯了…對最終用戶而言3d性能固然很重要,對kwin和mutter開發者們而言性能測試固然也很有意義,但是從用戶角度只看3d性能去選桌面環境是不是就很無厘頭了。我現在看 phoronix 的 fs 測評也就是這種感覺。我覺得搞這測評本身就沒啥價值,衡量的方式和衡量的對象都有問題
fc fs筆記
file-journaling.pdf
關於斷電保護之前的這篇介紹也很不錯