Btrfs メモ

Btrfs についていろいろ調べたので使い方のメモ。

参考ページ:

このメモの内容はほとんど参考ページのままなので、そちらを見た方がよさそう。

Copy on Write

Btrfs では CoW (Copy on Write) が使える。ファイルを単にコピーしただけでは容量を消費せず、ファイルを書き換えた際に初めてディスク領域を消費する。

ただしこれはただ cp しただけではダメで、cp --reflink=always(常に CoW) や cp --reflink=auto(可能なら CoW) を使う必要がある。

そのため、私は以下のようなラッパコマンドを用意して PATH に置いた。

#!/bin/sh -
exec /bin/cp --reflink=auto "$@"

シェルの alias を使う手も考えられるが、コマンドごと置き換えることで例えば make install 時の cp の挙動も変えられるので、コマンド自体を上書きすることにした。

無効化

逆に CoW を無効にしたい場合もある。データベースが扱うファイルのように、1つのバイナリファイルを何度も上書きするような使い方をする場合、CoW と相性が悪いらしい。

ディレクトリやファイル単位で CoW を無効にする場合は以下のように chattr を使う。

$ chattr +C </dir/file>

また、マウント時に nodatacow オプションを指定することで対象を丸ごと CoW 無効にできる。後述する subvolume のマウントの際などに使える。

圧縮

透過的に圧縮ができる。zliblzozstd を選択できる。今だと lzozstd を選んでおくのが良さそう。ここベンチマークの結果がある。

マウントオプションで compress=lzocompress=zstd を指定する。compress=zstd:3 のようにすることで、圧縮レベルも指定できる。zstd の場合は 1 から 15 で、デフォルトは 3。

また、一緒に space_cache も指定するとよい。これは新規の割り当て領域を毎回探すのではなく、予めある程度キャッシュしてくれる。

圧縮は、圧縮を有効にした後に作成されたファイルにしか適用されない。既存のファイルにも適用し直すには以下のコマンドを使う。

$ btrfs filesystem defragment -r -v -czstd /

これは / から辿って再帰的(-r)に処理をするが、後述の subvolume や別の FS をマウントしている箇所等には適用されないので注意。

また、後述する snapshot を作っている場合も注意。全部のファイルを書き出し直しているのと同じになるので、snapshot とファイルを共有できずに容量を食ってしまう。

subvolume

ファイルツリーの単位として subvolume というものを作れる。見た目は普通のディレクトリのように見える。

$ btrfs subvolume create my-subvolume
Create subvolume './my-subvolume'

$ ls
my-subvolume

snapshot

subvolume 単位で snapshot を取れる。一瞬で作れる。

$ btrfs subvolume snapshot my-subvolume my-snapshot
Create a snapshot of 'my-subvolume' in './my-snapshot'

$ ls -1
my-snapshot
my-subvolume

CoW により、余計な容量を取らない。お手軽なバックアップに使える。

-r オプションを使うことで、read only な snapshot を作ることもできる。せっかく取ったバックアップを誤って書き換えてしまわないようにできるので便利だ。read only かどうかは後から変更もできる。

subvolume は入れ子にすることができるが、その場合、それぞれ独立した subvolume ということになる。snapshot を作った場合、入れ子になった subvolume までは snapshot の対象にはならない。

$ btrfs subvolume create my-subvolume/nested-subvolume
Create subvolume 'my-subvolume/nested-subvolume'

$ echo file1 > my-subvolume/file1.txt
$ echo file2 > my-subvolume/nested-subvolume/file2.txt
$ btrfs subvolume snapshot my-subvolume my-snapshot2
Create a snapshot of 'my-subvolume' in './my-snapshot2'

$ ls -1 my-snapshot2
file1.txt
nested-subvolume

$ ls -1 my-snapshot2/nested-subvolume
$

ネストした subvolume があった場所には空のディレクトリがあるが、中身はない。

subvolume のマウント

subvolume は、それ自体を通常のファイルシステムのようにマウントすることができる。マウントオプションに subvol={path}subvolid={id} を与える。subvolidbtrfs subvolume list で確認できる。

これにより、構成を工夫することで、本来のファイルシステムのルートとは別のところをルートディレクトリとして使い、

subvolume + snapshot を使って簡易タイムマシーン的な構成を作る

定期的に snapshot を取って、何かあったときに古いファイルを取り出せるようにしてみる。Mac のタイムマシーン、あるいは富豪的ゴミ箱みたいなイメージ。

構成

ArchWiki の Snapper のページにある推奨ファイルシステムレイアウトをほぼそのまんま採用した。

subvolid=5
   |
   |- @ (subvol)        ← / (root) をマウント
   |  |- /.snapshots    ← /@snapshots をマウント
   |  |- /usr
   |  |- /bin
   |  |
   |  |- ...
   |  |
   |  |- /home          ← 別パーティション
   |  |
   |  `- var/
   |     |- lib/
   |     |  |- docker/  ← /@var/lib/docker をマウント
   |     |  |- mysql/   ← /@var/lib/mysql をマウント
   |     |- log/        ← /@var/log をマウント
   |     |- tmp/        ← /@var/tmp をマウント
   |
   |- @snapshots/ (subvol)
   |  |- ... (snapshot)
   |
   `- @var/
      |  |- docker/ (subvol)
      |  |- mysql/  (subvol)
      |- log/ (subvol)
      `- tmp/ (subvol)

ファイルシステムの本当のルート(subvolid=5)を直接使わず、その直下の @ をシステムのルートディレクトリとしてマウントする。

log や tmp など、バックアップ対象にしたくない箇所はそれぞれ subvolume として外に出しておき、マウントする。

特定の snapshot に戻したい場合は、ArchWiki にまんま書いてあるが、USB で別システムでブートし、subvolid=5 をマウントしたあと、@ を削除 or 退避させて戻したい snapshot の snapshot を @ に作ればよい。

/home 以下も概ね同じだが、私はすでに /home/thinca を作ってしまっており、これをまるごと subvolume に変換する必要があったので以下の手順を行った。直接変換するようなコマンドはなく、cp するしかないらしい。

# cd /home
# btrfs subvolume create new
# cp -a -p -T --reflink=always thinca/ new/
# mv thinca old
# mv new thinca

snapshot の作成

snapshot を置いておく場所は用意できたので、ここに何かがあった時や定期的に snapshot を作っていきたい。

この手のことをするプログラムは、やはりみんな欲しいらしく結構な数が作られている模様。さきほどちらっと出た Snapper もその 1 つ。

試しに Snapper を使おうとしてみたが、動かし方がよくわからず…設定ファイルを書いてみても認識してくれない。

元々大したことをやろうというわけでもないので、やりたいことをやるスクリプトを書いてみた(たぶんこういうノリで謎スクリプトが量産されたのだろう…また1つ増えてしまった…)。

https://github.com/thinca/bsnap

使い方はざっくり README に書いたが、これで、//.snapshots に、/home/thinca/home/.snapshots/thinca に、それぞれ daily, weekly, monthly で snapshot を取る設定を書いた(weekly や monthly は記事執筆時点でまだ発動してないが…)。

例えば以下は / に関する daily の設定ファイル。形式は Bash script で、単にスクリプト変数を定義しているだけである。

# スナップショットのグループ
group=daily

# スナップショットを置くディレクトリ
snapshots_dir=/.snapshots

# スナップショットを取る対象のディレクトリ
target=/

# 最新のいくつを残すか
leave_count=30

# スナップショット作成時に cleanup を同時に行う
auto_cleanup=yes

これを /etc/bsnap/root-daily に置く。同じように home-daily やら root-weekly やらを置く。

で、これを定期的に実行するために systemd の service を作る。

# /etc/systemd/system/bsnap-create@.service

[Unit]
Description=Create a snapshot by bsnap

[Service]
Type=oneshot
ExecStart=/usr/local/bin/bsnap -c %i create

[Install]
WantedBy=default.target

この service を定期的に実行するために、timer を用意する。timer は daily、weekly、monthly で動かそうと思うので、それぞれのタイミングで発動するやつ、計3つ用意する。以下は daily のもの。

# /etc/systemd/system/bsnap-create-daily@.timer

[Unit]
Description=Create a snapshot by bsnap in daily

[Timer]
OnCalendar=04:00
Unit=bsnap-create@%i.service

[Install]
WantedBy=timers.target

あとは timer を有効にすれば、定期的に snapshot を取ってくれる。設定に従って古いものは自動的に削除してくれる、はず。

# 以下を、root/home それぞれと daily/weekly/monthly それぞれで計 6 個有効にする
$ sudo systemctl start bsnap-create-daily@root-daily.timer
$ sudo systemctl enable bsnap-create-daily@root-daily.timer

rm 時に snapshot を作成

また、rm を関数で wrap して、削除前に snapshot を取るようにした。

  • 一般ユーザー向けなので $HOME 以下のファイルを消そうとした場合のみ
    • 専用の home-rm という設定を作っておく
    • その場合は -fr がなくてもディレクトリを消せるようにする
      • こうしておくと、常に -fr なしでもファイルもディレクトリも消せる。そういう癖を付けておけば、うっかりゴミ箱がない環境で手軽にディレクトリを消そうとしたときに一旦エラーで止まって一呼吸置けるようになる
  • $HOME の中と外のファイルを同時に消そうとするとまずいが、そうそうしないと思うので目を瞑る
  • それ以外にも完璧に rm を置き換えるのは関数では難しく、妥協している
if [[ -e /etc/bsnap/home-rm ]]; then
  rm() {
    local arg snap=no will_err=no paths=()
    for arg in "$@"; do
      if [[ "${arg[1]}" != "-" ]]; then
        paths+=("${arg}")
        if [[ ! -e "${arg}" ]]; then
          will_err=yes
        elif [[ "$(realpath ${arg})" =~ ^"${HOME}" ]]; then
          snap=yes
        fi
      fi
    done
    if [[ "${snap}" == "yes" && "${will_err}" == "no" ]]; then
      bsnap -c home-rm create "$(basename "${paths[1]}")"
      command rm -fr "${paths[@]}"
    else
      command rm "$@"
    fi
  }
fi

雑感

Btrfs は調べれば調べるほど色んな機能が出てきて驚かされる。

まだ試せてない機能が他にもあるので、機会をみつけて触っていきたい。