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 のマウントの際などに使える。
圧縮
透過的に圧縮ができる。zlib
、lzo
、zstd
を選択できる。今だと lzo
か zstd
を選んでおくのが良さそう。ここにベンチマークの結果がある。
マウントオプションで compress=lzo
や compress=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}
を与える。subvolid
は btrfs 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
を置き換えるのは関数では難しく、妥協している
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 は調べれば調べるほど色んな機能が出てきて驚かされる。
まだ試せてない機能が他にもあるので、機会をみつけて触っていきたい。