Carpe Diem

備忘録

UnionFS で Docker のレイヤ構造を理解する

概要

DockerではAUFSという技術が使われています。
こちらはUnionFS(ディレクトリを重ね合わせることができる)の一つで、親のファイルシステムをすべてReadOnlyにして、その上に書き込み可能なレイヤを重ねて1つのファイルシステムのように扱います。

f:id:quoll00:20160126172455p:plain

Dockerfileで作成する際も、各行がレイヤとして1つ前のコンテナとの差分を保持して作成されます。なので途中で失敗しても、実行中のレイヤのみ破棄するので再実行が非常に高速です。
今回はそのUnionFSについてまとめてみました。

環境

UnionFSとは

複数ファイルシステムを一つの場所にマウントすることができます。例としては以下のように複数のフォルダがある状態で

├── folder001
│   └── foo
└── folder002
    ├── bar
    └── baz

UnionFSを使うことで以下のように1つの場所にまとめてマウントできます。

folder100/
├── bar
├── baz
└── foo

インストール

$ sudo apt-get install unionfs-fuse

使い方

$ unionfs-fuse フォルダ=パーミッション:フォルダ=パーミッション 統合用フォルダ

として使います。先のフォルダほど優先されます。先ほどの例だと以下になります。

$ unionfs-fuse folder001=RW:folder002=RW folder100

こうすると以下のようにまとまります。

.
├── folder001
│   └── foo
├── folder002
│   ├── bar
│   └── baz
└── folder100
    ├── bar
    ├── baz
    └── foo

パーミッションについてはRWはRead, Write可能。ROはReadOnlyで読み込みのみです。

ケーススタディ

folder001に書き込んだらどうなるか

folder100にも表示されます。

$ touch folder001/hoge
$ tree
.
├── folder001
│   ├── foo
│   └── hoge
├── folder002
│   ├── bar
│   └── baz
└── folder100
    ├── bar
    ├── baz
    ├── foo
    └── hoge

folder100に追加したらどうなるか

先のフォルダが優先されるので、folder001の方に追加されます。

$ touch folder100/fuga
$ tree
.
├── folder001
│   ├── foo
│   └── fuga
├── folder002
│   ├── bar
│   └── baz
└── folder100
    ├── bar
    ├── baz
    ├── foo
    └── fuga

各フォルダに同じ名前のファイルがある

以下の様な場合、どうなるでしょうか

├── folder001
│   └── foo  // 001と書かれている
└── folder002
    ├── bar
    └── foo  // 002と書かれている

こちらも先ほど述べたように、先のフォルダが優先されるので、先のフォルダの方が表示されます。

$ unionfs-fuse folder001=RW:folder002=RW folder100
$ cat folder100/foo 
001

この状態でファイルを変更しても、同じく先のファイルのみ変更されます。

$ echo 100 > folder100/foo 
$ cat folder001/foo 
100
$ cat folder002/foo 
002

Copy On Write

次は Copy on Write を検証します。この考え方がDockerのレイヤ構造と同じです。
newという新しいレイヤを追加したとします。そして過去のレイヤをROで保持します。

$ unionfs-fuse -o cow new=RW:folder001=RO:folder002=RO folder100

ここで注意なのが2点あり、

  • cowオプションを付ける
  • RWのレイヤを一番先頭に持ってきてくる

です。特に後者は見落としがちなので注意です。ROが先にあると、書き込めないのにそちらが優先されるためPermission Deniedになります。

この状態でfolder100にファイルを追加したり、更新したりすると new にのみその更新が入ります。

ファイル追加の場合

$ touch folder100/hoge
$ tree
.
├── folder001
│   └── foo
├── folder002
│   ├── bar
│   └── baz
├── folder100
│   ├── bar
│   ├── baz
│   ├── foo
│   └── hoge
└── new
    └── hoge

ファイル更新の場合

別フォルダにあったファイルをfolder100から更新した場合、その変更がnewへ反映されます。

$ echo change! > folder100/foo 
$ tree 
.
├── folder001
│   └── foo
├── folder002
│   ├── bar
│   └── baz
├── folder100
│   ├── bar
│   ├── baz
│   └── foo
└── new
    └── foo

ファイルの更新もfoler001はされず、newにのみ入ります。

$ cat folder001/foo 
001
$ cat new/foo 
change!

まとめ

このようにして親コンテナをReadOnlyにし、書き込み可能なレイヤを作成し、差分のみ保持していきます。 これはDockerに限らず、

  • マウント元を変更させたくない
  • 読み取り専用メディアにあるので変更できない
  • 差分バックアップしたい

といったケースでも利用可能な技術です。

ソース