Btrfs:認識、從Ext4遷移與快照方案

語言: CN / TW / HK

對於Arch系等依賴滾動更新的發行版,Btrfs的快照功能真的是太具有吸引力了。縱使我已經很久沒有遇到“滾炸”、縱使就算“滾炸”去Manjaro論壇看一眼一般都能解決,但是這些都不如一個“後悔藥”來得實在——遇到問題,重啟、選擇老快照、恢復,一切都是那麼美好。因此,前陣子(指12月中旬)我就把系統分割槽遷移到Btrfs上了。這篇部落格就主要記錄了遷移與快照的各種實現方案。

Btrfs:現代Linux檔案系統

本著Arch精神,我還是想先簡單介紹一下Btrfs。Btrfs(我一般念 B tree FS )是最早由Oracle貢獻的Linux檔案系統,如今已經進入Linux核心許久,是最有希望(我認為)成為未來Linux主流檔案系統的候選者。相較之下,類似的ZFS有License問題;潛力十足的ReiserFS因為作者謀殺入獄後開發進度就不太樂觀,且v4也沒被合入核心;XFS走的是類似Ext4的穩定路線,對新功能的支援較為保守(但是足夠穩定,可以用於資料盤)。這個開源軟體的命運啊,還真是不可預料。

Btrfs功能繁多,可以稱得上是新檔案系統功能的試驗田。到目前為止,Btrfs已經有了包括但不限於如下的功能:

  • 寫時複製(CoW)、事務
  • 線上卷管理:碎片整理、大小調整、跨裝置卷中物理裝置的增刪
  • 子卷:可以單獨掛載的檔案樹,類似LVM的邏輯卷
  • 軟體RAID
  • 透明壓縮:寫磁碟時自動壓縮
  • 快照
  • 支援資料校驗和

組合這些功能,可以實現很多非常不錯的功能。比如開啟透明壓縮、CoW後跨網路同步磁碟,可以最大限度的減少需要傳輸的資料量。此外,Btrfs也提供了一個非常不錯的CLI介面來進行管理。不過Btrfs的缺點也很明顯:資料恢復難度顯著大於Ext4等傳統FS、穩定性一般、讀寫速度也相對較差(主要是HDD,SSD基本沒有問題)。這些問題就算到了Btrfs上述功能已經完成了將近15年的今天,也仍舊需要改善。好在目前Facebook、群暉等企業都在使用Btrfs,未來還是明朗的。

不過,這些小問題並不能阻擋我 作死 嚐鮮的步伐。因為把系統分割槽遷移到Btrfs是有若干好處的:

  1. 快照是滾動更新的後悔藥
  2. 大量系統檔案並不效能敏感,開啟透明壓縮能節省不少空間
  3. CoW對SSD硬碟十分友好,而Btrfs的效能劣勢在SSD的補足下並不明顯(相反HDD可能有40%的效能損失)
  4. 系統分割槽也沒什麼重要資料,炸了也無所謂

子卷和快照

Btrfs一個非常重要的概念就是子卷。子卷可以理解成為“盤中盤”,可以單獨掛載,行為類似一個資料夾(可以移動,但不能 unlink )。子卷是可以巢狀的,不過此時子卷相當於同時掛載在一顆檔案樹,因此並不會迴圈巢狀。子卷可以根據其相對於根子卷(Btrfs分割槽自帶的子卷,路徑是 / ,ID是5)的路徑確定(比如 /subA ),不過因為子卷的路徑可以被移動,因此具體子卷是通過ID定位的。

快照就是基於子卷的,建立快照等同於建立一個和原子卷共享檔案的子卷。因此其實快照的原子卷的地位是平等的,建立快照後刪除原子卷也沒有什麼問題。結合根據路徑查詢子卷,就可以實現快照某個路徑了:把子卷 /sub 掛載到 /dest ,如果要恢復快照就用某個快照子卷替換掉子卷 /sub

從Ext4遷移

子卷規劃

瞭解了Btrfs之後,就可以考慮如何規劃子捲了。區分不同子卷的主要目的是為Btrfs提供的大量功能劃定作用的粒度。比如對於系統檔案(比如配置等等),我們可能不太在意它的存取效率,因此可以劃分一個單獨的子卷並在掛載的時候開啟壓縮;對於日誌之類的檔案,則沒有快照的意義,因此另立子卷不進行快照。

參考Ubuntu風格的命名方式,我推薦如下的子卷格式:

  • @ :根子卷,對應 / 。存放需要快照的系統檔案
  • @home :家子卷,對應 /home
  • @cache :快取子卷,對應 /var/cache 。包含Pacman包快取等沒必要快照的檔案
  • @log :日誌子卷,對應 /var/log 。不快照日誌,方便查錯

這個規劃方式主要是針對Manjaro等Arch系發行版。如果有其他需要,也可以新增如 @opt@srv 等子卷,反正不要錢也不麻煩(openSUSE預設有9個子卷)。

規劃完子卷後,遷移時按圖索驥即可。不過由於我只是想用到Btrfs的快照功能,加之比較擔心自己的資料,因此並沒有遷移家目錄。如果有此需求,請自行擴充遷移過程。此外,我也不建議通過遷移工具來直接把ext4分割槽轉為btrfs分割槽,因為至少在本文寫作時還有見到遷移後使用不穩定的報告。

所以,我最終採用的是Timeshift的遷移方式,也可以使用等價的 rsync 指令。首先備份好根分割槽。之後,建立btrfs分割槽並建立根子卷 @ (假設目標分割槽是 /dev/nvme0n1p1 )。

sudo mkfs.btrfs /dev/nvme0n1p1  # 這一步也可以在分割槽工具裡完成
sudo mount /dev/nvme0n1p1 /mnt/btrfs
sudo btrfs subvolume create /mnt/btrfs/@

然後就可以進入Live CD,把原系統分割槽揚掉了。之後通過Timeshift或 rsync 把備份好的根分割槽恢復到目錄 /mnt/btrfs/@ ,這樣就完成了檔案的遷移。接下來,逐一的建立子卷。比如要建立 /var/cache 對應的 @cache 子卷:

sudo btrfs subvolume create /mnt/btrfs/@cache
mv -v /mnt/btrfs/@/var/cache/* /mnt/btrfs/@cache

完成子卷建立後,還需要修改 fstab 以正確掛載(這裡編輯的是 /mnt/btrfs/@/etc/fstab )。注意每個子卷都需要增加對應的記錄,但是對應的磁碟分割槽UUID都是同一個,指向Btrfs分割槽。分割槽UUID可以通過 blkid 指令查詢。此外,非 ssd 磁碟需要去掉 ssd 選項。

UUID=...	/	btrfs	defaults,noatime,ssd,[email protected]	0	1
UUID=...	/var/log	btrfs	defaults,noatime,ssd,[email protected]	0	1
UUID=...	/var/cache	btrfs	defaults,noatime,ssd,[email protected]	0	1

最後更新一下引導就可以了。

manjaro-chroot /mnt/btrfs/@
grub-install --target=x86_64-efi --efi-directory=EFI資料夾 --bootloader-id=manjaro --recheck

此時重啟系統就應該在根子捲了。

系統快照方案

簡單梳理了下需求,並按照重要程度排序:

  • 方便的實時建立快照、實時回滾
  • 可以定時備份、清理
  • 可以啟動進快照,以備滾炸之需
  • 在滾動更新前自動建立快照

各種方案們

經過了解與分析,主要的方案有如下幾種。

  • Ubuntu風格:使用Timeshift,只能識別 @@home 子卷
  • openSUSE風格:使用Snapper,可以作用於任何子卷,快照存放在預設的 @/.snapshot
  • Arch Linux風格:使用Snapper,可以作用於任何子卷,但建立獨立的 @snapshot 子卷

權衡利弊

至於為什麼會有那麼多方案?沒錯,因為每一種都多少有點缺陷,所以就有一幫人出來造輪子……

首先是Timeshift的解決方案,這個方案就非常有Ubuntu的穩定氣質。它強制了子捲風格必須是 @@home ,也沒有提供快照的複雜功能,但是在使用者體驗上做的很好(尤其是GUI)。在啟動進快照時會自動彈出視窗提示回滾,在Live CD裡也可以進行恢復操作。因此,對於安穩的使用來說,我最推薦的也就是Timeshift方案了(這也是Manjaro預設的方案)。

Snapper和Timeshift就很不同了,比起Timeshift做的類似TimeMachine的系統恢復工作,Snapper更像是一個快照管理工具。它提供了豐富的快照管理方式:可以設定快照型別,並按型別定製自動清理規則;支援pre-post的方式管理快照,即在進行某操作的前後進行快照;甚至可以分析兩個快照之間檔案系統發生的變化,可以說是非常的Geek。但更Geek的是,它的GUI甚至不包含快照恢復功能。對於openSUSE,快照恢復可以通過 snapper rollback 解決;而對於Arch Linux風格,你甚至需要手動輸入一堆指令回滾。至於支援Live CD回滾?想都別想,老老實實敲指令!

究其因,Snapper是openSUSE的人做的,但是它們的辦法卻並不是很Arch。這裡的分歧在於快照子卷。Arch Linux社群認為,恢復快照的方式應該是這樣:首先刪除老的子卷( @ ),然後“複製”某個快照為新的子卷(實際操作也是一次快照)。但是openSUSE社群的思路完全是逆轉過來的,它們認為為啥要把系統放在固定的 @ 子卷呢?不如直接把系統放在一個可寫入的快照裡面(比如 @/.snapshot/0 )!每次回滾,比如說回滾到快照6,就當場給快照6建立一個可寫快照8,然後我的系統就直接變成了快照8(比如 @/.snapshot/8 )。相當於所有快照構成一系列世界線,然後真的系統待在其中一個上。但問題是,這樣需要修改 grub 的原始碼,這就很不Arch了。所以Arch Linux Wiki裡給出的Snapper用法就是用Snapper管理快照,但是獨立快照子卷且手動恢復快照。由於這是違背Snapper設計目的的用法,因此部分功能就是有問題的(rollback命令不可用、無法識別當前所在的快照)。不過我自己設計了一個Workaround,因此也能在Manjaro下實現openSUSE的方法,各位可以看完具體方案再做決定。

Ubuntu風格:Timeshift方案(推薦)

配置流程

非常滴簡單,你只需要:

sudo pacman -S timeshift timeshift-autosnap-manjaro grub-btrfs
sudo systemctl enable --now grub-btrfs.path

然後開啟Timeshift,按照配置嚮導的Btrfs設定即可。此時使用pacman安裝包也會建立快照,如果不需要建立快照,就在指令之前加上 SKIP_AUTOSNAP= 。還可以編輯 /etc/timeshift-autosnap.conf 檔案來控制快照行為:

  • 設定 updateGrubfalse ,因為不更新也會自動觸發
  • maxSnapshots 可以設定大一點,免得安裝時覆蓋了滾系統前的快照。我設定了5

如果你使用的不是Manjaro,那安裝 timeshift-autosnap 之後需要使用 sudo systemctl edit grub-btrfs.path 並修改為:

[Unit]
Description=Monitors for new snapshots of timeshift
DefaultDependencies=no
Requires=run-timeshift-backup.mount
After=run-timeshift-backup.mount
BindsTo=run-timeshift-backup.mount

[Path]
PathModified=/run/timeshift/backup/timeshift-btrfs/snapshots

[Install]
WantedBy=run-timeshift-backup.mount

Troubleshooting

如果建立快照時卡頓

建議在設定裡關閉 啟用配額組 功能,然後執行 sudo btrfs quota disable /

如果KDE下無法瀏覽快照檔案

新版本KDE應該不會出現這個問題。如果打不開資料夾,可以安裝一個其他檔案瀏覽器,比如Nemo。

rEFInd支援快照啟動

雖然有輪子,但是說實話效果一般且難看。所以我建議rEFInd引導Grub,然後Grub負責快照。如果實在看不慣兩個介面,建議編輯 /etc/default/grub

GRUB_TIMEOUT=2
GRUB_TIMEOUT_STYLE=hidden

Arch Linux/openSUSE風格:Snapper方案

兩種方案的麻煩程度大差不差。硬要說的話,我個人更喜歡openSUSE的方式,因為它能運用到Snapper本身的全部功能。雖然看完過程很可能被勸退,但是其實我真的挺喜歡Snapper強大的功能,非常值得一 試。

1. 安裝Snapper

sudo pacman -S snapper snapper-gui
sudo snapper -c root create-config /

2. Arch Linux風格特有的配置

雖然看著很麻煩,但其實基本照做即可(假設目標分割槽是 /dev/nvme0n1p1 )。大致的操作就是把Snapper建立的 @/.snapshots 刪掉,替換成獨立的子卷 @snapshots

sudo umount /.snapshots
sudo rm -r /.snapshots
sudo btrfs subvolume delete /.snapshots
sudo mkdir /.snapshots
sudo mount -o subvol=/ /dev/nvme0n1p1 /mnt
sudo btrfs subvolume create /mnt/@snapshots

然後修改 /etc/fstab ,參考子卷的方式增加一條將子卷 [email protected] 掛載到 /.snapshots 的即可。執行 sudo mount -a 生效。

3. Snapper配置

首先是配置定時快照與定時清理。編輯 /etc/snapper/configs/root (也可以通過GUI配置: sudo snapper-gui ),可以參考這個配置:

TIMELINE_MIN_AGE="1800"
TIMELINE_LIMIT_HOURLY="0"
TIMELINE_LIMIT_DAILY="2"
TIMELINE_LIMIT_WEEKLY="4"
TIMELINE_LIMIT_MONTHLY="4"
TIMELINE_LIMIT_YEARLY="1"

然後啟用定時任務:

sudo systemctl enable --now snapper-timeline.timer
sudo systemctl enable --now snapper-cleanup.timer

然後需要配置允許非root使用者建立、管理快照。也是先編輯 /etc/snapper/configs/root ,在 ALLOW_USERS 裡增加自己的使用者( whoami )。然後修改快照目錄的許可權:

sudo chmod 0750 /.snapshots/
sudo chown :使用者 /.snapshots/

重啟生效。

4.openSUSE風格特有配置

主要的操作是把系統轉移進某一個可寫的快照。

  1. 為了使用 manjaro-chroot ,安裝包 manjaro-tools-base (執行 sudo pacman -S manjaro-tools-base )。其實不裝也行,把指令換成 chroot 也一樣。
  2. 編輯 /etc/fstab ,把掛載根目錄一行的 subvol=/ 去掉;參考子卷的方式增加一條將子卷 subvol=/@/.snapshots 掛載到 /.snapshots 的配置。
  3. 建立一個快照作為當前的系統快照: snapper create --read-write --type single -d "Init snapshot" 。在指令輸出中應該可以看到建立快照的快照號,把它設定為當前快照: sudo btrfs subvolume set-default /.snapshots/快照號/snapshot
  4. 更新一下Grub,讓系統啟動到這個快照: manjaro-chroot /.snapshots/快照號/snapshot bash -c "sudo grub-install && sudo update-grub"
  5. 重啟。然後看看 findmnt / 裡面顯示的子卷是不是設定的那個快照號。
  6. 如果是,那就來一個刺激的狠活。先掛載它 sudo mount -o subvol=/ /dev/nvme0n1p1 /mnt ,然後進行一個巨大刪除 sudo rm -rf /mnt/@/* (也可以手動刪除,不過千萬別把 .snapshot 刪了)。

5.配置Grub啟動到快照的選單

安裝 grub-btrfs 包( sudo pacman -S grub-btrfs ),執行 sudo systemctl edit grub-btrfs.path 並修改為:

[Unit]
Description=Monitors for new snapshots of snapper
DefaultDependencies=no
Requires=
After=
BindsTo=

[Path]
PathModified=/.snapshots

[Install]
WantedBy=multi-user.target

然後執行即可: sudo systemctl enable --now grub-btrfs.path

快照回滾

之前提到過,Snapper的快照回滾並不是一樁易事。不過這些問題都可以通過寫指令碼來解決。

Arch Linux風格

先來看看手動的方式吧(假設分割槽是 /dev/nvme0n1p1 ):

sudo mount /dev/nvme0n1p1 /mnt
sudo btrfs subvolume snapshot /mnt/@ /mnt/@bad
sudo btrfs subvolume delete /mnt/@
sudo btrfs subvolume snapshot /mnt/@snapshots/要恢復的快照號/snapshot /mnt/@

要恢復的快照號需要手工檢查,如果在快照中可以通過 snapper ls 檢視。如果在Live CD中,可以通過精準的猜測 和一點運氣和奇蹟 逐個檢視快照資料夾內的 xml 得到。

由於過程確實很繁瑣,於是我就寫了一個指令碼,建議儲存在 /usr/local/bin/rollback ,這樣就可以 rollback 快照號 了。指令碼會建立一個快照,然後進行恢復操作。如果當前不在快照,還會建立 @old 子卷儲存當前系統,需要重啟後手動刪除。當然,這個指令碼 可以在Live CD裡使用。

#!/bin/sh
set -e
if [[ x"$1" == x ]]; then
  echo "No snapshot number given." 1>&2
  echo "Usage: rollback [snapshot to rollback]" 1>&2
  exit 1
fi
root_dev=`findmnt -n -o SOURCE / | sed 's/\[.*\]//g'`
root_subvol=`findmnt -n -o SOURCE / | sed 's/.*\[\(.*\)\].*/\1/'`
echo ">= Rollback to #$1 on device $root_dev"
# create snapshot before
sudo snapper create --read-only --type single -d "Before rollback to #$1" --userdata important=yes
sudo mount -o subvol=/ $root_dev /mnt
# check enviornment
if [[ x"$root_subvol" == x/@ ]]; then
  echo "Warning: Not run in a snapshot, a subvolume @old will be created. You should consider remove it after reboot." 1>&2
  if [[ -d /mnt/@old ]]; then
    echo "Found last @old, remove it."
    sudo btrfs subvolume delete /mnt/@old >/dev/null
  fi
  sudo mv /mnt/@ /mnt/@old
else
  sudo btrfs subvolume delete /mnt/@ >/dev/null
fi
sudo btrfs subvolume snapshot /mnt/@snapshots/$1/snapshot /mnt/@ >/dev/null
sudo umount /mnt

openSUSE風格

如果你在用openSUSE,那啟動進一個快照後執行 snapper rollback 就完事了,但可惜我們並不是openSUSE。因為正常的Grub並不能在不更新的情況下啟動到預設子卷,因此只能手動更新它了。同樣,用一個指令碼來完成這些操作,同樣建議儲存在 /usr/local/bin/rollback 。不過就不需要手動指定快照號了,直接 rollback 即可。

#!/bin/sh
set -e
root_dev=`findmnt -n -o SOURCE / | sed 's/\[.*\]//g'`
sudo snapper rollback $1
echo ">= Update GRUB on $root_dev"
sudo mount $root_dev /mnt
if [[ -d /boot/efi ]]; then
  manjaro-chroot /mnt grub-install --target=x86_64-efi --efi-directory=/boot/efi --bootloader-id=manjaro --recheck
else
  read -p "Enter MBR device (e.g. /dev/sda): " install_dev
  manjaro-chroot /mnt grub-install --force --target=i386-pc --recheck --boot-directory=/boot $install_dev
fi
manjaro-chroot /mnt update-grub
sudo umount /mnt

同樣,指令碼 能在Live CD裡用。此外,雖然如今PC分割槽一般都不會是MBR格式,但如果真是則還需要手動輸入引導裝置。

openSUSE風格在Live CD中的快照回滾

還是基於之前的原因,本來很簡單的操作因為Grub的緣故還要增加一步更新操作。此外,Live CD中就需要指定快照號了,確定方法和Arch的一樣。不過還好,一般的小場面也用不到Live CD。

sudo mount /dev/nvme0n1p1 /mnt
sudo btrfs subvolume set-default /mnt/@snapshots/快照號/snapshot
manjaro-chroot /mnt grub-install --target=x86_64-efi --efi-directory=EFI資料夾 --bootloader-id=manjaro --recheck

成功進去系統後建議執行一次 rollback

Troubleshooting

如果建立快照、列出快照時卡頓

真的巨卡無比,建議執行 sudo btrfs quota disable / 就沒事了。

rEFInd支援快照啟動

類似Timeshift,理由、解決方案也一樣。

Reference

  1. http://forum.manjaro.org/t/howto-btrfs-and-snapper/25417
  2. http://wiki.archlinux.org/title/snapper
  3. http://bbs.archlinux.org/viewtopic.php?id=194491
  4. http://www.reddit.com/r/openSUSE/comments/l015pb/snapper_rollback_problem/
  5. http://forum.manjaro.org/t/btrfs-snapper-the-suse-way-with-rollback/52279
  6. http://wiki.manjaro.org/index.php/Restore_the_GRUB_Bootloader
  7. http://github.com/teejee2008/timeshift/issues/606