【連載】世界一わかりみが深いコンテナ & Docker入門 〜 その6:Dockerのファイルシステムってどうなってるの? 〜

コンテナ・Docker

こんにちは、サイオステクノロジー武井です。いよいよ佳境に入ってきた連載「世界一わかりみが深いコンテナ & Docker入門 」ですが、今回はDockerのファイルシステムです。

全7回シリーズでお届けする予定で、今回は第6回目となります。

  1. その1:コンテナってなに?
  2. その2:Dockerってなに?
  3. その3:Dockerfileってなに?
  4. その4:docker-composeってなに?
  5. その5:Dockerのネットワークってどうなってるの?
  6. 今回はこちら → その6:Dockerのファイルシステムってどうなってるの?
  7. その7:実践!!Dockerでアプリケーション開発!!(執筆中)

Dockerのファイルシステム

Dockerは、その1:コンテナってなに?で紹介したDockerリポジトリにたくさんのDockerイメージを格納しています。その容量を節約するために、ちょっと特殊なファイルシステムを採用しています。それは、「OverlayFS(Overlay Filesystem)」というもので、ちょうど画像や写真を編集するソフト「Photoshop」のレイヤーのようなイメージです。OverlayFSのおかげで劇的に容量を効率化出来ているDockerリポジトリですが、まず、そのOverlayFSの仕組みからご説明したいと思います。(他にも色々なファイルシステムがありますが、本ブログでは一番メジャーと思われるOverlayFSについてのみ説明します)

OverlayFSとは?

OverlayFSについてお話します。

OverlayFSのイメージ

OverlayFSとは、端的にいうと、レイヤーを重ね合わせて結合してできるファイルシステムです。ナンノコッチャという感じですが、まず以下の図を見てください。

layer01〜layer03までの3つのディレクトリがあります。OverlayFSではこれら3つのレイヤーを結合してmergeディレクトリのような見せ方をすることが出来ます。

では、それぞれのレイヤーに同じファイル名のファイルがあった場合はどうなるでしょうか?

上図のようにlayer02とlayer03のディレクトリに同じファイル名のファイルBがあった場合、上のレイヤーのほうが見えることとなります。

これはOverlayFSの超簡単なイメージであり、実際はもうちょっと複雑な動きをします。次にそれを説明したいと思います。

OverlayFSの実際の動き

ここではOverlayFSの実際の動きをご説明するとともに、実際にコマンド叩いて実践したいと思います。

まず、その前にちょっと説明させてください。OverlayFSには以下の4つのレイヤーの概念があります。

lowerdir
重ね合わせるレイヤーのベースとなるディレクトリです。先程のイメージ図で記載した「layer01〜layer03ディレクトリ」に相当します。OverlayFSの仕組み上、このディレクトリのファイルに対して変更がされることはないので、基本的にこのディレクトリは読み込み専用でOKです。
upperdir

mergeddirに対して変更をかけたファイルが保存されるディレクトリです。

workdir
内部的に利用される作業用ディレクトリです。
mergeddir
lowerdirとupperdirを結合したディレクトリです。ファイルに対して追加・変更・削除などの操作を行うディレクトリでもあります。

 

説明だけではわかりにくいと思いますし、私も最初この説明だけでは全くわかりませんでした。なので、実践してみたいと思います。以下のような構成をもとに、実際にOverlayFSを構築します。upperdirはここでは使いませんし、一旦その存在を忘れてもらってOKです。OverlayFSはややこしいので、順を追って説明していきます。

lowerdirに相当する2つのディレクトリ「lower01」「lower02」、upperdirに相当するディレクトリ「upper」、mergeddirに相当するディレクトリ「merged」 を作成します。

期待する動きとしては、lower01ディレクトリにあるhoge.txt(中身はhogeと書いてある)と、lower02ディレクトリにあるfuga.txt(中身はfugaと書いてある)の両方のファイルがmergedディレクトリに表示されるというものです。upperは、今回の説明では使いませんし、ややこしいので気にしないでください。

 

では早速実践してみましょう!!OverlayFSを使うためには、毎度おなじみmountコマンドを使います。書式は以下のとおりです。

mount -t overlay [一意の識別名] -o lowerdir=[lowerdirに指定するディレクトリ],upperdir=[upperdirに指定するディレクトリ],workdir=[workdirに指定するディレクトリ] [mergeddirに指定するディレクトリ]

上図の構成を実現するためのコマンドは以下のとおりです。

lowerdirに複数のディレクトリを指定する場合は、コロンで区切り、左側に指定するほうが上になります。なので今回の場合は、lower02:lower01と指定します。

では、本当に期待通りの動作になっているか見てみましょう。

キタ━━━━(゚∀゚)━━━━!!

期待通りですね。

では、次にlower02ディレクトリにhoge2という内容のhoge.txtを追加してみます。lower02のほうがlower01より上のレイヤーなので、mergedディレクトリのhoge.txtの中身はhoge2になるはずです。図にすると以下のような構成ですね。

 

では試してみませう(๑•̀ㅂ•́)و✧ lowerdirは基本読み取り専用を前提としているので、lowerdir内のファイルを変更する場合、再マウントが必要になります。

キタ━━━━(゚∀゚)━━━━!!

期待通りですね。

ファイルを追加するときの動き

ここからは、ファイルの追加・削除・更新のときのそれぞれのユースケースにて、OverlayFSがどのような動きをするのか解説したいと思います。

では、先程ご説明した以下の構成のOverlayFSによって作られたファイルシステムにファイルを追加したいと思います。追加するファイル名「piyo.txt」、その中身はpiyoという文字列のファイルになります。

 

ここで初めてupperdirの出番なのです。OverlayFSにファイルを追加・更新・削除などの変更処理を加える場合、OverlayFSの仕様により、それらは必ずupperdirに反映されます。

ユーザーに見えるのはmergeddirであり、ユーザーはこのmergedディレクトリにファイルを追加するオペレーションをするわけですが、OverlayFS的にはpiyo.txtはupperディレクトリに追加されます。そして、upperディレクトリに書き込まれたファイルはlower01ディレクトリやlower02ディレクトリに書き込まれたファイルと同じように、mergedディレクトリに見えるようになります。図にすると以下のような感じです。

 

つまり、OverlayFSはlowerdirとupperdirを重ね合わせたもの(mergrddir)が、ユーザーに見えるディレクトリになるわけです。

では、実際にやってみたいと思います。

最初はlower01ディレクトリにhoge.txt、lower02ディレクトリにfuga.txtがある状態です。まず、この状態を以下のように作ります。「OverlayFSの実際の動き」でご紹介したことと同じことをしているだけですが。

 

ここでmergeddirにpiyo.txtを追加してみましょう。確かにmergeddirディレクトリにファイルが追加されていることがわかります。

 

でも、実際に追加したファイルはuperdirディレクトリにあります。

 

ということで、追加したファイルはすべてupperdirの方に反映されることがわかりました。

ファイルを更新するときの動き

次にファイルを更新するときの動きを説明します。「ファイルを追加するときの動き」でご紹介した以下の構成のfuga.txtというファイルの内容をfugaからfuga2に変更してみます。

 

ユーザーがmergedディレクトリ内のfuga.txtをhogeからhoge2に変更するオペレーションをすると、ファイルシステム内部の動きは、まずlower02のfuga.txtがupperディレクトリにコピーされます。

 

次に、uperディレクトリ内のfuga.txtの内容がfugaからfuga2に変更されます。

 

そして、このfuga.txtというファイル名のファイルは、lower02ディレクトリとupperディレクトリの両方に存在してます。OverlayFSの仕様では、より上位の層のレイヤーのファイルがユーザーに見えることとなるので、mergedディレクトリには、upperディレクトリにあるファイルが見えることとなります。つまり以下のような状態です。

 

では実践してみましょう。「ファイルを追加するときの動き」の状態で、mergedディレクトリ内のhoge.txtの内容をhogeからhoge2に変更してみます。

 

mergedディレクトリ内のhoge.txtの内容はhoge2になっているわけですが、upperディレクトリのhoge.txtも合わせてhoge2になっていることがわかります。

 

つまり、mergedディレクトリに加えた変更は、一旦lower01ディレクトリもしくはlower02ディレクトリのファイルをupperディレクトリにコピーし、それからその内容を変更します。この仕組をコピー・オン・ライトといいます。

ファイルを削除するときの動き

次にファイルを削除するときの動きを見てみます。「ファイルを更新するときの動き」でご紹介した以下の構成から、hoge.txtを削除してみます。

 

hoge.txtを削除すると下図のようになります。upperディレクトリに何やら新しいファイルが出来て、mergeddirディレクトリにはhoge.txtは見えなくなりました。

upperディレクトリに出来たファイルは「ホワイトアウトファイル」と呼ばれるもので、ファイルが削除されたことを表すものです。OverlayFSでは、lowerdirの層にあるディレクトリには変更を加えることはしません。そういう仕様だからです(この仕様のありがたみは、このあとの説明でご説明します)。では、「ファイルが削除された」ということを表現する方法が難しいわけですが、そこでOverlayFSでは、先程のホワイトアウトファイルと呼ばれるものを、削除対象と同名のファイル名でupperディレクトリに置くことで、ファイルシステム的に削除されたことにしてしまうわけです。つまりmergeddirからは見えなくなるということになります。

では、この「ホワイトアウトファイル」というファイルの実態についてですが、これは「キャラクタデバイスファイル」と呼ばれるものです。ここでは、本筋から離れるので多くを語りませんが、Linuxのファイルの種類には「ファイル」「ディレクトリ」「シンボリックリンク」などのほかに「デバイスファイル」というものがあります。Linuxはハードディスクなどの物理的なデバイスや画面への出力など何でもファイルとして表現する特徴があります。ハードディスクなら/dev/sdaみたいなのがありますし、画面になにか文字を出力したい場合は、/dev/stdoutに書き出したりします。ホワイトアウトファイルもこのデバイスファイルの一種です。デバイスファイルには「キャラクタデバイスファイル」と「ブロックデバイスファイル」があり、前者は1文字単位で、後者はある程度まとまった単位で通信します。ここでは、それほどキャラクタデバイスファイルやブロックデバイスファイルについては、知らなくてもいいかもしれません。とにかくキャラクタデバイスファイルは、OverlayFSではファイルが削除されたことを表すマーカーみたいなものなのです。

 

では実践してみましょう。

確かに削除されてますね。

 

では、upperディレクトリを見てみましょう。

あれ?何やら新しいhoge.txtという新しいファイルが出来ていますね。しかも、先頭の一文字が「c」となっています。これがキャラクタデバイスファイルです(ディレクトリだとd、シンボリックリンクだとlとかになっています)。

 

もちろん下位のレイヤー(lower01ディレクトリ、lower02ディレクトリ)には何ら変化はありません。

 

OverlayFSでのファイルの追加・更新・削除を実施してみました。いかがでしたでしょうか?なんとなく雰囲気は掴んで頂けたかと思います。次は、OverlayFSがどのようにDockerに応用されているかを見てみます。

DockerでOverlayFSを試してみよう!!

では、このOverlayFSをDockerでどのように使われているか体感してみましょう!!

コンテナを作ってOverlayFSの動きを確認する

そのために、以下のDockerイメージを作成します。

  • ベースイメージはCentOS7
  • hogeと書かれたテキストファイルを/root/hoge.txtに配置
  • fugaと書かれたテキストファイルを/root/fuga.txtに配置

以下のDockerfileを作成します。

 

そして以下のコマンドを実行してDockerのイメージを作成します。

 

先ほど作成したイメージでコンテナを起動します。

 

mountコマンドでファイルシステムのマウント状況を確認します。

 

lowdir、upperdirなどに色々何やら指定されていますね。これを先程の図に置き換えると以下のようになります。(図が小さいのでクリックして拡大してみてください)

 

先程のmountコマンドの結果にて、lowerdirオプションでコロン(:)区切りで指定した部分が、OverlayFSのlowerdirの層に該当します。upperdirオプションはOverlayFSのupperdir層に該当します。でもまだ、このコンテナには何も変更を加えていないので何もありません。「overlya on…」の直後に指定されているディレクトリは、OverlayFSのmergeddir層に該当します。

そして、上図から見てわかるように、Dockerfileに記載した1行が、lowerdirの1層に該当します。そしてその順番はmountコマンドのlowerdirオプションで指定されている順番の前にある方がより新しい物となっています。

lowerdirの一番下の層はCentOSのバイナリやライブラリが格納されています。真ん中の層はhoge.txtがある層、一番上はfuga.txtがある層ですね。

つまりDockerfileに記載されている一行ごとが、OverlayFSの1層に該当するのです。ただし、LBAELコマンドなどファイルシステムに影響のないコマンドは、OverlayFSの層は作成されません。

新しくファイルを作ってみる

ここでさらにOverlayFSのコンテナでの動きを確認するために、先程作成したコンテナに新しいファイルを作成してみましょう。

 

upperdirの層に新しくpiyo.txtというファイルが追加されていました。つまり以下のようになっています。

 

先程、「OverlayFSとは?」の「ファイルを追加するときの動き」で説明した動きと全く同じものとなっていることがわかると思います。

【補足】必ず追加されるレイヤー

先程の図では触れていませんでしたが、lowerdirオプションで指定されている一番最初のレイヤー(つまりlowerdirの一番上のレイヤー)には、どのコンテナも必ず以下のようなディレクトリ・ファイル構成を含みます。

どうやらこれは、Dockerが必ず勝手に追加するレイヤーのようです。例えば、.dockerenvがあることによって、これがコンテナかどうかを識別したりなど、そういうシステム的な用途に使うレイヤーのようです。

Dockerリポジトリにイメージをpushしてみよう!!

ここからいよいよ核心に迫ってきます。「なぜOverlayFSを用いると、Dockerイメージの容量を劇的に効率化できるのか?」がわかるまで、あともうちょっとです。そのためには、Dockerリポジトリにイメージをpushして、その仕組を説明する必要があります。

まずは簡単なイメージをpushしてみる

Docker Hubの中身を見ることはできませんので、Dockerのプライベートリポジトリを作ってみます。これは、その名の通り、自分の好きな環境に自分だけのマイDockerリポジトリを作成できるものです。このプライベートリポジトリはDocker Hubと同じ動きをします。

では、プライベートリポジトリを作成します。

これだけです。Docker Hubからregistryという名前のイメージがDockerのプライベートリポジトリで、これpullして、localhostの5000番のポートでアクセスできるようにしています。

では、先程作成したイメージをpushしてみましょう。その前にまず準備があります。まず以下の書式のコマンドでDockerイメージにタグ打ちします。

docker tag [イメージ名] [Dockerリポジトリのホスト名]:[Dockerリポジトリのポート番号]/[Dockerリポジトリ内でのイメージ名]:[Dockerリポジトリ内でのタグ名]

この書式に準じて、以下のコマンドを発行します。

ホスト名localhost、ポート番号5000のDockerリポジトリに対して、ホストPC内のtestapp01というDockerイメージをイメージ名tespapp01、タグ1.0.0でpushするための準備になります。

docker imagesコマンドを発行すると、このタグうちされたイメージが確認できます。

 

これでpushする準備は整いました。ではpushします。

 

これでpushは完了しました。これらの情報はプライベートリポジトリ内の/var/lib/registry/docker/registry/v2ディレクトリ内に格納されています。しかし、結構複雑な構成になっています。

大きく分けますと、pushした各イメージのメタ情報が格納されているrepositoriesというディレクトリ、メタ情報から参照され実際のコンテンツが格納されるblobsというディレクトリから構成されます。

では、先程pushしたイメージがどのように格納されているか紐解いていきます。

まず、メタ情報を見てみます。先の程の図の通り、pushしたtespapp01というイメージ名で、タグが1.0.0のイメージは、/var/lib/registry/docker/registry/v2/repositories/testapp01/_manifests/tags/1.0.0/current/link というファイルに保存されています。この中身を見てみましょう。

 

この値はblobsディレクトリにあるコンテンツを指しています。blobsの下には、コンテンツを一定の規則でsha256でハッシュ化したディレクトリの下に保存されています。この例では、/var/lib/registry/docker/registry/v2/blobs/sha256/ca/cae2b6d2048363477707e2efeebb392cf77769e2fc4aab15e4692c76f9c2b399/dataに保存されていることとなります。

では、このメタ情報の中身を見てみましょう。

 

メタ情報がJOSN形式で色々書かれていますが、大事なのはlayersというフィールドです。ここには、配列の形式で、このイメージを構成するレイヤーの情報が格納されています。3つのレイヤーから成り立っていることがわかります。

そして、この順番も重要で、layersフィールドの中に定義されている配列は、上にある方が一番下のレイヤーということになります。このあたりは、後ほどもっと詳細にご説明します。

digestの値は、コンテンツを一定の規則でハッシュ化したもので、blobsディレクトリのディレクトリ名のもととなっています。

では、このメタ情報をもとにblobsディレクトリの中身を実際に見てみることにしましょう。

まず、digestが「75f829a71a1c5277a7abf…」のコンテンツを見てみましょう。これは、先程ご説明したようにblobsディレクトリ配下のディレクトリ名と同じなので、このレイヤーのコンテンツは、/var/lib/registry/docker/registry/v2/blobs/sha256/75/75f829a71a1c5277a7abf55495ac8d16759691d980bf1d931795e5eb68a294c0/dataであることがわかります。

レイヤーのコンテンツは、tarで丸められgunzipで圧縮されていますので、解凍しますと以下のようなファイルが出てきます。

 

これはCentOS7のイメージですね。今度は、その一つ下の配列にあり、digestが「e1d238d7466d5d8ee1c493d…」のコンテンツを見てみましょう。このコンテンツは、/var/lib/registry/docker/registry/v2/blobs/sha256/e1/e1d238d7466d5d8ee1c493d55edfd05b793f2f5e24694eaa427bfab49c1e3fe1/dataに格納されています。こちらも同様にtarで丸められgunzipで圧縮されていますので、解凍します。

 

おお!!これは、hoge.txtを追加したレイヤーですね。ここまでやるとわかるかとは思いますが、blobs配下の各ディレクトリは、Dockerfile内で定義した各コマンドが生成したレイヤーは以下の図のように格納されています。

 

先程のイメージをちょっと変更したものをpushしてみる

さて、ここで先程のイメージをちょっと変更したイメージを作って、プライベートリポジトリにpushしてみます。以下のDockerfileを作成してtesapp02というイメージを作ります。testapp01との差分は、hello.txtが追加されたということだけです。

 

ビルドしてtestapp02というイメージを作ります。

 

先ほどと同じ要領でタグ打ちして、プライベートリポジトリにpushします。

 

さて、プライベートリポジトリの中のファイルを見てみると以下のようになっています。メタ情報が格納されているrepositoriesディレクトリの中に、testapp02というディレクトリが増えています。これは先程pushしたイメージ名のメタ情報になります。

では、先程と同じ要領でtestapp02というイメージのメタ情報を見てみましょう。

 

よーく目を凝らしてみ見ます。layersのフィールドの配列には4つのレイヤーが含まれます。ただしtestapp01のものと比べると、一番最後の配列にあるレイヤー(digestが「b5795aa121e49…」のもの)が増えているのみです。だいたい想像は付きますが、きっとこれは、hello.txtを追加したレイヤーかなと。実際見てみたいと思います。blobsディレクトリを見てみます。

やっぱりそうでしたね。

では次にblobsディレクトリを確認してみますと、以下のような構成になっています。

 

つまり、全く異なるイメージであるはずのtestapp01とtestapp02は、同じレイヤーはプライベートリポジトリ上で共有しているのです。

つまり、Dockerは、OverlayFSを利用して、Dockerfile内で発行された各コマンドによって生成されたファイルを「レイヤー」という単位で分けて管理して、同じレイヤーは重複してリポジトリにはアップせず、リポジトリ上で共有することで、容量を節約しているのです。

もしこれが、OverlayFSを利用しないでtestapp01、testapp02をまるごと違うものとして全てリポジトリにpushしたらどうなるでしょうか?CentOSのイメージも含まれるので、その容量はかなり大きくなることが想像できます。

まとめ

いかがでしたでしょうか?DockerのファイルシステムであるOverlayFS、そしてDockerがなぜOverlayFSを使うメリットを説明してみました。随分複雑なことをしているんですね、Docker。泣かせてくれます。でも、苦労してまとめたので、ぜひ見てくれたら幸いです。





ご覧いただきありがとうございます。
ブログの最新情報はSNSでも発信しております。
ぜひTwitterのフォロー&Facebookページにいいねをお願い致します!



>> 雑誌等の執筆依頼を受付しております。
   ご希望の方はお気軽にお問い合わせください!


ご覧いただきありがとうございます! この投稿はお役に立ちましたか?

役に立った 役に立たなかった

8人がこの投稿は役に立ったと言っています。

コメント投稿

メールアドレスは表示されません。


*