See the Elephant

1992生まれのプログラマが書くエンジニアブログ

dockerが使うUnionFileSystemを僕なりに解釈した

こちらも合わせてお読みください namu-r21.hatenablog.com

dockerのイメージとコンテナについて今一度

昨日書いた記事が運良くはてブに載り, 色々な方に見ていただけた.
namu-r21.hatenablog.com

そのおかげで, 有用なコメントを頂けた.

なんでファイルシステムの話なのにメモリの話とごちゃまぜになってるんだろ。  
コンテナを終了したらメモリ上のデータは消えるが差分ファイルシステムの writable に書かれたデータは残る。rm すると消える。

このコメントを理解するために, dockerが利用している
UnionFileSystemCopy on Writeについて調べた.

僕なりの解釈を書いていこうと思う.

dockerのファイルシステム

dockerでは, Union File Systemを導入している.
これは, コピーオンライトで動作するファイルシステムである.

dockerのファイルシステムそのものについては公式の解説が特ににわかりやすい.
docker-doc イメージ、コンテナ、ストレージ・ドライバの理解

今回は,

  1. コピーオンライト
  2. UnionFileSystem
  3. dockerにおけるUnionFileSystem
  4. 永続的なデータの保存

について掘り下げて書いていく.


コピーオンライトについて

先日のエントリdockerのファイルシステムについて知る - 1++では, 以下のように書いた.

コピーオンライトとは

"子プロセス生成時に親プロセスのメモリー空間を複製せず,書き込み処理が発生したときにはじめて複製する仕組み"である. 

このとき, 親プロセスのメモリ空間はRead onlyである.  

dockerでは, "イメージ"を親プロセス, "コンテナ"を子プロセスとして扱う. 基本的に "コンテナ単位でメモリ空間" が与えられる.
そもそもコピーオンライトとはなんなのか

コピーオンライトは, Copy on Writeと書く. 解説は, docker公式ドキュメントを引用する.
引用元 : docker-doc イメージ、コンテナ、ストレージ・ドライバの理解

コピー・オン・ライト(copy-on-write、cow)とは、共有とコピーのストラテジに似ています。

このストラテジは、システム・プロセスが自分自身でデータのコピーを持つより、同一インスタンス上にあるデータ共有を必要とします。

書き込む必要があるプロセスのみが、データのコピーにアクセスできます。
その他のプロセスは、オリジナルのデータを使い続けられます。

コピーオンライトは, 複数のプロセスが同時に動作するときに使用される, ストレージ共有の戦略である. つまり, これ自体はメモリだろうがファイルシステムだろうが関係無い.

データのコピーは非常に"重い"処理なため, その回数をできる限り減らすべく生まれたファイルシステムの最適化手法のようだ.

コピーオンライトではデータは無駄にコピーせず, 読み出しは共有領域から読み出す. 書き込みが必要な場合のみコピーして書き換える という挙動を取る.

このように, 書くとき(write)に初めてデータをコピー(copy)するから"Copy on Write"というらしい.


Union File Systemについて

UnionMountとUnion-type Filesystem(1),
まとめて束ねるUnionFSの不思議な世界を参考にした.

Unionとは, 合体という意味である.

僕の解釈ではUnion File Systemは, 複数のファイルシステムを階層的に合体させて, 1つのファイルシステムとして扱うことができるファイルシステムという理解で落ち着いた.

UnionFileSystemは, Unionマウントと呼ばれる特殊なマウント方法を取る.
その説明の前に, 通常のマウントから説明する.

その後, UnionFileSystemについて話したい.

通常のマウント

UnionFileSystemの前に, まずは通常のマウントについて考える.

f:id:namu_r21:20161027003900p:plain:w500

ここでは, /mntというディレクトリにdisc1(別名/dev/d1), disc2(別名/dev/d1)というディスクを順に1度ずつマウントする場合を考える.

2度実行した後, /mntの内容を確認すると, ユーザから見てdisc1のfileA, fileCは見当たらずdisc2のfileB, fileCのみが見える. つまり, 同時にマウントできるのは, 後にマウントした1つのディスクだけである.

Union マウント

UnionFileSystemでは, Unionマウント(以下Union)というマウントを行う.
これは, 同一のディレクトリから複数のディスクを1つのディレクトリとして透過的に扱うことができるようにするマウント方法である.

UnionMountとUnion-type Filesystem(1)を引用する.
以下のようにUnionFileSystemを表現している.

すでにディスクをマウントしているディレクトリ(マウントポイント)に別のディスクを重ねてマウントし、1つのマウントポイントから2台のディスクを同時に使用可能にするもの

f:id:namu_r21:20161027003927p:plain:w500

Unionを利用すると, 1つのディレクトリ上に3つのファイルが存在するように見える(fileCは同名なため1つとしてカウント). つまり, 実際には2つのディスクをマウントしているにもかかわらず, "見かけ上"は1つのディスクを扱っているように見える.

Unionでは, ディスクを階層的にマウントしていく. このため, 上記したように"別のディスクを重ねる"という表現になる.

実際には複数のディスクをマウントしているため, 複数のディスク上に同名のファイルがある場合問題がおきる. そのため, 常に上位レイヤのディスクを優先して読み込む.

図の場合, /dev/d2が上位, /dev/d1が下位ディレクトリである. fileCが同名ファイルであるが, この場合は, /dev/d2fileCが読み出されることとなる.

Union File System

Union File Systemは, Unionを利用したファイルシステムである.

Unionを行う場合, 下位ディスクをRead-Only, 上位ディスクをwrite/readに設定することが一般的である. 最上位のディスクをwritable状態にして, ユーザにファイルシステムを提供する.

f:id:namu_r21:20161027003958p:plain:w500

下位ディスクのデータをReadする必要がある場合はそのまま下位ディスク上のデータが読まれる.
下位ディスクのデータに対して, 書き込みを行う必要がある場合は, Copy on Writeを行う.

  1. Writableディスクに対象データをコピー
  2. Writableディスク上で対象データを編集
  3. Writableディスク上に対象データを保存

前述したように, Unionでは常に上位レイヤのディスクを優先して読み込む.
そのため, 最上位ディスクに保存することでデータを上書きしていくことができる.


dockerにおけるUnionFileSystem

冒頭でも行ったようにdockerはUnionFIleSystemを利用している.

dockerでは, イメージ用のディスクがRead-onlyなファイルシステムであり, その上にWritableなコンテナ用のディスクが重ねられる.

docker イメージとコンテナにおけるコピーオンライトの関係

図はdocker-doc イメージ、コンテナ、ストレージ・ドライバの理解より引用.

docker runでイメージからコンテナを起動した時点では, コンテナ・レイヤには何も記録されてない.

コンテナ上でファイルの"読み出し"を行う場合は, イメージ・レイヤの共通ストレージからデータ読み出しが行われる. このレイヤは, Read-onlyなため書き換えはできない.

対して, イメージ・レイヤの既存データに対して変更を加えた場合, 1度コンテナ・レイヤに対象データがコピーされそのデータを書き換えることになる.

その変更はイメージからの変更差分のみを記録する.
これがUnion File Systemの特徴である.

コンテナ内の状態変化はコンテナ・レイヤで閉じていて, 他のコンテナや大元のイメージには影響しない .

同じイメージから複数のコンテナを作った場合, コンテナごとにコンテナ・レイヤが用意される.
このため, コンテナ同士が互いに干渉せず, 複数の同環境で多様なオペレーションを行える.

僕のなかでdockerのイメージはこう解釈した.

dockerにおける"イメージ"は, 
"ベースイメージ(ファイルシステムの初期状態)に対する
ファイルの変更差分を階層的に記録した「ファイルシステムそのもの」"

コンテナは以下のような感じ.

イメージがもつファイルシステムを引き継いだ
変更部分のみを保持する固有のストレージを持つ1つの実行環境. 

dockerコンテナのライフサイクル

先日のエントリdockerのファイルシステムについて知る - 1++では, 以下のように書いた.

  dockerでは, イメージを元に"コンテナを作ったり消したりする"できることが特徴であり, 
「コンテナは使い捨て」という感覚で使われることが多い. 

コンテナはこのように"生き死に"を繰り返すので, 
「コンテナのライフサイクル」と呼ばれる.  

そして, "コンテナ上のメモリ空間はコンテナの終了とともに消える". 
つまり, コンテナ上でのファイル追加, 変更は"永続的に残らない"のである.

この部分についてもコメントでご指摘をいただいたので加筆. mapk0yさんのコメントを引用させていただきます.

コンテナを終了したらメモリ上のデータは消えるが差分ファイルシステムの writable に書かれたデータは残る。rm すると消える。

どうやら, docker rmでコンテナを削除するまでは, コンテナのwritable上にファイルが残るようだ. つまり, コンテナを削除しなければ, コンテナ上にデータは残る.

では, 永続的にデータをどうやって残すか.

コンテナのライフサイクルに関わらず永続的に残したいデータがある場合は, 以下の方法を取る.

  1. データボリュームを利用する.
  2. 現在のコンテナをイメージとしてcommitする(非推奨)
  3. データコンテナを利用する(推奨)

データ・ボリュームの利用

これは, ホストOS側にデータを記録する方法である. これをデータ・ボリュームと呼ぶ.
データ・ボリューム docker docs 公式和訳が詳しい. コンテナの起動時にホストOSのディレクトリ(データボリューム)をコンテナ起動時にマウントする. これによって, ファイルシステムの一部としてデータボリュームを利用できる.
.

現在のコンテナの状態をイメージとしてcommitする(非推奨)

dockerでは, コンテナのスナップショットをイメージとして吐き出すdocker commitというコマンドがある.

つまり, これまでコンテナ・レイヤだったストレージをイメージに含んでしまうのである.

これは元のイメージをある1つの環境に依存して汚してしまうため, お勧めできない方法である.

docker データ・コンテナ

最もお勧めできる方法は, データコンテナであるようだ. この方法では, 以下の手順をふむ.

  1. データボリュームをマウントしたデータ保存用のコンテナ(データコンテナ)を1つ作る.
  2. 他のコンテナにデータコンテナをマウントする

データコンテナのメリットは以下のようなものだ

  • データボリュームをrmするまでは, 永続的にデータが残る.
  • 複数のコンテナから同一のデータを参照できる.
  • 1つでも参照されているボリュームがあると, rmによるコンテナ削除ができない.

つまり, 1つのコンテナと存在するものの,
データコンテナとして参照され続けている間は
削除の対象にならない.

dockerを通してUnionFileSystemを勉強してみた所感

ファイルシステムがある種, "一方向のバージョン管理"のような仕組みを取られていて衝撃的であった.
きっちり記憶領域が分かれているから, "コンテナ間が依存しない"ということがわかった.

加えて, "データボリューム"周りの話についても, 以前より理解が深くなった.

dockerの永続的なデータ保持方法を知りたくて, 調べ物を始めたけれど
気づいたらファイルシステムの話をしていた.

まだまだ仮想環境やOSの話はわからないので学ぶことが多い.
今はvagrantの勉強をしているので, 次はそちらについて書く.