2020年9月15日 星期二

從觀念到實作的 btrfs 入門教學

如果你有至少一顆 ssd 硬碟, 貴哥大力推薦升級至較新版的 linux (kernel 5.4 以上, 例如 貴哥實驗室 ulyana 版), 快來享用可快照、 可容網多個系統根目錄、 省記憶體、 超讚的 btrfs 檔案系統。

一、 預備觀念

根據 這個討論串, 最新一代 (除了先軀 zfs 之外, 其他大約近十年) 具有一些共通特性的檔案系統包含昇陽 (Sun Microsystems) 所開發 (現為 Oracle 所擁有) 的 zfs、 Linux 上原生的 btrfs、 蘋果的 apfs、 微軟的 refs 等等。 草草看一下維基百科的 檔案系統的對比, apfs 沒有資料、 refs 只支援 windows, 兩者都可以直接丟到垃圾桶裡面。 以 「元資料」 和 「特點」 兩個表格來看, zfs 跟 btrfs 看來也是四者當中最佳的選擇。 過去兩年 bionic beaver 的年代, 我的電腦都是採用 zfs; 但是因為 ZFS 現在在興訟大王 Oracle 手中, 所以這次安裝 linux mint 20 (ulyana) 我就改用 btrfs 了。

先介紹一下 zfs 跟 btrfs (可能也適用於其他某些 fs) 的重要共通特性。 很多文章都說 zfs 跟 btrfs 屬於 "COW" (copy-on-write) 類型的檔案系統。 但比較精確的說法應該是: 它們都屬於 "ROW (redirect-on-write)" 類型的檔案系統 -- 每當檔案內容更新時, 會把新的內容寫到新的磁區, 而不是覆蓋掉原先的磁區。 LVM 才是 COW 類型的... 比檔案系統更底層的東東。 所以 zfs 跟 btrfs 製作快照的速度比 LVM 快很多。 大推 富含超連結短文 圖解很清楚的文章。 不過呢, 現在大多數人都用 "COW" 籠統稱呼兩者了。 總之, zfs 跟 btrfs 較適合用於 ssd 而較不適合用於機械式的硬碟, 因為 fragmentation 的問題會令後者速度變很慢。

撕開了一小角的貼紙 玩了一陣子的 btrfs 之後, 我發覺以下的想像/模型很好用 -- 真實的實作方式當然比這模型複雜很多; 不過在某些限制條件下 (例如不要用 nested volumes, 下詳), 這個模型目前對我而言都沒出過問題。 請先了解 (python 的) Shallow Copy vs Deep Copy。 想像整個 btrfs 是一棵樹 (像是很多層的 nested array 或 nested dict)。 每次資料有變動要寫回硬碟上, 都是重新要一塊儲存空間而不是寫回原處。 如果沒有快照 (snapshot) 用到這一小塊資料, 舊的空間就可以回收再利用; 如果有快照用到它, 那麼舊的空間就不可以回收。 更簡單的想像: 目前的檔案系統跟快照所佔用的空間大小, 就像撕開了一小角的貼紙跟底板紙所佔的總面積, 檔案系統的改變越多, 貼紙就撕得越開、 空間用得越兇。

二、 實作測試準備

首先, 建議使用較新版的 kernel。 我曾用 bionic 的 4.15 版 kernel, 還沒用到很複雜的指令, btrfs 檔案系統就被我玩壞掉了。 我成功的 btrfs 經驗都是在 ulyana 的 5.4.0 版 kernel 完成的, 不如就下載 貴哥實驗室 ulyana 版 的 iso 檔來用吧! 如上所述, 建議採用 ssd 硬碟或隨身碟, 不要用機械式硬碟。 如果你有閒置的隨身碟或一整顆 ssd 硬碟, 也可下載虛擬機映像檔, 把隨身碟或 ssd 餵給虛擬機當資料碟。 事實上我做的虛擬機映像檔裡面已有現成的 (含多個快照) 的 btrfs 讓你複製/傳送 (下詳)。

硬碟空間配置: 不論是 MBR 分割表或 GPT 分割表, 建議至少要有一兩個分割區格式化成傳統的 ext4 (或更古老的 vfat), 把 ulyana 的 vmlinuz、 initrd、 root.squashfs 還有 extlinux 以 半自助的方式 裝進去, 以便在最壞的情況下仍可開機。 如果你還會需要用到其他舊版的 linux, 或是未來想玩其他不同的檔案系統, 那麼建議在 LVM 裡面挖一個 logical volume , 或是留一小個完整的分割區 (至少 20G) 給 btrfs 用; 如果是一顆隨身碟, 而且以後想安裝的版本都會支援 btrfs, 那就乾脆省略 lvm, 直接把隨身碟剩下的所有空間作為一個 btrfs 分割區, 因為以後可以有好幾個新版的 linux 並存在同一個 btrfs 分割區裡面, 就像 zfs 一樣。

三、 建立與觀察檔案系統

以下假設規畫給 btrfs 使用的分割區為 /dev/sdx99。

wipefs -a /dev/sdx99
mkfs -t btrfs -L sandbt /dev/sdx99
btrfs filesystem show
mkdir /mnt/sandbt
mount LABEL=sandbt /mnt/sandbt
df	# 確認一下 /dev/sdx99 有被掛載起來
btrfs sub list /mnt/sandbt/

其中 sandbt 是自己任取的名字 -- 我在 SanDisk 上建立的 btrfs。 (不同的隨身碟上面建立的 btrfs 當然要取不同的名字哦!) 現在新建的 btrfs 掛載在 /mnt/sandbt 。

使用 btrfs, 最重要最有趣的當然是建立與使用 subvolumes。 每個 subvolume 都是一個目錄 (但反之不然), 與目錄最大的不同是: 未來可以讓某個版本的 linux 只看到某個 subvolume 並以它為根目錄, 如此不同版本的 linux 就可以並存於同一個 btrfs 而不互相干擾。 btrfs sub list /mnt/sandbt/ 列出它所有的 subvolumes。 目前應該是空的; 但你若用我的虛擬機映像檔下類似的指令, 應該會看到

ID 262 gen 17 top level 5 path ulyana/1-btrzfs
ID 272 gen 20 top level 5 path ulyana/2-live-pxe
ID 273 gen 23 top level 5 path ulyana/3-virt-br
ID 274 gen 26 top level 5 path ulyana/4-essential
ID 276 gen 29 top level 5 path ulyana/5-browser
ID 277 gen 32 top level 5 path ulyana/6-vnc-apache2
ID 280 gen 35 top level 5 path ulyana/7-heavy-apps
ID 293 gen 2863 top level 5 path ulyana/live

之類的, 這些都是我建的 快照 (snapshot)。 每個快照也都是一個 subvolume, 就像 python 裡面, (因為 shallow copy 而) 跟別的變數部分共用儲存空間的變數也都還是一個變數。

因為我還沒有很熟悉 btrfs (而且也暫時看不到需求), 所以 目前我還沒有試過一層包一層的 nested subvolumes。 把 subvolume 放在單純的子目錄裡面倒是沒什麼問題。 目前的規畫是這樣: 在 top level subvolume 底下, 為每個版本的 linux 建一個子目錄 (例如 ulyana), 在那底下, 以後每建一個 snapshot 就佔一個 subvolume。

四、 建立與傳送快照

終於可以上好料了: 快照管理。 這裡假設有另一顆硬碟上的另一個名為 wdbt 的 btrfs 可以掛載在 /mnt/wdbt 底下; 但你也可以自行修改指令, 在同一個 btrfs 底下建立並行的 (非 nested) 其他新的 subvolumes 例如 /mnt/sandbt/bonnie/s[1-4] 。 過程當中不妨開另一個分頁, 每做幾步就用 ls -l /mnt/sandbt/...btrfs sub list /mnt/sandbt/ 查看一下每個指令的效果。

mkdir /mnt/sandbt/andy
btrfs sub create /mnt/sandbt/andy/live
date > /mnt/sandbt/andy/live/1-woody.txt
btrfs sub snap -r /mnt/sandbt/andy/live /mnt/sandbt/andy/s1
date > /mnt/sandbt/andy/live/2-bo.txt
btrfs sub snap -r /mnt/sandbt/andy/live /mnt/sandbt/andy/s2
date > /mnt/sandbt/andy/live/3-rex.txt
btrfs sub snap -r /mnt/sandbt/andy/live /mnt/sandbt/andy/s3
date > /mnt/sandbt/andy/live/4-buzz.txt
btrfs sub snap -r /mnt/sandbt/andy/live /mnt/sandbt/andy/s4

mkdir /mnt/sandbt/snapdump
btrfs send /mnt/sandbt/andy/s1 > /mnt/sandbt/snapdump/s1
btrfs send /mnt/sandbt/andy/s2 -p /mnt/sandbt/andy/s1 > /mnt/sandbt/snapdump/s2
btrfs send /mnt/sandbt/andy/s3 -p /mnt/sandbt/andy/s2 > /mnt/sandbt/snapdump/s3
btrfs send /mnt/sandbt/andy/s4 -p /mnt/sandbt/andy/s3 > /mnt/sandbt/snapdump/s4

mount LABEL=wdbt /mnt/wdbt
mkdir /mnt/wdbt/bonnie
btrfs receive /mnt/wdbt/bonnie < /mnt/sandbt/snapdump/s1
btrfs receive /mnt/wdbt/bonnie < /mnt/sandbt/snapdump/s2
btrfs receive /mnt/wdbt/bonnie < /mnt/sandbt/snapdump/s3
btrfs receive /mnt/wdbt/bonnie < /mnt/sandbt/snapdump/s4
btrfs sub snap /mnt/wdbt/bonnie/s4 /mnt/wdbt/bonnie/live

一個 subvolume 長得就像是 (的確也就是) 一個目錄, 但是要用 btrfs sub create ... 建立而不是用 mkdir ... 建立。 同樣地, 用 btrfs sub delete ... 刪除而不是用 rmdir ... 。 我們先建立 andy/live 這個 subvolume, 然後在裡面隨便放幾個文字檔, 每建立一個新的文字檔, 就用 btrfs sub snap -r 來源 快照 建立一個快照。 這裡的 -r 是 read-only -- 這樣等一下才可以傳送。 之後若有必要, 也可以這樣查詢 read-only 屬性及取消唯讀改回可讀寫:

btrfs property list /mnt/sandbt/ulyana/2-live-pxe
btrfs property get /mnt/sandbt/ulyana/2-live-pxe ro
btrfs property set /mnt/sandbt/ulyana/2-live-pxe ro false

再來用 btrfs send 某快照 -p 前一版快照 > 備份存檔 來儲存快照。 可以想像: 告知 btrfs 前一版快照, 它才知道備份到什麼地方即可停止, 因為以下 (較舊、 未更動過的檔案) 都跟前一版相同, 不需要重複備份。 當然, 第一版的快照沒有 「前一版」 所以存檔時就不需要這個參數。 如果你查手冊: man btrfs-send 或是上網爬文, 會看到大家說 -p 後面的參數是 parent。 但是! 這個用語很令人混淆, 應該要換一個名字才對。 這個 parent 指的是前一版本的快照; 這跟 nested subvolumes 當中的父子關係是完全無關的兩件事。 [2024/4/23]重要! 一個快照 (A) 一旦生過任何一個 child (B), 也就是一旦下過 btrfs send B -p A不可以再被這樣設定: btrfs property set A ro false (可讀寫), 否則在某些情況下當你要在他處 receive B 的時候會出現 no such file or directory 的錯誤, 因為 (B) 會找不到 (A) 本來的檔案。

還原/複製到其他 btrfs 就更簡單了: btrfs receive 目的地路徑 < 備份存檔 系統自己會從備份存檔裡面的資訊決定誰是前一版。 但是這些快照都是唯讀的, 如果要繼續上線使用的話, 就要先用 btrfs sub snap /mnt/wdbt/bonnie/s4 /mnt/wdbt/bonnie/live 建立一個新的、 可讀寫的 「快照」 (我把它稱為 live)。 (想像其實只不過是從唯讀的 s4 複製出一個名為 live、 可讀寫的 shallow copy 而已。)

如果是要立即複製, 顯然可以用 pipe 串起來, 像這樣: btrfs send /mnt/sandbt/andy/s2 -p /mnt/sandbt/andy/s1 | btrfs receive /mnt/wdbt/bonnie 。 我自己在存檔時, 則是會把 send 的結果 pipe 給 gzip 以便節省空間, 像這樣: btrfs send /mnt/sandbt/andy/s2 -p /mnt/sandbt/andy/s1 | gzip > /mnt/sandbt/snapdump/s2.gz

五、 其他常用指令

btrfs sub ... 其實是 btrfs subvolume ... 的簡寫。 btrfs 其他的子指令 (subcommands) 也都可以這樣簡寫。 但查手冊時當然要用全名: man btrfs-subvolume

想更改 subvolume 的名稱? 直接把它當成目錄 mv 即可。

如果因為某些原因而不想掛載 top level subvolume, 也可以這樣掛載一個較低層的 subvolume: mount LABEL=sandbt -o subvol=andy/s2 /mnt/test/ 當然, 要查看時, 用 df 很難分辨, 要用 mount 才分得出來已掛載的是哪個 subvolume。

把某個 subvolume 玩壞了, 要怎麼還原? 這個問答 的步驟是正確的; 但解說是錯的 (不是 "reinstate" 而是另建一個新的 snapshot)。 比方說上述的 /mnt/sandbt/andy/live 玩壞了, 那就直接拿 /mnt/sandbt/andy/s4 來 shallow copy 一下就回春了:

mv /mnt/sandbt/andy/live /mnt/sandbt/andy/broken
btrfs sub snap /mnt/sandbt/andy/s4 /mnt/sandbt/andy/live

可以再用上述的 btrfs property set ... 把 broken 變成唯讀以便 "驗屍", 或是直接用 btrfs sub delete ... 把它刪掉。

六、 拿 btrfs 的 subvolume 作為 root file system

想要拿 btrfs 的一個 subvolume 作為 root file system 比 zfs 的情況 簡單直覺很多。 延續上面, 假設你的 btrfs 名為 gaia, 掛載在 /mnt/gaia 底下, 並且假設你已從我做的虛擬機映像檔把 ulyana/* 諸多 snapshots 用 send/receive 的方式複製到 /mnt/gaia/ulyana/* 去, 又已經建立可讀寫的 snapshot: btrfs sub snap /mnt/gaia/ulyana/7-heavy-apps /mnt/gaia/ulyana/live 接下來只需要做到兩件事:

  1. 在開機選單裡加上 root=LABEL=sandbt rootflags=subvol=ulyana/live 例如我都用 extlinux 開機, 所以我的選單長得類似這樣:
    label gaia:ulyana
            menu label mint 20 ulyana xfce @ gaia (btrfs)
            kernel /ulyana-g20K/vmlinuz-5.4.0-42-generic
            append initrd=/ulyana-g20K/initrd.img-5.4.0-42-generic root=LABEL=gaia rootflags=subvol=ulyana/live net.ifnames=0 biosdevname=0
    
  2. 編輯 /mnt/gaia/ulyana/live/etc/fstab 把 / 那一句改成: LABEL=gaia / btrfs subvol=ulyana/live 0 1

順利的話, 下次就可以拿 gaia 的 ulyana/live 當作系統根目錄開機囉! 讀者可以參考我做的 ulyana 虛擬機映像檔。

七、 雜記

另一個理解快照的比喻是 google 地圖上的 「規畫路線」。 想像你的系統隨著時間演變, 就像是你用手緩緩地拉著最終目的地, 而每個快照就像是中途的每個額外目的地。 每次為 ulyana/live 做快照, 就像是在最終目的地的位置新增一個重疊的中途目的地 (很簡單, 馬上完成)。 隨著你繼續使用系統, 最終目的地就慢慢移往別處。 如果刪除某個中途目的地, 那就需要重新規畫路線, 反而比較花時間。 最終的總路徑會比較短一些 (使用的總空間會減少) 但如果刪掉的中途目的地本來就沒有太繞路, 那麼節省的路途 (節省的空間) 也就很有限。 例如我可以把系統中的 [2-6]-* 五個快照刪掉, 只留下 1-btrzfs 跟 7-heavy-apps 。 你可以不斷地 df 查看空間的變化: 需要花一些時間空間才會完全釋放出來, 而且節省的並不多。 建議不要刪掉最早的快照, 因為它可能擁有最多快照共用的空間。 這樣萬一有需要, 還可以從第二早的快照再慢慢一點一點加回來。

因為 redirec-on-write 的特性, 想要在 zfs 跟 btrfs 上面使用 swapfile 都會遇到問題, 要小心處理。 對 btrfs 來說, 沒有 zfs 的 volume 選項可用; 但是可以透過 「關閉個別檔案的 ROW/COW」 來完成。 重點就是要先 chattr +C ...。 詳見 arch 的 wiki reddit 的討論。 當然, 如果你的系統裡有 lvm, 那麼採用 lvm 的一個 volume 來當 swap partition 會更單純。

如果我建議的思考方式 (shallow copy / 撕貼紙) 不夠用了, 更上一級 (很多級) 的閱讀可以考慮這篇超級深入詳盡的中文文章: 「Btrfs vs ZFS 實現 snapshot 的差異」

使用 btrfs 作為 root file system 工作已經幾週, 沒什麼問題, 也曾還原 snapshot 幾次, 覺得很好用。 而且只需要配置 2G 的記憶體就可以開 (虛擬) 機、 在裡面開一個 firefox。 以記憶體的使用來說, btrfs 比 zfs 客氣多了, 超棒。 我要把所有的隨身碟都升級成 ulyana on btrfs!

2 則留言:

  1. 嗨,你好


    請問為什麼我的btrfs send -p 命令無效.因為我第一個文件夾有123這個文件,進行第二次備份是它竟然還是存在,這是為什麼


    btrfs send /.snapshots/aa-bak-2 -p /.snapshots/aa-bak-1 | btrfs receive /bb/
    At subvol /.snapshots/aa-bak-2
    At snapshot aa-bak-2
    root@ubuntu:/aa# tree -a /bb
    /bb
    ├── aa-bak
    ├── aa-bak-1
    │   └── 123
    └── aa-bak-2
    ├── 123
    ├── 456
    └── test
    └── readme


    回覆刪除
    回覆
    1. 你如果沒把 123 刪掉, 那麼它繼續存在也是正常的啊。

      刪除

因為垃圾留言太多,現在改為審核後才發佈,請耐心等候一兩天。