Btrfs 詳解:快照
![](https://linuxstory.org/wp-content/uploads/2024/06/092244yog42vb0l3v5h33h-800x400.jpg)
這篇文章會探討什麼是 Btrfs 快照,它們如何工作的,你在日常生活中進行快照的好處。這篇文章是《Btrfs 詳解》系列文章中的一篇。從 Fedora Linux 33 開始,Btrfs 就是 Fedora Workstation 和 Fedora Silverblue 的默認文件系統。
如果你錯過了,這裡是本系列的上一篇文章:Btrfs 詳解:子卷
簡介
想像一下,你長時間處理一個文件,反覆添加和撤銷修改。然後,在某個時刻你意識到:兩小時前你撤銷的部分修改,現在會非常有用。而昨天在你銷毀那個設計之前,你也已經修改了這個特殊的部分。當然,由於你會定期保存文件,所以舊的改動會丟失。很多人可能都遇到過這樣的情況。如果能恢復舊版本的文件,而無需定期手動複製,豈不美哉?
這是一個 Btrfs 快照可以幫助你的特別場景。當你使用正確的話,快照同時也為你的電腦提供了很好的備份方案。
下面你會找到一些關於快照的例子。如果你想跟著操作,你必須擁有訪問某些 Btrfs 文件系統的許可權和 root 許可權。你可以通過下面命令來驗證一個目錄的文件系統。
$ findmnt -no FSTYPE /home
btrfs
這個命令會輸出你 /home/
目錄的文件系統名稱。如果它是 btrfs
,那就可以了。讓我們創建一個新的目錄去做實驗:
$ mkdir ~/btrfs-snapshot-test
$ cd ~/btrfs-snapshot-test
在下面的文本中,你會看到很多像上面顯示的那樣的命令輸出框。請在閱讀/比較命令輸出時請記住,框中的內容在行末會被換行。這使得識別跨多行的長行變得困難,降低了可讀性。如果有疑問,試著調整瀏覽器窗口的大小,看看文本的變化!
Btrfs 快照
讓我們從一個基本的問題開始:什麼是 Btrfs 快照?如果你在文檔 [1] 和維基 [2] 中查找,你不會立刻找到這個問題的答案。事實上,從「功能」一節里是找不到的。如果你搜索一下,你會發現快照和 Btrfs 子卷一起被大量地提及 [3] 。所以現在做什麼呢?
還記得快照在系列前面的文章里兩次被提到嗎?是這樣說的:
CoW 的優勢在哪裡?簡單的說:文件被修改和編輯的歷史被保存了下來。Btrfs 保存文件舊版本的引用(inode)可以輕易地被訪問。這個引用就是快照:文件系統在某個時間點的狀態鏡像。這將是這系列文章里的單獨的一篇,所以暫時留到後面介紹。
以及:
另外一個分離
/
和/home
的優勢是我們可以分別進行 快照 。子卷是快照的邊界,對一個子卷的快照永遠不會包含該子卷下面的其他子卷的內容。快照的更多細節會在後續的文章中介紹。—— Btrfs 詳解:子卷
看起來快照是和 Btrfs 子卷相關的。你可能之前在其他地方聽到過快照,比如說 LVM(邏輯卷管理器)。雖然技術角度上它們都是為了同一個目的,但它們在實現方面有所不同。
每個 Btrfs 快照是一個子卷。但是,不是每個子卷都是一份快照。區別在於子卷裡面包含的內容。一個快照是子卷加上一些內容:它包含對現在和過去版本的文件的引用(inode)。讓我們看看快照是從哪來的!
創建 Btrfs 快照
想使用快照功能,你需要一個 Btrfs 子捲來進行快照。讓我們在測試目錄(~/btrfs-snapshot-test
)里創建一個:
$ cd ~/btrfs-snapshot-test
$ sudo btrfs subvolume create demo
Create subvolume './demo'
$ sudo chown -R $(id -u):$(id -g) demo/
$ cd demo
因為 Btrfs 子卷默認是被 root 所有的,你必須用 chown
去修改子卷里的文件的所有權到普通用戶上。現在我們在裡面新加一些文件:
$ touch foo bar baz
$ echo "Lorem ipsum dolor sit amet, " > foo
你的目錄現在看起來像這樣:
$ ls -l
total 4
-rw-r--r--. 1 hartan hartan 0 Dec 20 08:11 bar
-rw-r--r--. 1 hartan hartan 0 Dec 20 08:11 baz
-rw-r--r--. 1 hartan hartan 29 Dec 20 08:11 foo
讓我們從這裡創建第一次快照:
$ cd ..
$ sudo btrfs subvolume snapshot demo demo-1
Create a snapshot of 'demo' in './demo-1'
這就好了。讓我們看看發生了什麼:
$ ls -l
total 0
drwxr-xr-x. 1 hartan hartan 18 Dec 20 08:11 demo
drwxr-xr-x. 1 hartan hartan 18 Dec 20 08:11 demo-1
$ tree
.
├── demo
│ ├── bar
│ ├── baz
│ └── foo
└── demo-1
├── bar
├── baz
└── foo
2 directories, 6 files
這看起來是一份拷貝!為了驗證,我們從快照里讀取 foo
的內容:
$ cat demo/foo
Lorem ipsum dolor sit amet,
$ cat demo-1/foo
Lorem ipsum dolor sit amet,
當我們修改原始文件時,真正的效果變得明顯:
$ echo "consectetur adipiscing elit, " >> demo/foo
$ cat demo/foo
Lorem ipsum dolor sit amet,
consectetur adipiscing elit,
$ cat demo-1/foo
Lorem ipsum dolor sit amet,
這表明快照仍然持有「舊」版本的數據:foo
的內容沒有改變。到目前為止,你可以通過一個簡單的文件複製來實現完全相同的目標。現在你也可以繼續處理舊文件了。
$ echo "sed do eiusmod tempor incididunt" >> demo-1/foo
$ cat demo-1/foo
Lorem ipsum dolor sit amet,
sed do eiusmod tempor incididunt
但是在底層,我們的快照實際上是一個新的 Btrfs 子卷。你可以通過下面的命令來驗證這一點:
$ sudo btrfs subvolume list -o .
ID 259 gen 265 top level 256 path home/hartan/btrfs-snapshot-test/demo
ID 260 gen 264 top level 256 path home/hartan/btrfs-snapshot-test/demo-1
Btrfs 子卷 vs. 文件複製
這一切有什麼意義呢?到目前為止快照看起來是一個更加複雜的複製文件的方式。事實上,快照不僅僅是表面上看起來那麼簡單。讓我們來創建一個更大的文件:
$ dd if=/dev/urandom of=demo/bigfile bs=1M count=512
512+0 records in
512+0 records out
536870912 bytes (537 MB, 512 MiB) copied, 1.3454 s, 399 MB/s
現在有一個512 MB 大小的新文件 demo/bigfile
。讓我們創建另一個快照,這樣在你修改數據的時候就不會丟失:
$ sudo btrfs subvolume snapshot demo demo-2
Create a snapshot of 'demo' in './demo-2'
現在我們通過追加少量字元串到文件來模擬變化:
$ echo "small changes" >> demo/bigfile
這是生效後的文件結構:
$ tree
.
├── demo
│ ├── bar
│ ├── baz
│ ├── bigfile
│ └── foo
├── demo-1
│ ├── bar
│ ├── baz
│ └── foo
└── demo-2
├── bar
├── baz
├── bigfile
└── foo
3 directories, 11 files
但是真正的神奇的發生在其他地方。你已經複製了 demo/bigfile
,你現在擁有了兩個大約 512 MiB 的文件。但是,因為它們是不同的拷貝,它們應該會佔據共 1 GiB 的空間。記住兩個文件的差異不超過 10 位元組 —— 和原文件大小相比這幾乎沒什麼差別。
Btrfs 快照工作原理與文件複製不同:而是它們保持對當前和過去的 inode 的引用。當你在文件追加更新時,在底層 Btrfs 分配更多的空間去存儲更新,同時在原來的 inode 增加對新數據的引用。之前的內容保持不變。為了便於理解,你可以認為這是僅僅「存儲」原文件和修改版本的差異。
讓我們看看這個效果:
$ sudo compsize .
Processed 11 files, 5 regular extents (9 refs), 3 inline.
Type Perc Disk Usage Uncompressed Referenced
TOTAL 100% 512M 512M 1.0G
none 100% 512M 512M 1.0G
這個有趣的數字出現在 TOTAL
一行:
Referenced
是當前目錄下所有文件大小的總和Disk Usage
是用於在磁碟上存儲文件分配空間的大小
你有一共 1 GiB 的文件,但存儲它們僅僅佔據了 512 MiB。
Btrfs 快照和備份
目前為止,在這篇文章中,你已經看到如何創建 Btrfs 快照和它們的特別之處。有人可能會想:如果我在我的 PC 本地進行一系列的快照,我就有一個可靠的備份策略。 其實不是這樣的 。如果 Btrfs 子卷共享的底層數據被偶然破壞了(被 Btrfs 之外的東西影響,比如宇宙射線),所有指向這些數據的子卷都會存在相同的錯誤。
為了讓快照成為真正的備份,你應該將它們存儲到一個不同的 Btrfs 系統上,例如在一個外部驅動器上。為了本文的目的,讓我們在一個文件里創建一個新的 Btrfs 系統,並掛載它來模擬一個外部驅動。如果你有一個格式為 Btrfs 的外部驅動器,請隨意替換以下命令中提到的所有路徑來試試!讓我們創建一個新的 Btrfs 文件系統:
注意:下面的命令會在你的文件系統上創建一個 8 GB 大小的新文件。如果你想跟著下面的步驟,請確保你的磁碟空間至少有 8 GB 剩餘。請不要分配小於 8 GB 到這個文件,否則 Btrfs 可能在掛載時會遇到問題。
$ truncate -s 8G btrfs_filesystem.img
$ sudo mkfs.btrfs -L "backup-drive" btrfs_filesystem.img
btrfs-progs v5.18
See http://btrfs.wiki.kernel.org for more information.
[ ... ]
Devices:
ID SIZE PATH
1 8.00GiB btrfs_filesystem.img
這些命令創建了名為 btrfs_filesystem.img
的 8 GB 新文件,同時在上面格式化了一個 Btrfs 文件系統。現在你可以像外部驅動器一樣掛載它:
$ mkdir backup-drive
$ sudo mount btrfs_filesystem.img backup-drive
$ sudo chown -R $(id -u):$(id -g) backup-drive
$ ls -lh
total 4.7M
drwxr-xr-x. 1 hartan hartan 0 Dec 20 08:35 backup-drive
-rw-r--r--. 1 hartan hartan 8.0G Dec 20 08:37 btrfs_filesystem.img
drwxr-xr-x. 1 hartan hartan 32 Dec 20 08:14 demo
drwxr-xr-x. 1 hartan hartan 18 Dec 20 08:11 demo-1
drwxr-xr-x. 1 hartan hartan 32 Dec 20 08:14 demo-2
妙,現在掛載在 backup-drive
下面有一個獨立的 Btrfs 文件系統!讓我們嘗試進行快照並且把快照放進去:
$ sudo btrfs subvolume snapshot demo backup-drive/demo-3
Create a snapshot of 'demo' in 'backup-drive/demo-3'
ERROR: cannot snapshot 'demo': Invalid cross-device link
發生了什麼?噢,你嘗試對 demo
進行一次快照並把它存在不同的 Btrfs 文件系統里(從 Btrfs 視角來看是一個不同的設備)。還記得一個 Btrfs 子卷僅持有對文件和內容的引用(inode)?這正是問題所在:文件和內容存在於我們的 home
文件系統,但不在新創建的 backup-drive
。你得找到一種方式去傳輸子卷和其內容到新的文件系統里。
在不同的 Btrfs 文件系統存儲快照
針對這個目的 Btrfs 工具有兩個特殊的命令。讓我們首先來看看它們是如何工作的:
$ sudo btrfs send demo | sudo btrfs receive backup-drive/
ERROR: subvolume /home/hartan/btrfs-snapshot-test/demo is not read-only
ERROR: empty stream is not considered valid
另一個錯誤!這時它告訴你我們想要傳輸的子卷不是只讀的。這是對的:你可以寫入新內容到所有目前為止創建的快照/子卷。你可以像這樣創建一個只讀的快照:
$ sudo btrfs subvolume snapshot -r demo demo-3-ro
Create a readonly snapshot of 'demo' in './demo-3-ro'
不像之前那樣,這裡 -r
選項被加到了 snapshot
子命令里。這創建一個只讀的快照,這很容易去驗證:
$ touch demo-3-ro/another-file
touch: cannot touch 'demo-3-ro/another-file': Read-only file system
現在你可以重新嘗試傳輸子卷:
$ sudo btrfs send demo-3-ro | sudo btrfs receive backup-drive/
At subvol demo-3-ro
At subvol demo-3-ro
$ tree
├── backup-drive
│ └── demo-3-ro
│ ├── bar
│ ├── baz
│ ├── bigfile
│ └── foo
├── btrfs_filesystem.img
├── demo
[ ... ]
└── demo-3-ro
├── bar
├── baz
├── bigfile
└── foo
6 directories, 20 files
成功了!你成功傳輸原來子卷 demo
的一個只讀快照到一個外部的 Btrfs 文件系統。
在非 Btrfs 文件系統存儲快照
上面你已經看到你如何能存儲 Btrfs 子卷/快照到其他的 Btrfs 文件系統。但如果你沒有其他的 Btrfs 文件系統並且不能新創建一個,比如說外部驅動器需要一個和 Windows 或 MacOS 兼容的文件系統,你可以做什麼呢?在這種情況下你可以存儲子卷在文件里:
$ sudo btrfs send -f demo-3-ro-subvolume.btrfs demo-3-ro
At subvol demo-3-ro
$ ls -lh demo-3-ro-subvolume.btrfs
-rw-------. 1 root root 513M Dec 21 10:39 demo-3-ro-subvolume.btrfs
文件 demo-3-ro-subvolume.btrfs
現在包含了隨後重建 demo-3-ro
子卷需要的所有東西。
增量地發送快照
如果你對不同的子卷重複執行這個操作,你會發現在某些時間點不同的子卷不再共享它們的文件內容。這是因為像上面一樣發送一個子卷,去重建這個單獨的子卷的所有數據將被傳送到目標位置。但是,你可以引導 Btrfs 只向目標位置發送不同子卷的差異!所謂的增量發送將保證共享的引用在子卷中仍然共享。為了展示這一點,新增一些變動到我們原來的子卷:
$ echo "a few more changes" >> demo/bigfile
然後創建另一個只讀子卷:
$ sudo btrfs subvolume snapshot -r demo demo-4-ro
Create a readonly snapshot of 'demo' in './demo-4-ro'
然後現在發送它:
$ sudo btrfs send -p demo-3-ro demo-4-ro | sudo btrfs receive backup-drive
At subvol demo-4-ro
At snapshot demo-4-ro
在上面的命令,-p
選項指定了一個父子卷用來計算差異。重要的是記住原 Btrfs 文件系統和目標 Btrfs 文件系統都必須包含相同的、未被修改過的父子卷!確保新的子卷真的在那裡:
$ ls backup-drive/
demo-3-ro demo-4-ro
$ ls -lR backup-drive/demo-4-ro/
backup-drive/demo-4-ro/:
total 524296
-rw-r--r--. 1 hartan hartan 0 Dec 20 08:11 bar
-rw-r--r--. 1 hartan hartan 0 Dec 20 08:11 baz
-rw-r--r--. 1 hartan hartan 536870945 Dec 21 10:49 bigfile
-rw-r--r--. 1 hartan hartan 59 Dec 20 08:13 foo
但你怎樣知道增量發送只傳輸了子卷間的差異呢?讓我們傳輸數據流到一個文件里然後看看它有多大:
$ sudo btrfs send -f demo-4-ro-diff.btrfs -p demo-3-ro demo-4-ro
At subvol demo-4-ro
$ ls -l demo-4-ro-diff.btrfs
-rw-------. 1 root root 315 Dec 21 10:55 demo-4-ro-diff.btrfs
根據 ls
,這個文件僅僅只有 315 位元組大小!這意味著增量傳輸只傳輸子卷間的差異,和額外的 Btrfs 相關的元數據。
從快照中恢復子卷
在繼續之前,讓我們清理掉這時候不再需要的東西:
$ sudo rm -rf demo-4-ro-diff.btrfs demo-3-ro-subvolume.btrfs
$ sudo btrfs subvolume delete demo-1 demo-2 demo-3-ro demo-4-ro
$ ls -l
total 531516
drwxr-xr-x. 1 hartan hartan 36 Dec 21 10:50 backup-drive
-rw-r--r--. 1 hartan hartan 8589934592 Dec 21 10:51 btrfs_filesystem.img
drwxr-xr-x. 1 hartan hartan 32 Dec 20 08:14 demo
到目前為止你已經成功創建了可讀/寫和只讀的 Btrfs 子卷快照,並把它們發送到外部。但是,為了把這作為備份策略,還要有一種方式去發送子卷回原來的文件系統和讓它們再次變為可寫。出於這個目的,我們移動 demo
子卷到其他地方並且嘗試從最近的快照中重建它。第一步:重命名為 broken
子卷。一旦恢復成功它會被刪除:
$ mv demo demo-broken
第二步: 傳回最近的快照到這個文件系統:
$ sudo btrfs send backup-drive/demo-4-ro | sudo btrfs receive .
At subvol backup-drive/demo-4-ro
At subvol demo-4-ro
[hartan@fedora btrfs-snapshot-test]$ ls
backup-drive btrfs_filesystem.img demo-4-ro demo-broken
第三步: 從快照創建一個可讀寫的子卷:
$ sudo btrfs subvolume snapshot demo-4-ro demo
Create a snapshot of 'demo-4-ro' in './demo'
$ ls
backup-drive btrfs_filesystem.img demo demo-4-ro demo-broken
上一步非常重要:你不能重命名 demo-4-ro
為 demo
,因為這仍然是一個只讀子卷!最後你可以檢查你所有你想要的東西是不是在那裡:
$ tree demo
demo
├── bar
├── baz
├── bigfile
└── foo
0 directories, 4 files
$ tail -c -19 demo/bigfile
a few more changes
最後的命令告訴你 bigfile
的最後 19 個字元實際上是上次變更執行的結果。這個時候,你可能想從 demo-broken
複製最近的更新到新的 demo
子卷。因為你沒有執行任何更新,你可以過時的子卷:
$ sudo btrfs subvolume delete demo-4-ro demo-broken
Delete subvolume (no-commit): '/home/hartan/btrfs-snapshot-test/demo-4-ro'
Delete subvolume (no-commit): '/home/hartan/btrfs-snapshot-test/demo-broken'
就是這樣!你成功從一個之前存在不同 Btrfs 文件系統(外部介質)的快照中恢復 demo
子卷。
子卷作為快照的邊界
在本系列的第二篇文章中我提到子卷作為快照的邊界,但這到底是什麼意思呢?簡單來說,子卷的一份快照僅包含這個子卷的內容,而不是下面嵌套的子卷。讓我們來看看這個:
$ sudo btrfs subvolume create demo/nested
Create subvolume 'demo/nested'
$ sudo chown -R $(id -u):$(id -g) demo/nested
$ touch demo/nested/another_file
讓我們像以前一樣進行一次快照:
$ sudo btrfs subvolume snapshot demo demo-nested
Create a snapshot of 'demo' in './demo-nested'
然後查看裡面的內容:
$ tree demo-nested
demo-nested
├── bar
├── baz
├── bigfile
├── foo
└── nested
1 directory, 4 files
$ tree demo
demo
├── bar
├── baz
├── bigfile
├── foo
└── nested
└── another_file
1 directory, 5 files
注意到 another_file
不見了,僅僅是目錄 nested
還在。這是因為 nested
是一個子卷:demo
的快照包含嵌套子卷的目錄(掛載點),但裡面的內容是缺失的。目前沒有方法遞歸地執行快照去包含嵌套子卷。但是,我們可以利用這個優勢去從快照中排除一些目錄!這通常對那些你容易再現的數據有用,或者它們很少變化。例子有虛擬機或者容器鏡像,電影,遊戲文件等等。
在總結之前,我們移除所有測試過程中創建的東西:
$ sudo btrfs subvolume delete demo/nested demo demo-nested
Delete subvolume (no-commit): '/home/hartan/btrfs-snapshot-test/demo/nested'
Delete subvolume (no-commit): '/home/hartan/btrfs-snapshot-test/demo'
Delete subvolume (no-commit): '/home/hartan/btrfs-snapshot-test/demo-nested'
$ sudo umount backup-drive
$ cd ..
$ rm -rf btrfs-snapshot-test/
基於 Btrfs 備份最後思考
如果你決定使用 Btrfs 來執行數據的定期備份,那麼你可能需要使用一個工具來自動完成這項任務。Btrfs 維基有一個專門針對 Btrfs [4] 的備份工具列表。在那裡,你還將看到另一個手動執行 Btrfs 備份步驟的摘要。就我個人而言,我對 btrbk
[5] 有很多很好的體驗,我正在使用它來執行我自己的備份。除了備份之外,btrbk
還可以在你的 PC 本地保存 Btrfs 快照列表。我使用它來防止意外的數據刪除。
如果你想要了解更多有關使用 Btrfs 進行備份的內容,歡迎在下面評論,我會考慮寫一篇專門討論這個話題的後續文章。
總結
本文研究了 Btrfs 快照,它們本質上是 Btrfs 子卷。你了解了如何創建可讀寫和只讀快照,以及這種機制如何有助於防止數據丟失。
本系列的後續文章將討論:
- 壓縮 - 透明地節省存儲空間
- 配額組 - 限制文件系統大小
- RAID - 替代 mdadm 配置
如果你還想了解與 Btrfs 相關的其他主題,請查看 Btrfs 維基 [2:1] 和文檔 [1:1] 。不要忘記查看本系列的前兩篇文章!如果你認為本文缺少了一些內容,請在下面的評論中告訴我們。再會!
參考資料
- https://btrfs.readthedocs.io/en/latest/Introduction.html ↩︎ ↩︎
- https://btrfs.wiki.kernel.org/index.php/Main_Page ↩︎ ↩︎
- https://btrfs.readthedocs.io/en/latest/Subvolumes.html ↩︎
- https://btrfs.wiki.kernel.org/index.php/Incremental_Backup#Available_Backup_Tools ↩︎
- https://github.com/digint/btrbk ↩︎
(題圖:MJ/ad8a61ed-ce6f-409e-b503-69104dd71149)
via: https://fedoramagazine.org/working-with-btrfs-snapshots/
作者:Andreas Hartmann 選題:lujun9972 譯者:A2ureStone 校對:wxy
本文轉載來自 Linux 中國: https://github.com/Linux-CN/archive