CentOS で aufs (another unionfs) を使う

  • フラッシュメモリバイスなディスクに Linux をインストールしたい
  • ⇒ 書き換え限度回数が心配だよ
  • ⇒ CDROM bootable な OS にすればいいよ
  • ⇒ システムの変更やアップデートのときが面倒だよ
  • ⇒ read only filesystem の上にかぶせることができる UnionFS を使えばいいよ
  • ⇒ UnionFS より aufs のほうがおすすめだよ

ということで,CentOS 5.2 に aufs をいれてみました。ちなみに「やってみた」レベルのお話です。

aufs とは

aufs とは,スタッカブルな「単一化」ファイルシステムです。まぁーつまり,単一ファイルツリーに複数の「ブランチ」を透過的に重ね合わせることができます。


KNOPPIX 5.1 以降で使われています。

ビルドする

残念ながら RPM パッケージは用意されてないので,CVS で最新版をダウンロードして自分でビルドする必要があります。CVS といっても作者によると常に安定版をアップロードするようにこころがけていらっしゃるそうなのでそんなに心配する必要はありません。

CVS によるダウンロードとビルドについては下記記事が参考になります。

CentOS(というか RedHat 系)の kernel 2.6.18 では,ファイルシステムのソースから i_blksize がなくなっているので,下記のようなパッチを当てる必要があります。

--- fs/aufs/cpup.h.orig 2008-04-14 08:38:24.000000000 +0900
+++ fs/aufs/cpup.h      2008-07-11 12:51:26.000000000 +0900
@@ -34,7 +34,7 @@
 static inline
 void au_cpup_attr_blksize(struct inode *inode, struct inode *h_inode)
 {
-#if LINUX_VERSION_CODE < KERNEL_VERSION(2, 6, 19)
+#if 0 && LINUX_VERSION_CODE < KERNEL_VERSION(2, 6, 19)
        inode->i_blksize = h_inode->i_blksize;
 #endif
 }

あとはビルドすればいいのですが,一応 local.mk にあるビルド時オプションを下記のようにいじっておきましょう。

export CONFIG_AUFS_HINOTIFY = y

ビルド時オプションの CONFIG_AUFS_HINOTIFY については aufs のマニュアルや下記記事を参照してください。

kernel ソースツリーに組み込むには,make -f local.mk kconfig してから,指示されたとおりにあれやこれややる必要があるのですが,カーネルモジュールとしてビルドするには,下記のようにするだけでよいです。

$ make -f local.mk

ビルドできたら,適宜ディレクトリにコピーします。

$ sudo mkdir /lib/modules/`uname -r`/kernel/fs/aufs

$ sudo cp aufs.ko /lib/modules/`uname -r`/kernel/fs/aufs

$ sudo depmod -a

周辺ツールもついてくるのですが,普通に使うぶんにはカーネルモジュール単体をコピーするだけで十分です。depmod -a すればリブートせずとも aufs が使えるようになります((depmod -a しただけでは lsmod には現れませんが,mount すれば自動的にロードしてくれます。))。

ためしに使ってみる

まず,下記のような2つのディレクトリがあったとして,

$ ls /tmp/a
1

$ ls /tmp/b
2  3

read-only な /tmp/b/tmp/a を read-write で重ねて,/tmp/c にマウントしてみましょう。

$ sudo mount -t aufs -o br:/tmp/a:/tmp/b=ro none /tmp/c

aufs ではマウント元のファイルシステムを指定する必要はありません(ので none としています)。マウントするディレクトリ等はすべてオプションで指定します。

オプションの br: というのは「ブランチ」の略です。上記設定は,

  • 最上位階層の branch として /tmp/a を指定
    • オプションはデフォルトで rw (read-write)
  • 第2階層の branch として /tmp/b を指定
    • オプションは ro (read-only)

になります。これを /tmp/c にマウントする,ということです。

なお,ro というのは,あくまで aufs からみて read only にするというだけの意味です。この例では,直接 /tmp/b ディレクトリにファイルを追加・削除することはできます((ただし UDBA で inotify を指定しないと /tmp/c に反映されません。))。

重ね合わせた結果は,

$ ls /tmp/c
1  2  3

のように,/tmp/a/tmp/b 下のファイルが両者とも見えています。

では,/tmp/c に新しいファイルを作ってみましょう。

$ touch /tmp/c/4

$ ls /tmp/c
1  2  3  4

$ ls /tmp/a
1  4

実際には rw な最上位階層の /tmp/a に新しいファイルが作られました。

ro な下階層のファイルを削除したらどうなる?

/tmp/c/3 というファイルは /tmp/b branch のものが見えているわけですが,この read only な 3 を削除するとどのようになるでしょうか。

$ rm /tmp/c/3
rm: remove regular empty file `/tmp/c/3'? y

$ ls /tmp/c
1  2  4

$ ls /tmp/a
1  4

$ ls /tmp/b
2  3

/tmp/c から 3 がなくなりました。しかし,マウント元の /tmp/b には依然残っています。

このように read only branch のファイル削除情報は,上位階層の branch に「ホワイトアウト(whiteout)」ファイルとして残っています。

$ ls -a /tmp/a
.  ..  1  4  .wh.3  .wh..wh.aufs  .wh..wh.plink

.wh.3 というのが,「3 というファイルがあるけど whiteout してね」という意味になるわけです。

この whiteout はアンマウントしても rw branch に残っているので,再マウントするとふたたび見えなくなります。

$ sudo umount /tmp/c

$ sudo mount -t aufs -o br:/tmp/a:/tmp/b=ro none /tmp/c

$ ls /tmp/c
1  2  4

ハードリンクはどうなる?(Pseudo Link)

branch 間でハードリンクを貼ってみましょう。

$ ln /tmp/c/2 /tmp/c/5

$ ls /tmp/c
1  2  4  5

inode 番号をみてみると,

$ ls -li /tmp/c
total 0
11 -rw-r--r-- 1 root root 0 Jul 14 10:43 1
14 -rw-r--r-- 2 root root 0 Jul 14 10:43 2
15 -rw-r--r-- 1 root root 0 Jul 14 10:44 4
14 -rw-r--r-- 2 root root 0 Jul 14 10:43 5

たしかに,25 は同じ inode 番号を示しています。

各ブランチの inode 番号をみてみると,

$ ls -li /tmp/a
total 0
324916 -rw-r--r-- 1 root root 0 Jul 14 10:43 1
324921 -rw-r--r-- 1 root root 0 Jul 14 10:44 4
324924 -rw-r--r-- 2 root root 0 Jul 14 10:43 5

$ ls -li /tmp/b
total 0
324917 -rw-r--r-- 1 root root 0 Jul 14 10:43 2
324918 -rw-r--r-- 1 root root 0 Jul 14 10:43 3

このように違う番号になっています。このような branch 間ハードリンク情報はメモリ内に格納されています。

では 2 の内容を変更してみましょう。

$ echo 'Hello!' >> /tmp/c/2

$ cat /tmp/c/2
Hello!

$ cat /tmp/c/5
Hello!

$ ls -li /tmp/c
total 8
11 -rw-r--r-- 1 root root 0 Jul 14 10:43 1
14 -rw-r--r-- 2 root root 7 Jul 14 10:45 2
15 -rw-r--r-- 1 root root 0 Jul 14 10:44 4
14 -rw-r--r-- 2 root root 7 Jul 14 10:45 5

依然 25 の実体は同一のままですが,内的には……

$ ls -li /tmp/a
total 8
324916 -rw-r--r-- 1 root root 0 Jul 14 10:43 1
324924 -rw-r--r-- 3 root root 7 Jul 14 10:45 2
324921 -rw-r--r-- 1 root root 0 Jul 14 10:44 4
324924 -rw-r--r-- 3 root root 7 Jul 14 10:45 5

$ ls -li /tmp/b
total 0
324917 -rw-r--r-- 1 root root 0 Jul 14 10:43 2
324918 -rw-r--r-- 1 root root 0 Jul 14 10:43 3

最上位 branch に新しい 25 が生成されています。

このように copy-on-write 的に働きます。が,さきほど述べたように,on-write する前は,メモリ上に情報が格納されているだけですので,アンマウントすると,branch 間ハードリンク情報がロストします。このことの対処法や Pseudo Link についてのより詳しい情報は aufs のマニュアルPseudo Link (hardlink over branches) 項を参照してください。

aufs で可能なこと,不可能なこと

これまでの例では,各階層の branch とマウント先ディレクトリは別物として指定していましたが,各 branch 自身にマウントすることもできます。

たとえば,最上位階層の rw な branch にマウントしてみます。

$ sudo mount -t aufs -o br:/tmp/a:/tmp/b=ro none /tmp/a

$ ls /tmp/a
1  2  3

$ touch /tmp/a/4

$ ls /tmp/a
1  2  3  4

$ umount /tmp/a

$ ls /tmp/a
1  4

うまく動いています。

また,下位階層の ro branch にマウントすることもできます。

$ sudo mount -t aufs -o br:/tmp/a:/tmp/b=ro none /tmp/b

$ ls /tmp/b
1  2  3

$ touch /tmp/b/4

$ ls /tmp/b
1  2  3  4

$ umount /tmp/b

$ ls /tmp/b
2  3

$ ls /tmp/a
1  4

これもうまく動きますね。先ほどの例と異なり,こちらは read only なディレクトリツリーに writable な階層を導入できるので,なかなか有用です。


いっぽう,すでに aufs でマウントされたツリーを下位階層としてマウントすることはできません((ビルド時に CONFIG_AUFS_ROBRy にしておくと,read only branch としてはマウントできるようになります。))。/tmp/c ディレクトリが既に aufs でマウントされているとして,

$ mount -t aufs
none on /tmp/c type aufs (rw,br:/tmp/a:/tmp/b=ro)

$ sudo mount -t aufs -o br:/tmp/d:/tmp/c=ro none /tmp/e
mount: wrong fs type, bad option, bad superblock on none,
       missing codepage or other error
       In some cases useful info is found in syslog - try
       dmesg | tail  or so

$ dmesg | tail -1
aufs test_add:353:mount[4536]: nested aufs /tmp/c

このように nested aufs として怒られてしまいます。

また,下位ディレクトリを下位階層 branch として指定することもできません。

$ sudo mount -t aufs -o br:/tmp/f/a:/tmp/f=ro none /tmp/f
mount: wrong fs type, bad option, bad superblock on none,
       missing codepage or other error
       In some cases useful info is found in syslog - try
       dmesg | tail  or so

$ dmesg | tail -1
aufs test_add:390:mount[4608]: /tmp/f is overlapped

このように overlapped として怒られます*1。なお,下位ディレクトリにある loopback file を branch として指定しようとしても,ちゃんと検出して同じように怒られます。

CentOS 5.2 で root filesystem を Read Only にして運用してみる

いよいよ実用的に使ってみましょう。

ファイルシステムを read only mount して,書き換えの可能性がある /var ディレクトリと /tmp ディレクトリ((よくよく考えたら /tmp ディレクトリは直接 tmpfs マウントすればよかったですね。))を aufs でマウントしてみます。

まず ramdisk として用いる tmpfs 用ディレクトリを掘っておきます。

# mkdir -p /memdisk/var /memdisk/tmp

/etc/fstab は以下の通りです。

LABEL=/         /        ext3   defaults,ro    1 1
LABEL=/boot     /boot    ext3   defaults,ro    1 2
tmpfs           /dev/shm tmpfs  defaults       0 0
devpts          /dev/pts devpts gid=5,mode=620 0 0
sysfs           /sys     sysfs  defaults       0 0
proc            /proc    proc   defaults       0 0
LABEL=SWAP-hda2 swap     swap   defaults       0 0

tmpfs /memdisk/var tmpfs  defaults        0 0
tmpfs /memdisk/tmp tmpfs  defaults        0 0
none  /var         aufs   defaults,br:/memdisk/var:/var=ro 0 0
none  /tmp         aufs   defaults,br:/memdisk/tmp:/tmp=ro 0 0

サンプル環境ということで,基本 //boot(と swap)しかパーティションを切っていないです。両者を ro としています。そして tmpfs/memdisk/var/memdisk/tmp を作成し*2,aufs で両者をマウントしています。

リブートしてみたら,うまく動きました。ramdisk の内容をみてみると,

# ls /memdisk/var/
empty  lib  lock  log  run  spool

# ls /memdisk/tmp/
ssh-oFxtlY1998

たしかにこちらに読み書きしているみたいです。シャットダウン時に file system busy で怒られる(ため時間がかかる)のですが,実害はおそらくないので無視しています。

より実用的に使うには,rc.sysinitrc0.drc6.d あたりにマウント・アンマウント処理を埋め込むべきでしょう。状況によってはアンマウント時に ro branch に write back するようにするとかっこいいかも。

*1:これができたら便利だったのに!。ループしてしまうので仕方ありませんけどね。

*2:ほんとは option で最大サイズを指定しておいたほうがいいですが,サンプルなので。