イメージ、コンテナ、ストレージ・ドライバの理解

ストレージ・ドライバを効率的に使うには、Docker がどのようにイメージを構築・保管するかの理解が必須です。そして、これらのイメージがコンテナでどのように使われているかの理解が必要になります。最後に、イメージとコンテナの両方を操作するための技術に対する、簡単な紹介をします。

イメージとコンテナはレイヤに依存

Docker イメージは読み込み専用(read-only)のレイヤが組(セット)になっているもので、それぞれのレイヤが層(スタック)として積み重なり、1つに統合された形に見えるものです。この1番目の層を ベース・イメージ (base imae) と呼び、他の全てのレイヤは、このベース・イメージのレイヤ上に積み重なります。次の図は、 Ubuntu 15:04 イメージが4つのイメージ・レイヤを組みあわせて構成されているのが分かります。

イメージ層

コンテナ内部に変更を加えた時を考えます。例えば、Ubuntu 15.04 イメージ上に新しくファイルを追加すると、下にあるイメージ層の上に、新しいレイヤを追加します。この変更は、新しく追加したファイルを含む新しいレイヤを作成します。各イメージ・レイヤは自身の UUID(universal unique identifier)を持っており、下の方にあるイメージの上に、連続したイメージ・レイヤを構築します。

コンテナ(ストレージの内容を含みます)は Docker イメージと薄い書き込み可能なレイヤとを連結したものです。この書き込み可能なレイヤは一番上にあり、 コンテナ・レイヤ(container layer) と呼ばれます。以下の図は ubuntu 15.04 イメージの実行状態です。

コンテナ・レイヤとイメージ

コンテナとイメージとの主な違いは、書き込み可能なレイヤ(writable layer)です。全てのコンテナに対する書き込み、つまり、新しいファイルの追加や既存のデータに対する変更は、この書き込み可能なレイヤに保管されます。コンテナが書き込み可能なレイヤを削除すると、コンテナも削除されます。イメージは変更されないままです。

それぞれのコンテナは、自分自身で書き込み可能なレイヤを持つので、全てのデータは対象のコンテナレイヤに保管されます。つまり、複数のコンテナが根底にあるイメージを共有アクセスすることができ、それぞれのコンテナ自身がデータをも管理できることを意味します。次の図は複数のコンテナが同じ Ubuntu 15.04 イメージを共有しているものです。

レイヤの共有

ストレージ・ドライバは、イメージ・レイヤと書き込み可能なコンテナ・レイヤの両方を有効化・管理する責任があります。ストレージ・ドライバは様々な方法で処理をします。Docker イメージとコンテナ管理という2つの重要な技術の裏側にあるのは、積み上げ可能なイメージ・レイヤとコピー・オン・ライト(CoW)です。

コピー・オン・ライト方式

共有とはリソース最適化のための良い手法です。人々はこれを日常生活通で無意識に行っています。例えば双子の Jane と Joseph が代数学のクラスを受けるとき、回数や先生が違っても、同じ教科書を相互に共有できます。あるとき、Jane が本のページ11にある宿題を片付けようとしています。その時 Jane はページ11をコピーし、宿題を終えたら、そのコピーを提出します。Jane はページ 11 のコピーに対する変更を加えただけであり、オリジナルの教科書には手を加えていません。

コピー・オン・ライト(copy-on-write、cow)とは、共有とコピーのストラテジ(訳者注:方針、戦略の意味、ここでは方式と訳します)に似ています。このストラテジは、システム・プロセスが自分自身でデータのコピーを持つより、同一インスタンス上にあるデータ共有を必要とするものとします。書き込む必要があるプロセスのみが、データのコピーにアクセスできます。その他のプロセスは、オリジナルのデータを使い続けられます。

Docker はコピー・オン・ライト技術をイメージとコンテナの両方に使います。この CoW 方式はイメージのディスク使用量とコンテナ実行時のパフォーマンスの両方を最適化します。次のセクションでは、イメージとコンテナの共有とコピーにおいて、コピー・オン・ライトがどのように動作してるのかを見てきます。

共有を促進する小さなイメージ

このセクションではイメージレイヤとコピー・オン・ライト技術を見ていきます。全てのイメージとコンテナ・レイヤは Docker ホスト上の ローカル・ストレージ領域 に存在し、ストレージ・ドライバによって管理されます。ストレージ領域の場所とは、ホストのファイルシステム上です。

docker pulldocker push でイメージ取得・送信する各命令の実行時、Docker クライアントはイメージ・レイヤについて報告します。以下のコマンドは、 Docker Hub から ubuntu:15.04 Docker イメージを取得(pull)しています。

$ docker pull ubuntu:15.04
15.04: Pulling from library/ubuntu
6e6a100fa147: Pull complete
13c0c663a321: Pull complete
2bd276ed39d5: Pull complete
013f3d01d247: Pull complete
Digest: sha256:c7ecf33cef00ae34b131605c31486c91f5fd9a76315d075db2afd39d1ccdf3ed
Status: Downloaded newer image for ubuntu:15.04

この出力を見ると、このコマンドが実際には4つのイメージ・レイヤを取得したのが分かります。上記のそれぞれの行が、イメージとその UUID です。これらの4つのレイヤーの組み合わせにより、 ubuntu:15.04 Docker イメージを作り上げています。

イメージ・レイヤは Docker ホスト上のローカル・ストレージ領域に保管されます。典型的なローカル・ストレージ領域の場所は、ホスト上の /var/lib/docker ディレクトリです。ストレージ・ドライバの種類により、ローカル・ストレージ領域の場所は変わる場合があります。以下の例では、 AUFS ストレージ・ドライバが使うディレクトリを表示しています。

$ sudo ls /var/lib/docker/aufs/layers
013f3d01d24738964bb7101fa83a926181d600ebecca7206dced59669e6e6778  2bd276ed39d5fcfd3d00ce0a190beeea508332f5aec3c6a125cc619a3fdbade6
13c0c663a321cd83a97f4ce1ecbaf17c2ba166527c3b06daaefe30695c5fcb8c  6e6a100fa147e6db53b684c8516e3e2588b160fd4898b6265545d5d4edb6796d

もし、別のイメージを pull (取得)するとき、そのイメージが ubuntu:15.04 イメージと同じイメージ・レイヤが共通している場合、Docker デーモンはこの状況を認識し、まだ手許に取得していないイメージのみをダウンロードします。それから、2つめのイメージを取得すると、この2つのイメージは、共通のイメージ・レイヤとして共有されるようになります。

これで、自分自身で実例を示して説明できるでしょう。 ubuntu:15.04 イメージを使うため、まずは取得(pull)し、変更を加え、その変更に基づく新しいイメージを構築します。この作業を行う方法の1つが、 Dockerfile と docker build コマンドを使う方法です。

  1. 空っぽのディレクトリに、 Dockerfile を作成し、ubuntu:15.04 イメージから始める記述をします。
FROM ubuntu:15.04
  1. 「newfile」 という名称の新規ファイルを、イメージの /tmp ディレクトリに作成します。ファイル内には「Hello world」の文字も入れます。

作業が終われば、 Dockerfile は次の2行になっています。

FROM ubuntu:15.04

RUN echo "Hello world" > /tmp/newfile
  1. ファイルを保存して閉じます。
  1. ターミナルから、作成した Dockerfile と同じディレクトリ上で以下のコマンドを実行します。
$ docker build -t changed-ubuntu .
Sending build context to Docker daemon 2.048 kB
Step 0 : FROM ubuntu:15.04
 ---> 013f3d01d247
Step 1 : RUN echo "Hello world" > /tmp/newfile
 ---> Running in 2023460815df
 ---> 03b964f68d06
Removing intermediate container 2023460815df
Successfully built 03b964f68d06

注釈

上記のコマンドの末尾にあるピリオド(.)は重要です。これは docker build コマンドに対して、現在の作業用ディレクトリを構築時のコンテキスト(内容物)に含めると伝えるものです。

上記の結果、新しいイメージのイメージ ID が 03b964f68d06 だと分かります。

  1. docker images コマンドを実行し、Docker ホスト上のローカル・ストレージ領域に、新しいイメージが作成されているかどうかを確認します。
REPOSITORY          TAG                 IMAGE ID            CREATED             VIRTUAL SIZE
changed-ubuntu      latest              03b964f68d06        33 seconds ago      131.4 MB
ubuntu
  1. docker history コマンドを実行すると、何のイメージによって新しい changed-ubuntu イメージが作成されたか分かります。
$ docker history changed-ubuntu
IMAGE               CREATED              CREATED BY                                      SIZE                COMMENT
03b964f68d06        About a minute ago   /bin/sh -c echo "Hello world" > /tmp/newfile    12 B
013f3d01d247        6 weeks ago          /bin/sh -c #(nop) CMD ["/bin/bash"]             0 B
2bd276ed39d5        6 weeks ago          /bin/sh -c sed -i 's/^#\s*\(deb.*universe\)$/   1.879 kB
13c0c663a321        6 weeks ago          /bin/sh -c echo '#!/bin/sh' > /usr/sbin/polic   701 B
6e6a100fa147        6 weeks ago          /bin/sh -c #(nop) ADD file:49710b44e2ae0edef4   131.4 MB

docker history の出力から、新しい 03b964f68d06 `` イメージ・レイヤが一番上にあることがわかります。先ほどの ``Dockerfile で、 echo "Hello world" > /tmp/newfile コマンドでファイルを追加しましたので、 03b964f68d06 レイヤにこのファイルが追加されたものだと分かっています。そして、4つのイメージ・レイヤは、先ほど ubuntu:15.04 イメージを構築する時に使った UUID と一致していることがわかります。

  1. ローカル・ストレージ領域の内容を、更に確認します。
$ sudo ls /var/lib/docker/aufs/layers
013f3d01d24738964bb7101fa83a926181d600ebecca7206dced59669e6e6778  2bd276ed39d5fcfd3d00ce0a190beeea508332f5aec3c6a125cc619a3fdbade6
03b964f68d06a373933bd6d61d37610a34a355c168b08dfc604f57b20647e073  6e6a100fa147e6db53b684c8516e3e2588b160fd4898b6265545d5d4edb6796d
13c0c663a321cd83a97f4ce1ecbaf17c2ba166527c3b06daaefe30695c5fcb8c

ここには幾つのレイヤが保管されているでしょうか。ここでは5つです。

新しい changed-ubuntu イメージは各レイヤのコピーを自分自身で持っていないことをに注意してください。下図にあるように、ubuntu:15.04 イメージの下にある4つのレイヤを、新しいイメージでも共有しているのです。

レイヤの共有

また、docker history コマンドは各イメージ・レイヤのサイズも表示します。 03b964f68d06 は 13 バイトのディスク容量しか使いません。なぜなら、下層のレイヤにあたるものは Docker ホスト上に存在しており、これらは ubuntu:15.04 イメージとして共有されているものです。つまり changed-ubuntu イメージが消費するディスク容量は 13 バイトのみです。

このイメージ・レイヤの共有こそが、効率的に Docker イメージとコンテナの領域を扱います。

コンテナを効率的にコピーする

先ほど学んだように、Docker イメージのコンテナとは、書き込み可能なコンテナ・レイヤを追加したものです。以下の図は ubuntu:15.04 をコンテナのベースと下レイヤを表示しています。

コンテナ・レイヤとイメージ

コンテナに対する全ての書き込みは、書き込み可能なコンテナ・レイヤに保管されます。他のレイヤは読み込み専用(read-only、RO)のイメージ・レイヤであり、変更できません。つまり、複数のコンテナが下層にある1つのイメージを安全に共有できるのです。以下の図は、複数のコンテナが ubuntu:15.04 イメージのコピーを共有しています。各コンテナは自分自身で読み書き可能なレイヤを持っていますが、どれもが ubuntu:15.04 イメージという単一のインスタンス(イメージ)を共有しています。

レイヤの共有

コンテナの中で書き込み作業が発生すると、Dockre はストレージ・ドライバでコピー・オン・ライト処理を実行します。この主の操作はストレージ・ドライバに依存します。AUFS と OverlayFS ストレージ・ドライバは、コピー・オン・ライト処理を、おおよそ次のように行います。

  • レイヤ上のファイルが更新されていないか確認します。まずこの手順が新しいレイヤに対して行われ、以降は1つ1つのベースになったレイヤを辿ります。
  • はじめてファイルのコピーが見つかると、「コピー開始」(copy-up)処理を行います。「コピー開始」とは、コンテナ自身が持つ薄い書き込み可能なレイヤから、ファイルをコピーすることです。
  • コンテナの薄い書き込み可能なレイヤに ファイルコピー してから、(そのファイルに)変更を加えます。

BTFS、ZFS 、その他のドライバは、コピー・オン・ライトを異なった方法で処理します。これらのドライバの手法については、後述するそれぞれの詳細説明をご覧ください。

たくさんのデータが書き込まれたコンテナは、何もしないコンテナに比べて多くのディスク容量を消費します。これは書き込み操作の発生によって、コンテナの薄い書き込み可能なレイヤの上に、更に新しい領域を消費するためです。もしコンテナが多くのデータを使う必要があるのであれば、データ・ボリュームを使うこともできます。

コピー開始処理は、顕著なパフォーマンスのオーバヘッド(処理時間の増加)を招きます。このオーバヘッドは、利用するストレージ・ドライバによって異なります。しかし、大きなファイル、多くのレイヤ、深いディレクトリ・ツリーが顕著な影響を与えます。幸いにも、これらの処理が行われるのは、何らかのファイルに対する変更が初めて行われた時だけです。同じファイルに対する変更が再度行われても、コピー開始処理は行われず、コンテナ・レイヤ上に既にコピーしてあるファイルに対して変更を加えます。

先ほど構築した changed-ubuntu イメージの元となる5つのコンテナに対し、何が起こっているのか見ていきましょう。

  1. Docker ホスト上のターミナルで、 次のように docker run コマンドを5回実行します。
$ docker run -dit changed-ubuntu bash
75bab0d54f3cf193cfdc3a86483466363f442fba30859f7dcd1b816b6ede82d4
$ docker run -dit changed-ubuntu bash
9280e777d109e2eb4b13ab211553516124a3d4d4280a0edfc7abf75c59024d47
$ docker run -dit changed-ubuntu bash
a651680bd6c2ef64902e154eeb8a064b85c9abf08ac46f922ad8dfc11bb5cd8a
$ docker run -dit changed-ubuntu bash
8eb24b3b2d246f225b24f2fca39625aaad71689c392a7b552b78baf264647373
$ docker run -dit changed-ubuntu bash
0ad25d06bdf6fca0dedc38301b2aff7478b3e1ce3d1acd676573bba57cb1cfef

これは changed-ubuntu イメージを元に、5つのコンテナを起動します。コンテナを作成すると、Docker は書き込みレイヤを追加し、そこに UUID を割り当てます。この値は、 docker run コマンドを実行して返ってきたものです。

  1. docker ps コマンドを実行し、5つのコンテナが実行中なのを確認します。
$ docker ps
 CONTAINER ID        IMAGE               COMMAND             CREATED              STATUS              PORTS               NAMES
0ad25d06bdf6        changed-ubuntu      "bash"              About a minute ago   Up About a minute                       stoic_ptolemy
8eb24b3b2d24        changed-ubuntu      "bash"              About a minute ago   Up About a minute                       pensive_bartik
a651680bd6c2        changed-ubuntu      "bash"              2 minutes ago        Up 2 minutes                            hopeful_turing
9280e777d109        changed-ubuntu      "bash"              2 minutes ago        Up 2 minutes                            backstabbing_mahavira
75bab0d54f3c        changed-ubuntu      "bash"              2 minutes ago        Up 2 minutes                            boring_pasteur

上記の結果から、 changed-ubuntu イメージを全て共有する5つのコンテナが実行中だと分かります。それぞれの CONTAINER ID は各コンテナ作成時の UUID から与えられています。

  1. ローカル・ストレージ領域のコンテナ一覧を表示します。
$ sudo ls containers
0ad25d06bdf6fca0dedc38301b2aff7478b3e1ce3d1acd676573bba57cb1cfef  9280e777d109e2eb4b13ab211553516124a3d4d4280a0edfc7abf75c59024d47
75bab0d54f3cf193cfdc3a86483466363f442fba30859f7dcd1b816b6ede82d4  a651680bd6c2ef64902e154eeb8a064b85c9abf08ac46f922ad8dfc11bb5cd8a
8eb24b3b2d246f225b24f2fca39625aaad71689c392a7b552b78baf264647373

Docker のコピー・オン・ライト方式により、コンテナによるディスク容量の消費を減らすだけではなく、コンテナ起動時の時間も短縮します。起動時に、Docker は各コンテナごとに薄い書き込み可能なレイヤを作成します。次の図は changed-ubuntu イメージの読み込み専用のコピーを、5つのコンテナで共有しているものです。

翻注:上記コマンドは、`/var/lib/docker`ディレクトリで実行してください。

もし新しいコンテナを開始するたびに元になるイメージ層全体をコピーしているのであれば、コンテナの起動時間とディスク使用量が著しく増えてしまうでしょう(訳者注:実際にはそうではありません。)。

データ・ボリュームとストレージ・ドライバ

コンテナの削除し、コンテナに対して書き込まれたあらゆるデータが削除されます。しかし、 データ・ボリューム の保管内容は、コンテナと一緒に削除されません。データ・ボリュームは、コンテナ内に直接マウントするファイルかディスク容量です。

データ・ボリュームはストレージ・ドライバによって管理されません。データ・ボリュームに対する読み書きは、ストレージ・ドライバを迂回し、ネイティブなホストの速度で操作できます。コンテナ内に複数のデータ・ボリュームをマウントできます。複数のコンテナが1つまたは複数のデータ・ボリュームをマウントできます。

以下の図は、1つの Docker ホストから2つのコンテナを実行しているものです。Docker ホストのローカル・ストレージ領域の中に、それぞれのコンテナに対して割り当てられた領域が存在しています。また、Docker ホスト上の /data に位置する共有データ・ボリュームもあります。このディレクトリは両方のコンテナからマウントされます。

共有ボリューム

データ・ボリュームは Docker ホスト上のローカル・ストレージ領域の外に存在しており、ストレージ・ドライバの管理から独立して離れています。コンテナを削除したとしても、Docker ホスト上の共有データ・ボリュームに保管されたデータに対して、何ら影響はありません。

データ・ボリュームに関する更に詳しい情報は、 コンテナでデータを管理する をご覧ください。