【知乎】日志型文件系統(tǒng) - 原理和優(yōu)化

日志型文件系統(tǒng) - 原理和優(yōu)化

蘭新宇
talk is cheap
說明:本文示例基本來自CRASHCONSISTENCY: FSCKANDJOURNALING。
位于磁盤上的文件系統(tǒng)需要面臨的一個問題是:當(dāng)系統(tǒng)crash或者意外掉電的時候,如何維持?jǐn)?shù)據(jù)的一致性。比如現(xiàn)在為了完成某項功能,需要同時更新文件系統(tǒng)中的兩個數(shù)據(jù)結(jié)構(gòu)A和B,因為磁盤每次只能響應(yīng)一次讀寫請求,勢必造成A和B其中一者的更新先被磁盤接收處理。如果正好在其中一個更新完成后發(fā)生了掉電或者crash,那么就會造成不一致的狀態(tài)。
假設(shè)一個文件系統(tǒng)在磁盤上的分布是這樣的:其中包括一個文件的inode(即I[v1]),data block(即Da)和記錄分配狀態(tài)的inode bitmap, data bitmap。

現(xiàn)在要在文件末尾追加一部分內(nèi)容,那么需要分配一個新的data block(即Db),這會導(dǎo)致inode和data bitmap都發(fā)生相應(yīng)的變化。

因此需要對磁盤的三個不同位置(3個blocks)分別進行寫操作,如果在這三次寫操作的間隙發(fā)生了crash,那么可能出現(xiàn)若干種情況:只更新了其中一個部分,或者只更新了其中的兩個部分。
傳統(tǒng)的Unix系統(tǒng)對此的解決辦法是使用fsck工具,但是fsck通常需要在發(fā)生crash重啟后,掃描整個磁盤,繁瑣且速度較慢,因此目前更流行的做法是使用日志(Journaling)。
原理
日志型文件系統(tǒng)的基本思想是這樣的:在真正更新磁盤上的數(shù)據(jù)之前,先往磁盤上寫入一些信息,這些信息主要是描述接下來要更新什么,相當(dāng)于?wrtie ahead,因此這種方式又被稱為write-ahead logging。
這樣,即便發(fā)生crash,也可通過記錄的日志信息,回溯并恢復(fù)crash前正在進行的操作(稱為replay)。在更新的時候增加一點額外的操作,換來了recovery時所需的工作量的減少。
在Linux中,ext2文件系統(tǒng)將磁盤劃分成若干個block groups,每個block group包含一個inode bitmap, data bitmap, inodes和若干個data blocks,它沒有使用日志。

而在ext3文件系統(tǒng)中,加入了對日志的支持,日志部分單獨占據(jù)一塊磁盤空間。

還是前面那個例子,現(xiàn)在我們要更新磁盤上的inode(I[v2]), bitmap(B[v2])和data block (Db)。在更新這個數(shù)據(jù)之前,我們把這個更新操作的步驟(稱為一個?transcation),加上分別表示這個transcation開始和結(jié)束的TxB和TxE,寫入磁盤。
Transaction的概念起源于數(shù)據(jù)庫領(lǐng)域,它有助于在操作未能完成的情況下保證數(shù)據(jù)的一致性。同一transcation的TxB和TxE具有相同的sequence number。

在一個記錄步驟信息的transcation完成之后,才真正地更新磁盤上的文件數(shù)據(jù)(這一步稱為checkpoint)。
Journal是為了保證數(shù)據(jù)一致性的,而journal本身也是由多個部分組成,那在寫入journal的過程中發(fā)生了crash怎么辦?由于I/O scheduling等原因,各個部分不一定是按照提交的順序?qū)懭氲模╫ut of order),那么可能出現(xiàn)下圖所示的這種情況:缺少了Db,但TxB和TxE都存在,系統(tǒng)恢復(fù)的時候會誤以為這是一個完整的transcation。

解決的辦法是使用write barrier。寫入除TxE以外的部分后(這一步叫做?journal write),執(zhí)行一次barrier操作(對于支持journal checksum的ext4文件系統(tǒng),此步驟可省略)。如果在此期間crash了,由于沒有TxE,這個transcation會被認(rèn)為是不完整的,重啟后就不會試圖恢復(fù)這個transcation所代表的步驟。

待journal write順利完成以后,再寫入TxE(這一步叫做?journal commit),然后再執(zhí)行一次barrier操作,以保證數(shù)據(jù)的寫入是發(fā)生在journal完成之后(write barrier在保證數(shù)據(jù)一一致性的同時,會不可避免地對性能造成影響,如果能夠接受不使用barrier帶來的潛在風(fēng)險,可以在mount文件系統(tǒng)的時候使用"nobarrier"選項)。

因此,在日志型文件系統(tǒng)中,一個完整的數(shù)據(jù)寫操作由"journal write","journal commit"和"checkpoint"三部分組成。寫操作完成后,就可以釋放journal本身所占據(jù)的磁盤空間了。

至此,涉及多個block的寫操作的數(shù)據(jù)一致性問題算是有了保證,但這基于的是一個block的數(shù)據(jù)要么完全寫了,要么完全沒寫的前提,那會不會出現(xiàn)一個block的數(shù)據(jù)只寫了一部分的情況呢(half-written)?這其實是一個原子性的問題,由磁盤本身提供保障,即對一個block的操作必須是原子的(不可分割的)。
優(yōu)化
journal好是好,不過要多做的工作也是顯而易見的,別的不說,磁盤的I/O負(fù)載首先就會蹭蹭地上升。那有什么辦法可以優(yōu)化嗎?(此處的「優(yōu)化」是真的優(yōu)化哈,不是公司裁人的那種所謂“優(yōu)化”)。
一種比較容易想到的方法是將多個journal的操作進行聚合處理,這種batching的思想在軟件設(shè)計中也是隨處可見的。結(jié)合到日志型文件系統(tǒng)自身的過程,還可以將journal中對"Db"的記錄移除,即journal中只包含對inode和bitmap更新的記錄。

此時,write barrier只能保證對meta data(包括inode和bitmap)的真實寫操作發(fā)生在journal的寫操作之后,由于磁盤寫操作的out of order特性,user data的真實寫操作則可能發(fā)生在此過程中的任何節(jié)點,這種模式被稱為"Writeback"。
允許out of order對性能的提升當(dāng)然是有裨益的,但如果crash是發(fā)生在journal寫操作之后,meta data的真實寫操作之前(假設(shè)也在user data的寫操作之前),那么進行文件系統(tǒng)的recover時,meta data的寫操作會被replay,但是user data的寫操作不會,這將有可能造成同一文件的meta data和user data的不一致(圖1左側(cè)部分)。

不過呢,Writeback模式相對完全不用journal的模式,造成不一致的概率降低了,不一致帶來的危害也降低了,作為性能和穩(wěn)健的平衡,還是有相當(dāng)?shù)目扇≈幍?。如果想把穩(wěn)健性再提高一點呢,就再多做出一點限制:即保證對user data的真實寫操作發(fā)生在journal的寫操作之前,這就是"Ordered"模式(圖1中間部分)。
對于Ordered的模式,如果crash發(fā)生在user data的寫操作之后,journal的寫操作之前,那么將造成這一部分user data的丟失,不過不會造成不一致的問題。如果crash發(fā)生在journal的寫操作之后,meta data的真實寫操作之前,那么完全可以通過replay來還原。
可見,從"Writeback"模式,到"Ordered"模式(同為Metadata Journal),再到本文最開始介紹的基本模式(Data Journal),數(shù)據(jù)丟失和不一致的風(fēng)險是依次降低的,而對性能的損耗則是依次升高的。現(xiàn)代的文件系統(tǒng)通常會提供多種模式的選擇,供不同場景下的用戶使用。
參考:
日志文件系統(tǒng)是怎樣工作的
https://en.wikipedia.org/wiki/Ext3
https://en.wikipedia.org/wiki/Journaling_file_system
Barriers and journaling filesystems
原創(chuàng)文章,轉(zhuǎn)載請注明出處
編輯于 2021-02-25 00:14
「真誠贊賞,手留余香」
還沒有人贊賞,快來當(dāng)?shù)谝粋€贊賞的人吧!
文件系統(tǒng)
操作系統(tǒng)
Linux 內(nèi)核