RAMをキャッシュにしてbcacheを使ってみる

この前のUSBメモリでLinuxを運用するときのtmpfsなどの設定(永続化のためのsystemdの設定とか) - turgenev’s blogの記事では、USBメモリLinuxの高速化のためにtmpfs(RAM上のファイルシステム)を使う方法を紹介しました。最後のほうでは、まるごとRAMを使うのではなくUSBメモリのキャッシュとしてうまい具合にRAMを使ってくれる方法を検討しましたが、検討するだけで終わっていました。

今回はそのとき言及したbcacheについて、実際にRAMをキャッシュとして動かしてみたので設定方法を紹介します。

前回記事で書いた通り、RAMだけにキャッシュされている状態が長時間続くというのはトラブルで異常終了するとバック側(USB)まで含めてデータの整合性が取れなくなる(ファイルシステムが壊れる)ことを意味するので、非常にリスクが高くなります。(writebackモードを使わなければいいのですが、それだと書き込み性能が出ません)

通常の用途では、キャッシュ側も不揮発性のSSDを使い、しかも電源トラブルにも対応できるものをできれば複数台使うのが推奨されているくらいです。あくまで実験用と考えてください。

USB側でのパーティションの作成

今回はバッキングデバイス(メインのデータ保管場所)としてUSBメモリを使用します。実験用ということでUSBメモリの一部のみ4GBくらいを使うことにします。そこでパーティションを作成します。具体的なファイルシステムとして使うわけではないのでGPartedの「未フォーマット」を使って作成しました。(これの意味するところは知りません)(後述のwipefsコマンドを使うとこの状態になりそう)

以下、/dev/sda3に「未フォーマット」の4GBの領域を作ったものとして説明します。

RAM上のブロックデバイスの準備

以降、root権限が必要なコマンドがほとんどなのでrootシェルで実行することを想定します。

modprobe brdでRAM上ブロックデバイス(/dev/ram0など)が使えるようになります。/devをみてもいいですが、lsblk -aと打つとサイズ含め一覧表示されるのでわかりやすいです。手元ではデフォルトで64MBのサイズになりました。このブロックデバイスは通常のディスクと同等で、中にパーティションを作成して使うこともできます(/dev/ram0p1のような名前になる)。

bcacheではディスク(/dev/sdaなど)でもパーティション(/dev/sda1など)でも同じようにストレージとして使うことができるので、今回はわざわざパーティションを作る必要はありません。

bcacheデバイスの作成と登録

sudo modprobe bcacheでカーネルモジュールを読み込んでおきます(この段階で必要だったか覚えていませんがカーネルモジュールがあるかどうかの確認も兼ねて)。

次にbcache-toolsパッケージをaptなどでインストールします。

make-bcacheコマンドが使えるようになるので、まずバッキングデバイス側を作成します。

make-bcache -B /dev/sda3

-Bはバッキングデバイスを意味します。これで/dev/sda3がbcacheでフォーマット(?)され、さらに(udevによって?)自動でbcacheデバイスとして登録されて/dev/bcache0が作成されます。lsblkをするとsda3の下にbcache0がぶら下がっているのがわかると思います。

ここまでの内容をもとに戻したいときは、まずbcache0をストップさせてからwipefsで/dev/sda3をリセットします。

echo 1>/sys/block/bcache0/bcache/stop
wipefs -a /dev/sda3

ちなみにwipefsはメタ情報に関する部分だけ消してくれるようで、中のデータは消えません(bcacheを作り直せば同じデータが見えるようになります)。

次にキャッシュデバイス側を作成します。-Cオプションを使います。

make-bcache -C /dev/ram0 --bucket 64k

出力は以下のようになります。

UUID:            9bffb899-f12e-4932-9ab1-d8bd1f36369d
Set UUID:        72e46772-1c15-4c4f-bef6-1ad16b9db565
version:        0
nbuckets:        1024
block_size:        1
bucket_size:        128
nr_in_set:        1
nr_this_dev:        0
first_bucket:        1

注意点として、このnbucketsのところが1024より少ないとこの後のコマンドがうまくいかないです(ソースコード)。nbucketsというのはこのデバイスの中にbucketがいくつあるかというのを表します。bucketのサイズはここでは128と出ていますが、これは「/dev/ram0のセクタサイズの何倍であるか」を表します。セクタサイズというのはどのデバイスでもほとんど512か4Kのどちらかで、/dev/ram0では512でした。したがってbucketのサイズはバイトで表すと512x128=64Kということになります。/dev/ram0のサイズは64Mだったのでbucketが1024個入ることになります。ちなみに--bucketは何も指定しないと128kになります。

/dev/sda3の場合と違い、/dev/ram0はudevによる自動登録の対象ではない(?)ので手動でbcache用に登録する必要があります。(つまりRAMではなくSSDなどを使った場合はこの操作は不要)(逆に/dev/ram0をバッキングデバイスとして使う場合は同じことをする必要がある)

echo /dev/ram0 > /sys/fs/bcache/register

これでls /sys/fs/bcacheとすると72e46772-1c15-4c4f-bef6-1ad16b9db565/(さっきのmake-bcache -CのSet UUIDと同じ)というフォルダが作成されているのがわかると思います。nbucketsが1024より小さいと作成されません(このコマンドの出力にエラーメッセージは出ませんが、dmesg | grep bcacheとするとnbuckets is too smallと出ているのがわかります)。ここにはバッキングデバイスは表示されません。逆にキャッシュデバイスを作成しても/dev/bcacheXは作成されず、lsblkにも出ません。

このキャッシュデバイス関連の操作をもとに戻したいときは、まずデバイスを停止(?)してからwipefsをします(stopじゃなくてunregisterでもいける?)。

echo 1>/sys/fs/bcache/72e46772-1c15-4c4f-bef6-1ad16b9db565/stop
wipefs -a /dev/ram0

(バッキングデバイスも含めて)このへんで何かうまくいかないときはecho 1 > /sys/fs/bcache/pendings_cleanupをしてwipefsをして最初からやり直すとうまくいくことがあります。

キャッシュデバイスをアタッチする

ここまではバッキングデバイスとキャッシュデバイスを別々に操作しており、互いに関連づけられてはいません。以下のコマンド

cat /sys/block/bcache0/bcache/state

を実行すると「no cache」と返ってきます。

そこで、キャッシュデバイスをバッキングデバイス側(から作成された/dev/bcache0)にアタッチします。

echo 72e46772-1c15-4c4f-bef6-1ad16b9db565 > /sys/block/bcache0/bcache/attach

このとき72e46772-1c15-4c4f-bef6-1ad16b9db565//sys/fs/bcacheに存在しないとNo such file or directoryと言われます。

これでcat /sys/block/bcache0/bcache/stateがclean(キャッシュ登録済かつキャッシュに何もない)になったと思います。この状態でlsblk -aをすると/dev/ram0のほうにもbcache0がぶら下がっています。

同じディレクトリのattachではなくdetachに書き込むと登録解除できます。

実はmake-bcacheコマンドでバッキングデバイスとキャッシュデバイスを一気に作成することもでき、そうするとこのアタッチ操作が自動で行われます。ただし前述の通りram0のregisterは手動でやる必要があります(それをやるとno cacheからcleanに変わる)。つまりまとめると以下のコマンドを実行すればOKです。

make-bcache -B /dev/sda3 -C /dev/ram0 --bucket 64k
echo /dev/ram0 > /sys/fs/bcache/register

パーティション作成、マウント

/dev/bcache0は普通のブロックデバイス(ディスクと同等)なのでGPartedのようなパーティション編集ソフトでパーティションテーブルやパーティション(/dev/bcache0p1のような名前になる)の作成を行うことができます。マウント/アンマウントも普通のパーティションと同様です。普通のumountでうまくいかないときはumount -l(lazy unmount)だとできることがあります。

ライトバックモードへの変更、パフォーマンスの確認

以下のように、デフォルトではwrite throughモードになっており、書き込みがキャッシュされません(そのかわり異常終了しても安全)。

# cat /sys/block/bcache0/bcache/cache_mode
[writethrough] writeback writearound none

そこでecho writeback > /sys/block/bcache0/bcache/cache_modeとするとライトバックモードになります。書き込みを行うとさっきのcleanがdirtyになると思います。

キャッシュヒット率などのパフォーマンスは /sys/block/bcache0/bcache/stats_total/cache_hit_ratio などで見ることができます。

その他詳細な設定はhttps://www.kernel.org/doc/Documentation/bcache.txtなどを見てください。

キャッシュをcleanにする

RAMの内容は再起動で消えてしまうので正常終了時にはキャッシュの内容をすべてバッキングデバイスに反映させておく必要があります。これは

echo 0 > /sys/block/bcache0/bcache/writeback_percent

とするとできます。Bcache - ArchWikiによると「1分以内に完了する」と書いてあるんですが、厳密にどれくらいかかるのかよくわかりません。さっきのdirtyがcleanに戻るのを確認するほうが確実かもしれません。

systemdによる自動化

ここまでの内容を起動のたびに行うようにsystemdで設定してみました。systemdのオプションなどに関してはこの前のUSBメモリでLinuxを運用するときのtmpfsなどの設定(永続化のためのsystemdの設定とか) - turgenev’s blogを参照してください。

まずsystemdのファイル(/etc/systemd/system/manage-bcache.service)です。

[Unit]
Description=Manage bcache
DefaultDependencies=no
After=local-fs.target
Conflicts=shutdown.target
Before=systemd-resolved.service systemd-timesyncd.service

[Service]
Type=oneshot
RemainAfterExit=true
ExecStart=/my/bcache.sh start
ExecStop=/my/bcache.sh stop
TimeoutStopSec=infinity

[Install]
WantedBy=sysinit.target

次にシェルスクリプト(/my/bcache.sh)です。

#!/bin/sh

if [ "$1" = "start" ]; then
    modprobe brd rd_nr=4 rd_size=$((1024*256))
    uuid=$(make-bcache -C /dev/ram0 --bucket 64k | grep "Set UUID" | awk "{print \$3}")
    echo /dev/ram0 > /sys/fs/bcache/register
    sleep 1
    echo $uuid > /sys/block/bcache0/bcache/attach
  echo 50 > /sys/block/bcache0/bcache/writeback_percent
    mount /dev/bcache0p1
fi

if [ "$1" = "stop" ]; then
    umount -l /dev/bcache0p1
    echo 0 > /sys/block/bcache0/bcache/writeback_percent
    while true; do
        current_state=$(cat "/sys/block/bcache0/bcache/state")
        if [ "$current_state" = "clean" ]; then
            break
      fi
        sleep 1
    done
    sleep 1
    uuid=$(basename $(readlink -f /sys/block/bcache0/bcache/cache))
    echo $uuid > /sys/block/bcache0/bcache/detach
    # sleep 3
    # echo 1 > /sys/fs/bcache/$uuid/stop
    # sleep 3
    # wipefs -a /dev/ram0
fi

RAMディスクのサイズは少し大きくして256MBにしてみました。writeback_percentも増やしてみました。

終了時はアンマウント後にwriteback_percentを0にしてcleanになるまで待ってからdetachしています。RAMディスクが揮発するので毎回make-bcacheが必要で、そのたびにUUIDも変わるのでちょっと面倒です。ちょくちょくsleepを挟まないとエラーが出ることがありました。コメントアウトしている部分は、シャットダウン時には必要ないです。(起動中に手動でsystemctl stopをするなら要ります)

ルートパーティションなどでの利用

通常のbcacheをルートパーティションで使用することは可能です(Thinkpad x220にUbuntu 16.04をbcache付きでインストール - 脇見運転LinuxでルートファイルシステムをRAID+bcache化する #Ubuntu - Qiitaなど)。ブートの初期段階ではbcacheが使えないため、最低限/boot/とは別の(bcacheでない)パーティションにする必要があるようです。

しかし揮発性であるRAMを使う場合は、先ほどsystemdユニットに書いたような操作をブートの初期段階で行う必要があると思うので、ルートパーティションに使うのは結構難しいのではないかと思います。

それと冒頭でも述べた通り、特にwritebackモードの場合は異常終了時にファイルシステムが破損する可能性が高いのでルートパーティションで使うことはそもそもあまり想定できません。

bcachefsについて

bcache自体はどちらかというと枯れた技術(それでもlvmのdm-cacheよりは新しそう)で、bcachefsというbcacheをもとにしたファイルシステムが最近は注目されているようですが、RAMを使う場合にはbcacheのほうが柔軟な扱いができそうな気がしたのでbcachefsには触りませんでした。