イメージ、コンテナ、ストレージ・ドライバについて¶
ストレージ・ドライバを効率よく利用するためには、Docker がどのようにしてイメージをビルドし保存するのかを理解しておく必要があります。 さらにそのイメージをコンテナがどのように利用するのかを理解しておくことも重要です。 つまりイメージとコンテナの双方の操作を可能とする技術に関して、おおまかに知っておく必要があります。
Docker がイメージ内やコンテナ内にてデータをどのように管理するのかを理解しておけば、コンテナ作りやアプリケーション Docker 化の最良な方法、さらに稼動時のパフォーマンス低下を回避する方法が身につくはずです。
イメージとレイヤ¶
Docker イメージは一連のレイヤから構成されます。 個々のレイヤは、そのイメージの Dockerfile 内にある 1 つの命令に対応づいています。 一番最後にあるレイヤを除き、これ以外はすべて読み込み専用のレイヤです。 たとえば以下のような Dockerfile を考えてみます。
FROM ubuntu:15.04
COPY . /app
RUN make /app
CMD python /app/app.py
この Dockerfile には 4 つのコマンドがあります。
コマンドのそれぞれが 1 つのレイヤを生成します。
まずは FROM
命令によって ubuntu:15.04
イメージから 1 つのレイヤが生成されるところから始まります。
COPY
命令は Docker クライアントのカレントディレクトリから複数のファイルを追加します。
RUN
命令は make
コマンドを実行してアプリケーションをビルドします。
そして最後のレイヤが、コンテナ内にて実行するべきコマンドを指定しています。
各レイヤは、その直前のレイヤからの差異だけを保持します。 そしてレイヤは順に積み上げられていきます。 新しいコンテナを生成したときには、それまで存在していたレイヤ群の最上部に、新たな書き込み可能なレイヤが加えられます。 このレイヤは「コンテナ・レイヤ」と呼ばれることがあります。 実行中のコンテナに対して実行される変更処理すべて、たとえば新規ファイル生成、既存ファイル修正、ファイル削除といったことは、その薄い皮のような書き込み可能なコンテナ・レイヤに対して書き込まれます。 以下の図は Ubuntu 15.04 イメージに基づいて生成されたコンテナを表わしています。
ストレージドライバー というものは、そういった各レイヤーが互いにやり取りできるようにします。 さまざまなストレージドライバーが利用可能であり、利用状況に応じて一長一短があります。
コンテナとレイヤ¶
コンテナとイメージの大きな違いは、最上部に書き込みレイヤがあるかどうかです。 コンテナに対して新たに加えられたり修正されたりしたデータは、すべてこの書き込みレイヤに保存されます。 コンテナが削除されると、その書き込みレイヤも同じく削除されます。 ただしその元にあったイメージは、変更されずに残ります。
複数のコンテナを見た場合、そのコンテナごとに個々の書き込み可能なコンテナ・レイヤがあって、データ更新結果はそのコンテナ・レイヤに保存されます。 したがって複数コンテナでは、同一のイメージを共有しながらアクセスすることができ、しかも個々に見れば独自の状態を持つことができることになります。 以下の図は、Ubuntu 15.04 という同一のイメージを共有する複数コンテナを示しています。
注釈
複数イメージを必要としていて、さらに同一のデータを共有してアクセスしたい場合は、そのデータを Docker ボリュームに保存して、コンテナ内でそれをマウントします。
Docker はストレージ・ドライバを利用して、イメージ・レイヤと書き込み可能なコンテナ・レイヤの各内容を管理します。 さまざまなストレージ・ドライバでは、異なる実装によりデータを扱います。 しかしどのようなドライバであっても、積み上げ可能な(stackable)イメージ・レイヤを取り扱い、コピー・オン・ライト(copy-on-write; CoW)方式を採用します。
ディスク上のコンテナ・サイズ¶
稼働中コンテナの概算サイズを確認するには docker ps -s
コマンドを実行します。
サイズに関連した 2 つのデータがカラム表示されます。
size
: (ディスク上の)データ総量。 各コンテナの書き込みレイヤが利用するデータ部分です。
virtual size
: コンテナにおいて利用されている読み込み専用のイメージデータと、コンテナの書き込みレイヤのsize
を足し合わせたデータ総量。 複数コンテナにおいては、読み込み専用イメージデータの全部または一部を共有しているかもしれません。 1 つのイメージをベースとして作った 2 つのコンテナでは、読み込み専用データを 100% 共有します。 一方で 2 つの異なるイメージが一部に共通するレイヤを持っていて、そこからそれぞれに 2 つのコンテナを作ったとすると、共有するのはその共通レイヤ部分のみです。 したがってvirtual size
は単純に足し合わせで計算できるものではありません。 これはディスク総量を多く見積もってしまい、その量は無視できないほどになることがあります。
起動しているコンテナすべてが利用するディスク総量は、各コンテナの size
と virtual size
を適宜組み合わせた値になります。
複数コンテナが同一の virtual size
になっていたら、各コンテナは同一のイメージをベースにしていると考えられます。
またコンテナがディスク領域を消費するものであっても、以下に示す状況はディスク総量の算定には含まれません。
- ロギング・ドライバ
json-file
を利用している場合に、そのログファイルが利用するディスク量。 コンテナにおいてログ出力を大量に行っていて、ログローテーションを用いていない場合には、このディスク量は無視できないものになります。 - コンテナが利用するボリュームやバインドマウント。
- コンテナの設定ファイルが利用するディスク領域。 そのデータ容量は少ないのが普通です。
- (スワップが有効である場合に)ディスクに書き込まれるメモリデータ。
- 試験的な checkpoint/restore 機能を利用している場合のチェックポイント。
コピー・オン・ライト方式¶
コピーオンライト(copy-on-write; CoW)は、ファイルの共有とコピーを最も効率よく行う方式です。 イメージ内の下の方にあるレイヤに、ファイルやディレクトリが存在していた場合に、別のレイヤ(書き込みレイヤを含む)からの読み込みアクセスが必要であるとします。 このときには、当然のことながら存在しているそのファイルを利用します。 そのファイルを修正する必要のある別のレイヤがあったとすると、これを初めて修正するとき(イメージがビルドされたときやコンテナが起動したときなど)、そのファイルはレイヤにコピーされた上で修正されます。 こうすることで入出力を最小限に抑え、次に続くレイヤの各サイズも増やさずに済みます。 この利点に関しては、さらに詳しく後述します。
共有によりイメージサイズはより小さく¶
docker pull
を実行してリポジトリからイメージをプルするとき、あるいはイメージから新たにコンテナを生成するにあたってそのイメージがまだローカルに生成されていないとき、各レイヤはプルによって個別に取得されて、Docker のローカル保存領域、たとえば Linux では通常 /var/lib/docker/
に保存されます。
取得された各レイヤは、以下の例のようにして確認することができます。
$ docker pull ubuntu:15.04
15.04: Pulling from library/ubuntu
1ba8ac955b97: Pull complete
f157c4e5ede7: Pull complete
0b7e98f84c4c: Pull complete
a3ed95caeb02: Pull complete
Digest: sha256:5e279a9df07990286cce22e1b0f5b0490629ca6d187698746ae5e28e604a640e
Status: Downloaded newer image for ubuntu:15.04
各レイヤは、Docker ホストのローカル保存領域内にて、それぞれのディレクトリ配下に保存されます。
ファイルシステム上のレイヤデータを確認するなら、/var/lib/docker/<storage-driver>/layers/
の内容を一覧表示します。
以下はデフォルトのストレージ・ドライバである aufs
の例です。
$ ls /var/lib/docker/aufs/layers
1d6674ff835b10f76e354806e16b950f91a191d3b471236609ab13a930275e24
5dbb0cbe0148cf447b9464a358c1587be586058d9a4c9ce079320265e2bb94e7
bef7199f2ed8e86fa4ada1309cfad3089e0542fec8894690529e4c04a7ca2d73
ebf814eccfe98f2704660ca1d844e4348db3b5ccc637eb905d4818fbfb00a06a
ディレクトリ名はレイヤ ID に対応するものではありません。 (Docker 1.10 以前は対応づいていました。)
ここで 2 つの異なる Dockerfile を利用している状況を考えます。
1 つめの Dockerfile からは acme/my-base-image:1.0
というイメージが作られるものとします。
FROM ubuntu:16.10
COPY . /app
2 つめの Dockerfile は acme/my-base-image:1.0
をベースとして、さらにレイヤを追加するものとします。
FROM acme/my-base-image:1.0
CMD /app/hello.sh
2 つめのイメージには 1 つめのイメージが持つレイヤがすべて含まれ、さらに CMD
命令による新たなレイヤと、読み書き可能なコンテナ・レイヤが加わっています。
Docker にとって 1 つめのイメージにおけるレイヤはすべて取得済であるため、再度プルによって取得する必要がありません。
2 つのイメージにおいて共通して存在しているレイヤは、すべて共有します。
この 2 つの Dockerfile からイメージをビルドした場合、docker image
や docker history
コマンドを使ってみると、共有されているレイヤに対する暗号化 ID は同一になっていることがわかります。
新規に
cow-test/
というディレクトリを生成して移動します。cow-test/
ディレクトリにて、以下の内容で新規ファイルを生成します。#!/bin/sh echo "Hello world"
ファイルを保存して実行可能にします。
chmod +x hello.sh
- 前述した 1 つめの Dockerfile の内容を、新規ファイル
Dockerfile.base
にコピーします。
- 前述した 2 つめの Dockerfile の内容を、新規ファイル
Dockerfile
にコピーします。
cow-test/
ディレクトリ内にて 1 つめのイメージをビルドします。
$ docker build -t acme/my-base-image:1.0 -f Dockerfile.base . Sending build context to Docker daemon 4.096kB Step 1/2 : FROM ubuntu:16.10 ---> 31005225a745 Step 2/2 : COPY . /app ---> Using cache ---> bd09118bcef6 Successfully built bd09118bcef6 Successfully tagged acme/my-base-image:1.0
2 つめのイメージをビルドします。
$ docker build -t acme/my-final-image:1.0 -f Dockerfile . Sending build context to Docker daemon 4.096kB Step 1/2 : FROM acme/my-base-image:1.0 ---> bd09118bcef6 Step 2/2 : CMD /app/hello.sh ---> Running in a07b694759ba ---> dbf995fc07ff Removing intermediate container a07b694759ba Successfully built dbf995fc07ff Successfully tagged acme/my-final-image:1.0
2 つのイメージのサイズを確認します。
$ docker images REPOSITORY TAG IMAGE ID CREATED SIZE acme/my-final-image 1.0 dbf995fc07ff 58 seconds ago 103MB acme/my-base-image 1.0 bd09118bcef6 3 minutes ago 103MB
それぞれのイメージに含まれるレイヤを確認します。
$ docker history bd09118bcef6 IMAGE CREATED CREATED BY SIZE COMMENT bd09118bcef6 4 minutes ago /bin/sh -c #(nop) COPY dir:35a7eb158c1504e... 100B 31005225a745 3 months ago /bin/sh -c #(nop) CMD ["/bin/bash"] 0B <missing> 3 months ago /bin/sh -c mkdir -p /run/systemd && echo '... 7B <missing> 3 months ago /bin/sh -c sed -i 's/^#\s*\(deb.*universe\... 2.78kB <missing> 3 months ago /bin/sh -c rm -rf /var/lib/apt/lists/* 0B <missing> 3 months ago /bin/sh -c set -xe && echo '#!/bin/sh' >... 745B <missing> 3 months ago /bin/sh -c #(nop) ADD file:eef57983bd66e3a... 103MB
$ docker history dbf995fc07ff IMAGE CREATED CREATED BY SIZE COMMENT dbf995fc07ff 3 minutes ago /bin/sh -c #(nop) CMD ["/bin/sh" "-c" "/a... 0B bd09118bcef6 5 minutes ago /bin/sh -c #(nop) COPY dir:35a7eb158c1504e... 100B 31005225a745 3 months ago /bin/sh -c #(nop) CMD ["/bin/bash"] 0B <missing> 3 months ago /bin/sh -c mkdir -p /run/systemd && echo '... 7B <missing> 3 months ago /bin/sh -c sed -i 's/^#\s*\(deb.*universe\... 2.78kB <missing> 3 months ago /bin/sh -c rm -rf /var/lib/apt/lists/* 0B <missing> 3 months ago /bin/sh -c set -xe && echo '#!/bin/sh' >... 745B <missing> 3 months ago /bin/sh -c #(nop) ADD file:eef57983bd66e3a... 103MB
注釈
docker history
の出力において<missing>
として示される行は、そのレイヤが他のシステムにおいてビルドされていることを示しています。 したがってローカルシステム上では利用することができません。 この表示は無視して構いません。
コピーによりコンテナーを効率的に¶
コンテナを起動すると、それまであったレイヤの最上部に、書き込み可能な薄いコンテナ・レイヤが加えられます。 コンテナがファイルシステムに対して行った変更は、すべてそこに保存されます。 コンテナが変更を行っていないファイルは、その書き込みレイヤにはコピーされません。 つまり書き込みレイヤは、できるだけ容量が小さく抑えられることになります。
コンテナ内にあるファイルが修正されると、ストレージ・ドライバはコピー・オン・ライト方式により動作します。
そこで実行される各処理は、ストレージ・ドライバによってさまざまです。
aufs
, overlay
, overlay2
といったドライバの場合、だいたい以下のような順にコピー・オン・ライト方式による処理が行われます。
- 更新するべきファイルをイメージ・レイヤ内から探します。 この処理は最新のレイヤから始まって、ベース・レイヤに向けて順に降りていき、一度に 1 つのレイヤを処理していきます。 ファイルが見つかるとこれをキャッシュに加えて、次回以降の処理スピードを上げることに備えます。
- 見つかったファイルを初めてコピーするときには
copy_up
という処理が行われます。 これによってそのファイルをコンテナの書き込みレイヤにコピーします。
- 修正が発生すると、コピーを行ったそのファイルが処理されます。 つまりコンテナは、下位のレイヤ内に存在している読み込み専用のそのファイルを見にいくことはありません。
Btrfs, ZFS といったドライバにおけるコピー・オン・ライト方式は、これとは異なります。 そのようなドライバが行う手法の詳細は、後述するそれぞれの詳細説明を参照してください。
データを大量に書き込むようなコンテナは、そういった書き込みを行わないコンテナに比べて、データ領域をより多く消費します。 コンテナの最上位にある書き込み可能な薄いレイヤ上に対して書き込み処理を行うことは、たいていが新たなデータ領域を必要とするためです。
注釈
書き込みが頻繁に行われるアプリケーションにおいては、コンテナ内にデータを保存するべきではありません。 かわりに Docker ボリュームを利用してください。 Docker ボリュームは起動されるコンテナからは独立していて、効率的な入出力を行うように設計されています。 さらにボリュームは複数のコンテナ間での共有が可能であり、書き込みレイヤのサイズを増加させることもありません。
copy_up
処理は際立った性能のオーバーヘッドを招きます。
このオーバーヘッドは、利用しているストレージ・ドライバによってさまざまです。
大容量ファイル、多数のレイヤ、深いディレクトリ階層といったものが、さらに影響します。
copy_up
処理は対象となるファイルが初めて修正されたときにだけ実行されるので、オーバーヘッドはそれでも最小限に抑えられています。
コピー・オン・ライトが動作している様子を確認するため、以下の例においては、前述した acme/my-final-image:1.0
イメージをベースとする 5 つのコンテナを見ていきます。
そして各コンテナがどれだけの容量を消費しているかを確認します。
注釈
以下の手順は Docker Desktop for Mac または Docker Desktop for Windows では動作しません。
Docker ホスト上の端末画面から、以下のような
docker run
コマンドを実行します。 各行の終わりには、各コンテナの ID を入力します。$ docker run -dit --name my_container_1 acme/my-final-image:1.0 bash \ && docker run -dit --name my_container_2 acme/my-final-image:1.0 bash \ && docker run -dit --name my_container_3 acme/my-final-image:1.0 bash \ && docker run -dit --name my_container_4 acme/my-final-image:1.0 bash \ && docker run -dit --name my_container_5 acme/my-final-image:1.0 bash c36785c423ec7e0422b2af7364a7ba4da6146cbba7981a0951fcc3fa0430c409 dcad7101795e4206e637d9358a818e5c32e13b349e62b00bf05cd5a4343ea513 1e7264576d78a3134fbaf7829bc24b1d96017cf2bc046b7cd8b08b5775c33d0c 38fa94212a419a082e6a6b87a8e2ec4a44dd327d7069b85892a707e3fc818544 1a174fc216cccf18ec7d4fe14e008e30130b11ede0f0f94a87982e310cf2e765
docker ps
コマンドを実行して、5 つのコンテナが実行中であることを確認します。CONTAINER ID IMAGE COMMAND CREATED STATUS PORTS NAMES 1a174fc216cc acme/my-final-image:1.0 "bash" About a minute ago Up About a minute my_container_5 38fa94212a41 acme/my-final-image:1.0 "bash" About a minute ago Up About a minute my_container_4 1e7264576d78 acme/my-final-image:1.0 "bash" About a minute ago Up About a minute my_container_3 dcad7101795e acme/my-final-image:1.0 "bash" About a minute ago Up About a minute my_container_2 c36785c423ec acme/my-final-image:1.0 "bash" About a minute ago Up About a minute my_container_1
ローカルの保存ディレクトリの内容を一覧表示します。
$ sudo ls /var/lib/docker/containers 1a174fc216cccf18ec7d4fe14e008e30130b11ede0f0f94a87982e310cf2e765 1e7264576d78a3134fbaf7829bc24b1d96017cf2bc046b7cd8b08b5775c33d0c 38fa94212a419a082e6a6b87a8e2ec4a44dd327d7069b85892a707e3fc818544 c36785c423ec7e0422b2af7364a7ba4da6146cbba7981a0951fcc3fa0430c409 dcad7101795e4206e637d9358a818e5c32e13b349e62b00bf05cd5a4343ea513
各サイズを確認します。
$ sudo du -sh /var/lib/docker/containers/* 32K /var/lib/docker/containers/1a174fc216cccf18ec7d4fe14e008e30130b11ede0f0f94a87982e310cf2e765 32K /var/lib/docker/containers/1e7264576d78a3134fbaf7829bc24b1d96017cf2bc046b7cd8b08b5775c33d0c 32K /var/lib/docker/containers/38fa94212a419a082e6a6b87a8e2ec4a44dd327d7069b85892a707e3fc818544 32K /var/lib/docker/containers/c36785c423ec7e0422b2af7364a7ba4da6146cbba7981a0951fcc3fa0430c409 32K /var/lib/docker/containers/dcad7101795e4206e637d9358a818e5c32e13b349e62b00bf05cd5a4343ea513
各コンテナは、ファイルシステム上において 32k しか容量をとっていません。
コピー・オン・ライト方式は容量を抑えるだけでなく、起動時間も節約します。 コンテナを起動するとき(あるいは同一イメージからなる複数コンテナを起動するとき)、Docker が必要とするのは、書き込み可能な薄いコンテナ・レイヤを生成することだけだからです。
仮に Docker が新たなコンテナを起動するたびに、その元にあるイメージ層をすべてコピーしなければならないとしたら、起動時間やディスク容量は著しく増大しているはずです。 このことは仮想マシン技術において、複数の仮想ディスクが仮想マシン 1 つに対して動作している様子にも似ています。
データ・ボリュームとストレージ・ドライバ¶
コンテナを削除したら、コンテナに対して書き込まれたあらゆるデータが削除されます。しかし、 データ・ボリューム (data volume) の保存内容は、コンテナと一緒に削除しません。
データ・ボリュームとは、コンテナが直接マウントするディレクトリまたはファイルであり、Docker ホストのファイルシステム上に存在します。データ・ボリュームはストレージ・ドライバが管理しません。データ・ボリュームに対する読み書きはストレージ・ドライバをバイパス(迂回)し、ホスト上の本来の速度で処理されます。コンテナ内に複数のデータ・ボリュームをマウントできます。1つまたは複数のデータ・ボリュームを、複数のコンテナで共有もできます。
以下の図は、1つの Docker ホストから2つのコンテナを実行しているものです。Docker ホストのローカル・ストレージ領域( /var/lib/docker/...
)の中に、それぞれのコンテナに対して割り当てられた領域が存在しています。また、Docker ホスト上の /data
に位置する共有データ・ボリュームもあります。このディレクトリは両方のコンテナからマウントされます。
データ・ボリュームは Docker ホスト上のローカル・ストレージ領域の外に存在しており、ストレージ・ドライバの管理から独立して離れています。コンテナを削除したとしても、Docker ホスト上の共有データ・ボリュームに保管されたデータに対して、何ら影響はありません。
データ・ボリュームに関する更に詳しい情報は、 コンテナでデータを管理する をご覧ください。
関連情報¶
参考
- About images, containers, and storage drivers
- https://docs.docker.com/engine/userguide/storagedriver/imagesandcontainers/