イメージ、コンテナ、ストレージ・ドライバについて

ストレージ・ドライバを効率よく利用するためには、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 イメージに基づいて生成されたコンテナを表わしています。

Docker イメージレイヤ

ストレージドライバー というものは、そういった各レイヤーが互いにやり取りできるようにします。 さまざまなストレージドライバーが利用可能であり、利用状況に応じて一長一短があります。

コンテナとレイヤ

コンテナとイメージの大きな違いは、最上部に書き込みレイヤがあるかどうかです。 コンテナに対して新たに加えられたり修正されたりしたデータは、すべてこの書き込みレイヤに保存されます。 コンテナが削除されると、その書き込みレイヤも同じく削除されます。 ただしその元にあったイメージは、変更されずに残ります。

複数のコンテナを見た場合、そのコンテナごとに個々の書き込み可能なコンテナ・レイヤがあって、データ更新結果はそのコンテナ・レイヤに保存されます。 したがって複数コンテナでは、同一のイメージを共有しながらアクセスすることができ、しかも個々に見れば独自の状態を持つことができることになります。 以下の図は、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 は単純に足し合わせで計算できるものではありません。 これはディスク総量を多く見積もってしまい、その量は無視できないほどになることがあります。

起動しているコンテナすべてが利用するディスク総量は、各コンテナの sizevirtual 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 imagedocker history コマンドを使ってみると、共有されているレイヤに対する暗号化 ID は同一になっていることがわかります。

  1. 新規に cow-test/ というディレクトリを生成して移動します。

  2. cow-test/ ディレクトリにて、以下の内容で新規ファイルを生成します。

    #!/bin/sh
    echo "Hello world"
    

    ファイルを保存して実行可能にします。

    chmod +x hello.sh
    
  1. 前述した 1 つめの Dockerfile の内容を、新規ファイル Dockerfile.base にコピーします。
  1. 前述した 2 つめの Dockerfile の内容を、新規ファイル Dockerfile にコピーします。
  1. 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
  1. 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
    
  1. 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
    
  1. それぞれのイメージに含まれるレイヤを確認します。

    $ 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 では動作しません。

  1. 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
    
  1. 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
    
  1. ローカルの保存ディレクトリの内容を一覧表示します。

    $ sudo ls /var/lib/docker/containers
    
    1a174fc216cccf18ec7d4fe14e008e30130b11ede0f0f94a87982e310cf2e765
    1e7264576d78a3134fbaf7829bc24b1d96017cf2bc046b7cd8b08b5775c33d0c
    38fa94212a419a082e6a6b87a8e2ec4a44dd327d7069b85892a707e3fc818544
    c36785c423ec7e0422b2af7364a7ba4da6146cbba7981a0951fcc3fa0430c409
    dcad7101795e4206e637d9358a818e5c32e13b349e62b00bf05cd5a4343ea513
    
  1. 各サイズを確認します。

    $ 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 ホスト上の共有データ・ボリュームに保管されたデータに対して、何ら影響はありません。

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