![[Programmer's High]](/community/blog/images/union/pg_high_logo.png)
はじめに
平成21年春のLinux Storage & Filesystems Workshopで、LinuxカーネルにUnionMountという機能を実装することについて話し合われました。
UnionMountとは、かつてのSunOSに実装されていたTranslucent(Transparent)FilesystemやBSDのunion mountに相当する機能で、すでにディスクをマウントしているディレクトリ(マウントポイント)に別のディスクを重ねてマウントし、1つのマウントポイントから2台のディスクを同時に使用可能にするものです。例えばLiveCD(HDDへインストールせずにCD/DVD/FlashのみでLinuxを起動し、使用可能にする環境)などではこの機能を積極的に利用しています。
この記事ではLinuxカーネルにこの機能を実装するこれまでの試みと、前述の話し合いの結果としてLinux Kernel Mailing Listへ投稿されたパッチシリーズについて考察します。
unionの基本機能
mount コマンドによりディスクをマウントし、マウントポイントのディレクトリ以下を参照すると、そのディスクに作成済みのファイルシステムのみが見えるというのが通常のマウントです。簡単な例を挙げてみましょう。
まず、2台のディスク /dev/sda 、 /dev/sdb それぞれにext3を作成し、ファイルを適当に作成しておきます。2台とも、いったん umount によりアンマウントしてから、同じマウントポイントにマウントします。 ls を実行すると後からマウントしたディスクしか見ることができません。
# mkfs -t ext3 /dev/sda # mkfs -t ext3 /dev/sdb # mount /dev/sda /mnt/a # mount /dev/sdb /mnt/b # echo aaa > /mnt/a/fileA # echo bbb > /mnt/b/fileB # umount /mnt/[ab] # mount /dev/sda /mnt # mount /dev/sdb /mnt --- 同じディレクトリへマウントする # ls fileB --- fileAは見えない # umount /mnt # umount /mnt --- アンマウントも2回実行する
ここで同一ディレクトリから複数のディスクを透過的に使用可能とするのがunionの基本機能です。ここでは mount に union オプションが実装されているものとして、例を挙げます。
# mount /dev/sda /mnt # mount --union /dev/sdb /mnt # ls fileA fileB --- fileAも見える
読み取り
単一のディレクトリから複数のディスク/ファイルシステムを参照する際のファイル読み取り操作は、直観的に理解できるでしょう。 fileA を読み取るには /dev/sda のディスクへアクセスし、 fileB の場合は /dev/sdb へアクセスします。union機能の実装により内部動作は異なりますが、従来通りに open(2) 、 read(2) を実行しても、union機能がそれぞれのファイルが存在するディスクを判断し、適切なディスクへアクセスするため、既存のツールを変更することなくそのまま使用できます( 図1 )。
図1: fileA へのアクセスイメージ

もし fileC が /dev/sda 、 /dev/sdb 両方のディスク上に存在した場合は、どちらから読み取るべきでしょうか。この問題はunion内に上位、下位の概念を導入することで直観的に解決できます( 図2 )。
図2:union では水平方向ではなく垂直方向へ積み重ねる

複数のディスクを同一ディレクトリへマウントする際に、ディスクを水平に並べるのではなく、垂直方法へ積み重ねるとイメージするとわかり易いでしょう。上位のファイルを優先することとし、「上位のファイル名が下位の同じファイル名をおおい隠した」と考えます(この構造からunion機能をstackable filesystem、積み上げ可能ファイルシステムとも呼びます)。一意な優先順位さえ決定すれば、上位である /dev/sdb から fileC を読み取るべきことは容易に理解できます。
ディレクトリの読み取り( readdir(3) )の動作も同様です。union内のディスクに対し、union機能が内部で順次 readdir(3) を実行し、重複するファイル名があれば下位のものを排除し、最終的な結果をユーザ空間へ返すことで、 fileC が2つ表示されることを防げます。
書き込み
ファイルへの書き込み、新規作成、削除の際にも上位、下位の概念が重要となります。union機能では、union内のディスクへ独自にreadonly、read/writeの属性を与える機能が一般的になっており、unionを2台のディスクから構成し、下位のディスクをreadonly、上位のディスクをread/writeとするのがもっとも単純な使用方法です。readonlyとされているディスクに対し、union機能が書き込み処理を行うことはありません。ユーザがファイルへの書き込み、新規作成、削除を行うと、常にread/writeとされているディスクへ処理を振り分けます。
ここで大きな問題が2つ考えられます。いずれも対象のファイルがreadonlyとされている下位のディスクにしか存在しないことに由来します。図2の fileA を例として考えてみましょう。
copy-up
ファイルへの書き込み処理は、書き込まれた部分以外の箇所は既存の内容を維持するのが通常の、いや必須な動作です。例えば、上記の例でfileA の内容は"aaa"ですが、この先頭の一文字だけを"A"に書き換えた場合の結果は"Aaa"であることを保証しなければなりません。しかし、上位ディスクには fileA は存在していないため、既存の内容(この例では"aaa")を上位ディスク内に保存する方法が必要になります。この問題に対応するため、union機能ではcopy-up(またはファイルベースのcopy-on-write)という機能を実装しています [1] 。この動作からunion機能はCOWファイルシステムとも呼ばれます。一般的に、アクセスが読み取りだけならばcopy-upは不要で、何らかの変更を加えた時点で初めてファイルがコピーされます。
[1] | copy-on-writeとはメモリ管理に関する用語でしたが、union機能では下位のディスクに存在するファイルを上位へコピーする意味で使用しています。このため混同を避けるためcopy-upという用語を使いますし、わざわざ「ファイルベースの」と付け加えたりします。 |
whiteout
ファイルの削除ができなければユーザの不便が大きすぎます。しかし、 fileA がreadonlyとされている下位のディスク上に存在している限り、実際には削除できません。
union
機能では、ファイルを「見かけ上」削除することでこの問題を解決します。実装方法はさまざまですが、共通しているのはread/writeである上位のディスクに特別なファイルを作成することで、下位に存在するファイルを隠してしまいます。この機能をwhiteout(直訳すると字消し)と呼びます。
後述するように、union機能の実装ごとにこの重要な2つの機能の実装も大きく異なります。
Linuxでの既存の実装
内部構造から見ると、union機能の実装には2種類あります。ファイルシステムとして実装するものと、マウントとして実装するものです。前述のディスクなどブロックデバイスをメンバとするのはマウントとしての実装です。ファイルシステムとしての実装とは、union機能を新たなファイルシステムとして実装するもので、 mount -t union_type_fs ... のように使用します。また、ファイルシステムとしての既存の実装ではunion内のメンバとして、ディスクではなく、マウント済みの別ファイルシステム内のディレクトリを指定します。わかり易く言い換えると、ディレクトリ単位でのunion実装とマウント(ブロックデバイス)単位でのunion実装とも言えます。
現時点ではLinuxカーネル本体にはunion機能は含まれていませんが、ファイルシステムとしての実装は外部モジュールとして開発されており、実際に広く利用されています。特にAUFSとUnionFSの2つが有名です。マウントとしての実装はまだあまり進められておらず、また現実にも使用されていません(本記事の冒頭で述べた平成21年春のパッチシリーズはUnionMountというマウントとしての実装です)。理解を助けるため、マウント操作からその違いを見てみましょう。いずれも、Linuxカーネルにそれぞれのパッチを適用後に、 /mnt/rw をread/writeな上位、 /mnt/ro をreadonlyな下位とし、 /mnt/u からアクセス可能とする操作です。union内のメンバ指定の差に注目してください。
マウント操作の違い
# mount -o ro /dev/sda /mnt/ro # mount /dev/sdb /mnt/rw # mount -t aufs -o br:/mnt/rw:/mnt/ro none /mnt/u -- AUFSのマウント例
または
# mount -t unionfs -o dirs=/mnt/rw=rw:/mnt/ro=ro none /mnt/u -- UnionFSのマウント例 # umount /dev/sd[ab] # mount -o ro /dev/sda /mnt/u # mount --union /dev/sdb /mnt/u -- UnionMountのマウント例
現在のほとんどのLiveCDは、ファイルシステムとして実装されたunion機能により実現されています。インストール済みLinuxシステムを丸ごとファイルシステムイメージ化し、CD-ROM/DVD/Flashに収め、システム起動時にファイルシステムイメージを下位のreadonlyメンバに、また上位メンバにはtmpfsなどを指定し、union機能により結合します。このLiveシステムではハードディスクを使用しないため、MS WindowsユーザがLinuxを試用する場合や、障害が発生したLinuxシステムの復旧などにも利用されています。
AUFS、UnionFS
現状でもっとも広く利用されているunion機能は、おそらくAUFSでしょう。AUFSは平成15年からプロジェクトが始まり、翌年にUnionFSのコードから分岐する形で本格化したもので、リリース以後その安定性から多くのUnionFSユーザがAUFSへ移行して行きました。現在のほとんどのLiveCDで使用されている他にも、次のような使用例がAUFSユーザメーリングリストに報告されています。
- ブラジルの大学ではPXEによるnetboot環境を提供しており、10,000台以上でAUFSを使用している。
- 2008年6月時点で世界最速とされた、NUMA CellシステムでもAUFSを使用している。
[2] | AUFSは毎週バージョンアップされていますが、LiveCDの中には古いバージョンのAUFSをずっと使い続けているものもあります。恐らくAUFSはあくまでもLive環境でのみの使用という考えからでしょう。このため、Live環境以外の使用方法、例えばNFSサーバ、開発作業のバージョンコントロールなど、にLiveCDのAUFSを使用するとエラーが発生する場合があります。 エラーを未然に防ぐためにも最新のAUFSの使用をお勧めします。 |
ソースコードの始まりこそUnionFSから分岐する形でしたが、その後の進化の結果、まったく異なるソフトウェアに成長しました。ユーザに見える機能として追加/変更されたものの一部を挙げます。
- inode番号の維持管理
- メンバディレクトリの直接操作(AUFSをバイパスする)可能
- NFSエクスポート可能、 seekdir(3) 対応
- 時間のかかる処理や特殊な権限を必要とする処理などのための専用カーネルスレッド
- pseudo-link(ファイルシステムをまたいだハードリンク)
- read/writeなメンバディレクトリを複数指定可能、およびその選択ポリシーも複数から選択可能
- 膨大なエントリ数に対応した readdir(3)
- copy-upのsparseファイル(ファイルの一部にディスクブロックが割り当てられていない「穴空きファイル」)対応
- 下位にエントリが存在しない場合などwhiteoutの必要性を考慮
- whiteoutをハードリンクとして実装
- df/statfs(2) 対応、全メンバの合計を返すモードと最上位のものを返すモード
その他にも、「メモリページをコピーしない mmap(2) 、 /proc/PID/exe 対応」など、バージョンアップしたUnionFSへと先祖返りのように取り込まれた機能もあります。
AUFSの構造
AUFSの構造へ話を進める前にVFSについて簡単におさらいしておきましょう。ご存知のようにLinuxには多数のファイルシステム(種類)が実装されています。ext[234]、iso9660、nfs、xfsなどです。カーネル内のモジュール階層からみると、これらはいずれもVFSという抽象ファイルシステムレイヤの下に位置します。
例えばユーザがディレクトリを1つ作成したとしましょう。その結果はディスク上に書き込まれ、保存されなければなりません。アプリケーションが mkdir(2) というシステムコールを発行すると、カーネル内のVFSレイヤに存在する sys_mkdir() という関数へ制御が渡ります。 sys_mkdir() は渡されたパラメータやプロセスのカレントディレクトリ、現在のマウントツリーなどを基に、作成するディレクトリ名を検索(lookup)し、パス名を解決(pathname resolution)します。
一般的なエラーチェックや前処理は行いますが、ディスクなどブロックデバイスおよびそのバッファを操作することはしません。ここでブロックデバイス/そのバッファを操作するのはVFSの下位に位置するext3などのファイルシステムです。パス名の解決が完了すると、操作対象のファイルシステムが特定でき、そのファイルシステムがディスク上のレイアウトや書き込む情報を担当します。ファイルシステム種類によりレイアウトは異なりますが、その実処理を担当するのはファイルシステムごとに実装されている mkdir() 関数であり、例えばext3ならば ext3_mkdir() です。VFSの sys_mkdir() はファイルシステム内の詳細には関知せず、ファイルシステムが用意した関数ポインタを経由し下位の ext3_mkdir() 関数を呼び出します( 図3 )。
図3:VFS とファイルシステムのレイヤ関係―ext3

AUFS/UnionFSもファイルシステムとして実装されているので、ext3Mkdirのext3やnfsと同列の位置付けになります。しかし、AUFSの基本機能は他のディレクトリを結合することにあり、自身でブロックデバイス/バッファを処理することはありません。その代わりにAUFSは、union内のメンバに対し、VFS内に用意されているヘルパ関数をコールします。深さは二段しかありませんが、 vfs_mkdir() を再帰的にコールすることになります( 図4 )。
図4 :VFS とファイルシステムのレイヤ関係―aufs

AUFSは通常のファイルシステムと同様に、VFSからコールされるものであると同時に、他のファイルシステムを使用するため、VFSをコールするものでもあります。VFS内の sys_mkdir() は vfs_mkdir() をコールする際に、セキュリティ機能などの前処理を行っていますが、aufs_mkdir()でも同等の処理を行っています。この構造は、union機能をファイルシステムとして実装する際の大きな特徴であるとともに、コードサイズが大きくなるデメリットでもあります。このデメリットのためLinuxカーネルにはファイルシステムとしてのunion機能の実装は取り込まれず、代わりに後述するマウントとしての実装であるUnionMountが取り込まれる見込みです。
AUFSには他にも特徴があります。バックエンドのブロックデバイスを持たないファイルシステムは珍しくありませんが、inode番号を保存/管理するため、union内でread/write可能なファイルシステム上に特殊なファイルを作成し、自らI/Oを行う点です。 カーネル内から自らファイルI/Oを行う処理は極めて稀で、AUFS以外では core dump とprocess accounting( /var/log/pacct )くらいでしょう。
AUFS付属のサンプル
AUFSのソースコードにはサンプルがいくつか付属しています。AUFSの機能を簡単に知るのに良い入門となるでしょう。ここではAUFSのバージョン(aufs1/aufs2)の差異を問わずまとめて紹介します。
- diskless
- クラスタ環境などでは、Linuxをインストールした直後の状態のHDDをコピーし、その後個別の設定を行う方法が採られることが多いが、このいわゆる「システム部分」は複数のホストで共通なため、一元管理し、AUFSの下位readonlyメンバとしてホスト間で共有する。
- brsync
- AUFS内のメンバ間でファイルの同期を採る。長期間使用したAUFSでread/writeメンバのサイズが肥大した場合に、旧readonlyメンバとマージした内容の新readonlyメンバを作成すると、read/writeメンバの容量を節約できる。
- auroot
- インターネットサーバなどでセキュリティ対策の一環として chroot を使用する場合に、AUFSを利用し、ファイルの重複を避ける。
- auware
- 前述のdiskless同様に、VMwareなどを使用し1ホスト内に複数の仮想サーバを立てる場合にも、システムの共通部分によるディスク消費が問題になりうる。既存のVMwareアプライアンスからファイルシステムを取り出し、共通部分をreadonlyメンバとして共有する。
図5:diskless サンプルのAUFSメンバ階層

Copyright © 2009 SENJU, Jiro
Part 2へ >>本稿で取り上げた内容は、記事の執筆時(2009年6月)の情報に基づいており、その後の実装の進展、変更等により、現在の情報とは異なる場合があります。
本連載「プログラマーズ・ハイ」では読者からのご意見、ご感想を切望しています。今後取り上げるテーマなどについてもご意見をお寄せください。