ストレージ ドライバ について¶
ストレージ ドライバと Docker ボリュームの比較¶
頻繁なデータの書き込みには Docker ボリュームを使うと、データをコンテナの利用期間を超えて保持されるようになります。また、データはコンテナ間でも共有可能になります。データの保持と性能改善に ボリュームを使う方法は ボリュームのセクション をご覧ください。
イメージ と レイヤー ¶
Docker
# syntax=docker/dockerfile:1
FROM ubuntu:18.04
LABEL org.opencontainers.image.authors="org@example.com"
COPY . /app
RUN make /app
RUN rm -r $HOME/.cache
CMD python /app/app.py
この Dockerfile には4つの命令が入っています。命令とは、ファイルシステムを変更して、レイヤーを作成するものです。 FROM
命令文は、 ubuntu:18.04
イメージからレイヤーを作り始めます。 LABEL
命令は、イメージの COPY
命令は、 Docker クライアントが現在いる(カレント)ディレクトリにあるファイルを追加します。1つめの RUN
命令は、 make
コマンドを使ってアプリケーションを構築し、それから、構築結果を新しいレイヤーに書き込みます。最終的に、 CMD
命令で、その(イメージを使って実行する)コンテナ内で何のコマンドを実行するか指定しますが、イメージのメタデータを変更するだけであり、新しいイメージを作成しません。
それぞれのレイヤーは、直前のレイヤーからの差分(違い)だけが集まったものです。そのため注意点としては、ファイルの追加と削除の結果、どちらも新しいレイヤーが作成されます。先の例では、 $HOME/.cache
ディレクトリを削除しましたが、それ以前のレイヤーには残ったままであり、イメージの合計容量に加えられます。効率的なイメージのために Dockerfile を最適化する方法は、 Dockerfile を書くベスト プラクティス と マルチステージ ビルドを使う のセクションをご覧ください。
レイヤーとは、各レイヤーの一番上に積み上げられます。新しいコンテナを作成すると、元のレイヤー上に新しい ubuntu:15.04
イメージを元にするコンテナを表します。
. . A storage driver handles the details about the way these layers interact with each other. Different storage drivers are available, which have advantages and disadvantages in different situations.
これらのレイヤーが、相互にやりとりできるようにする手法の詳細を、 ストレージ ドライバ が扱います。いろいろなストレージドライバが利用できますが、利用状況によってメリットとデメリットがあります。
コンテナ と レイヤー ¶
それぞれのコンテナは、自身の書き込み可能なコンテナ レイヤーを持ちます。また、このコンテナ レイヤーに全ての変更が保存されます。そのため、複数のコンテナが同じ元イメージを共有しながらアクセスでき、さらに、それぞれが自身のデータ状態を持てます。下図は、複数のコンテナが同じ Ubuntu 15.04 イメージを共有するのを表します。
注釈
複数のコンテナが、まったく同じデータに対し共有アクセスする必要がある場合は、 Docker ボリュームを使います。ボリュームについて学ぶには ボリュームのセクション をご覧ください。
Docker はストレージ・ドライバを利用して、イメージ・レイヤと書き込み可能なコンテナ・レイヤの各内容を管理します。 さまざまなストレージ・ドライバでは、異なる実装によりデータを扱います。 しかしどのようなドライバであっても、積み上げ可能な(stackable)イメージ・レイヤを取り扱い、コピー・オン・ライト(copy-on-write; CoW)方式を採用します。
ディスク上のコンテナ容量¶
実行しているコンテナの、おおよその docker ps -s
コマンドが使えます。容量に関連する2つの列があります。
size
(容量):各コンテナの書き込み可能なレイヤーが使用する、(ディスク上の)データ量virtual size``(仮想容量):コンテナによって使われている読み込み専用イメージが使用するデータ量に、コンテナの書き込み可能レイヤー ``size
(容量)を加えたもの。複数のコンテナは、複数もしくは全ての読み込み専用イメージデータを共有する場合があります。同じイメージを使い、2つのコンテナ起動すると、読み込み専用データの 100% を共有します。一方で、共通するレイヤーを持つものの異なるイメージを使い、2つのコンテナを起動すると、共通するレイヤのみ共有します。つまり、仮想容量を合計できません。容量が少なくない可能性があるため、合計ディスク容量は多く見積もられます。
全ての実行しているコンテナが、ディスク上で使用している合計ディスク容量は、おおよそ各コンテナの size
と virtual size
値を合計した値です。完全に同じイメージから複数コンテナを起動した場合、各コンテナのディスク上での合計容量は、「合計」( コンテナの size
)に、1つのイメージ容量( virtual size
- size
)を加えたものです。
コンテナが次の手法でディスク容量を確保する場合は、(容量として)カウントしません。
ロギング ドライバ によって保存されるファイルは、ディスク容量を使用しない。ただし、コンテナが大容量のログデータを生成し、ログローテートを設定していなけrば、問題になる可能性がある
コンテナによって使われるボリュームとバインド マウント
コンテナ用の設定ファイルは、非常に小さいため、ディスク容量を使わない
ディスクに書き込まれるメモリ(スワップ機能が有効な場合)
実験的な checkpoint/restore 機能を使っている場合のチェックポイント
コピー オン ライト (CoW) 方式¶
共有がイメージを小さくする¶
リポジトリからのイメージ docker pull
を使う時や、イメージからコンテナを作成する時にローカルでイメージが存在していなければ、それぞれのレイヤーを別々に取得し、Docker のローカル保管領域に保存します。これは、 Linux ホスト上であれば、通常 /var/lib/docker/
です。これらの取得するレイヤーは、以下の例から確認できます。
$ docker pull ubuntu:18.04
18.04: Pulling from library/ubuntu
f476d66f5408: Pull complete
8882c27f669e: Pull complete
d9af21273955: Pull complete
f5029279ec12: Pull complete
Digest: sha256:ab6cb8de3ad7bb33e2534677f865008535427390b117d7939193f8d1a6613e34
Status: Downloaded newer image for ubuntu:18.04
Docker ホストのローカル保存領域内に、それぞれのレイヤーが保管されます。ファイルシステム上のレイヤーを調べるには、 /var/lib/docker/<storage-driver>
の内容を確認します。こちらの例は overlay2
ストレージ ドライバを使います。
$ ls /var/lib/docker/overlay2
16802227a96c24dcbeab5b37821e2b67a9f921749cd9a2e386d5a6d5bc6fc6d3
377d73dbb466e0bc7c9ee23166771b35ebdbe02ef17753d79fd3571d4ce659d7
3f02d96212b03e3383160d31d7c6aeca750d2d8a1879965b89fe8146594c453d
ec1ec45792908e90484f7e629330666e7eee599f08729c93890a7205a6ba35f5
l
ディレクトリ名とレイヤー ID は対応していません。
2つの異なる Dockerfile を持っていると想像してください。1つめは acme/my-base-image:1.0
という名前のイメージを作成します。
# syntax=docker/dockerfile:1
FROM alpine
RUN apk add --no-cache bash
2つめのイメージは、 acme/my-base-image:1.0
を元にしますが、レイヤーを追加します。
# syntax=docker/dockerfile:1
FROM acme/my-base-image:1.0
COPY . /app
RUN chmod +x /app/hello.sh
CMD /app/hello.sh
2つめのイメージは、1つめのイメージからのレイヤーを全て含み、さらに CMD
と RUN
命令によって作成された新しいレイヤーと、読み書き可能なコンテナレイヤーが追加されました。Docker は1つめのイメージのレイヤーを既に全て持っているため、再度取得する必要はありません。共通しているレイヤーがあれば、すべて2つのイメージで共有します。
2つの Dockerfile からイメージを構築すると、 docker image ls
と docker image history
コマンドで、共有しているレイヤーの暗号化 ID が同じだと分かります。
新しいディレクトリ
cow-test/
を作成し、そこに移動します。
cow-test/
内で、hello.sh
という名前のファイルを作成し、以下の内容にします。#!/usr/bin/env bash echo "Hello world"
1つめの Dockerfile として、
Dockerfile.base
という名前の新しいファイルに、先の内容をコピーします。
2つめの Dockerfile として、
Dockerfile
という名前のファイルに、先の内容をコピーします。
cow-test/
ディレクトリ内で、1つめのイメージを構築します。コマンドの最後に.
を入れるのを忘れないでください。これは Docker に対して、イメージに追加する必要がある、あらゆるファイルを探す場所を伝えます。$ docker build -t acme/my-base-image:1.0 -f Dockerfile.base . [+] Building 6.0s (11/11) FINISHED => [internal] load build definition from Dockerfile.base 0.4s => => transferring dockerfile: 116B 0.0s => [internal] load .dockerignore 0.3s => => transferring context: 2B 0.0s => resolve image config for docker.io/docker/dockerfile:1 1.5s => [auth] docker/dockerfile:pull token for registry-1.docker.io 0.0s => CACHED docker-image://docker.io/docker/dockerfile:1@sha256:9e2c9eca7367393aecc68795c671... 0.0s => [internal] load .dockerignore 0.0s => [internal] load build definition from Dockerfile.base 0.0s => [internal] load metadata for docker.io/library/alpine:latest 0.0s => CACHED [1/2] FROM docker.io/library/alpine 0.0s => [2/2] RUN apk add --no-cache bash 3.1s => exporting to image 0.2s => => exporting layers 0.2s => => writing image sha256:da3cf8df55ee9777ddcd5afc40fffc3ead816bda99430bad2257de4459625eaa 0.0s => => naming to docker.io/acme/my-base-image:1.0 0.0s
2つめのイメージを構築します。
$ docker build -t acme/my-final-image:1.0 -f Dockerfile . [+] Building 3.6s (12/12) FINISHED => [internal] load build definition from Dockerfile 0.1s => => transferring dockerfile: 156B 0.0s => [internal] load .dockerignore 0.1s => => transferring context: 2B 0.0s => resolve image config for docker.io/docker/dockerfile:1 0.5s => CACHED docker-image://docker.io/docker/dockerfile:1@sha256:9e2c9eca7367393aecc68795c671... 0.0s => [internal] load .dockerignore 0.0s => [internal] load build definition from Dockerfile 0.0s => [internal] load metadata for docker.io/acme/my-base-image:1.0 0.0s => [internal] load build context 0.2s => => transferring context: 340B 0.0s => [1/3] FROM docker.io/acme/my-base-image:1.0 0.2s => [2/3] COPY . /app 0.1s => [3/3] RUN chmod +x /app/hello.sh 0.4s => exporting to image 0.1s => => exporting layers 0.1s => => writing image sha256:8bd85c42fa7ff6b33902ada7dcefaaae112bf5673873a089d73583b0074313dd 0.0s => => naming to docker.io/acme/my-final-image:1.0
イメージの容量を確認します。
$ docker image ls REPOSITORY TAG IMAGE ID CREATED SIZE acme/my-final-image 1.0 8bd85c42fa7f About a minute ago 7.75MB acme/my-base-image 1.0 da3cf8df55ee 2 minutes ago 7.75MB
各イメージの履歴を確認します。
$ docker image history acme/my-base-image:1.0 IMAGE CREATED CREATED BY SIZE COMMENT da3cf8df55ee 5 minutes ago RUN /bin/sh -c apk add --no-cache bash # bui… 2.15MB buildkit.dockerfile.v0 <missing> 7 weeks ago /bin/sh -c #(nop) CMD ["/bin/sh"] 0B <missing> 7 weeks ago /bin/sh -c #(nop) ADD file:f278386b0cef68136… 5.6MB
ステップのいくつかは容量がありません(
0B
)。これは、メタデータのみが変更されたもので、イメージレイヤーは作成されておらず、メタデータ自身の容量以外は一切かかりません。先ほどの例では、このイメージは2つのイメージレイヤーで構成されています。$ docker image history acme/my-final-image:1.0 IMAGE CREATED CREATED BY SIZE COMMENT 8bd85c42fa7f 3 minutes ago CMD ["/bin/sh" "-c" "/app/hello.sh"] 0B buildkit.dockerfile.v0 <missing> 3 minutes ago RUN /bin/sh -c chmod +x /app/hello.sh # buil… 39B buildkit.dockerfile.v0 <missing> 3 minutes ago COPY . /app # buildkit 222B buildkit.dockerfile.v0 <missing> 4 minutes ago RUN /bin/sh -c apk add --no-cache bash # bui… 2.15MB buildkit.dockerfile.v0 <missing> 7 weeks ago /bin/sh -c #(nop) CMD ["/bin/sh"] 0B <missing> 7 weeks ago /bin/sh -c #(nop) ADD file:f278386b0cef68136… 5.6MB
Notice that all steps of the first image are also included in the final image. The final image includes the two layers from the first image, and two layers that were added in the second image. 1つめのイメージのステップ全てが、最終イメージにも含まれているのに注目します。最終イメージには、1つめのイメージにある2つのレイヤーを含んでおり、これは、2つめのイメージによって追加されたものです。
注釈
<missing> ステップとは何でしょうか?
docker history
の出力にある<missing>
行が示すのは、それらのステップが、他のシステムで構築されて、 Docker Hub から取得したalpine
イメージの一部であるか、あるいは、 BuildKit をビルダー として構築されたかのどちらかです。BuildKit 以前は、「古い 」ビルダーはキャッシュ用途のため各ステップごとに、どちらも新しい「中間 」イメージを作成していました。そしてIMAGE
列でイメージの ID が見えていました。BuildKit は自身のキャッシュ機構を使うため、キャッシュ用途での中間イメージを必要としません。BuildKit での他の拡張モードについて学ぶには BuildKit でイメージを構築 をご覧ください。
各イメージのレイヤーを確認します。
docker image inspect
コマンドを使い、各イメージ内にあるレイヤーの暗号化 ID を表示します。$ docker image inspect --format "{{json .RootFS.Layers}}" acme/my-base-image:1.0 [ "sha256:72e830a4dff5f0d5225cdc0a320e85ab1ce06ea5673acfe8d83a7645cbd0e9cf", "sha256:07b4a9068b6af337e8b8f1f1dae3dd14185b2c0003a9a1f0a6fd2587495b204a" ]
$ docker image inspect --format "{{json .RootFS.Layers}}" acme/my-final-image:1.0 [ "sha256:72e830a4dff5f0d5225cdc0a320e85ab1ce06ea5673acfe8d83a7645cbd0e9cf", "sha256:07b4a9068b6af337e8b8f1f1dae3dd14185b2c0003a9a1f0a6fd2587495b204a", "sha256:cc644054967e516db4689b5282ee98e4bc4b11ea2255c9630309f559ab96562e", "sha256:e84fb818852626e89a09f5143dbc31fe7f0e0a6a24cd8d2eb68062b904337af4" ]
はじめの2つのレイヤーは、どちらも同じイメージを示しているのに注目してください。2つめのイメージには、2つの追加されたレイヤーが入っています。共有イメージレイヤーは唯一
/var/lib/docker
に保管され、また、イメージレジストリからの取得や送信時にも共有されます。共有イメージレイヤーは、このためにネットワーク帯域と容量を減らせられます。Tip
Tip: Docker コマンドに「--format」オプションで出力を成形
先の例では、レイヤー ID を JSON 配列形式で成形するため、
docker image inspect
コマンドに--format
オプションを付けて使いました。Docker コマンドの--format
オプションは強力な機能であり、awk
やsed
のような追加ツールを必要としなくても、出力結果を展開し、指定した情報に成形(フォーマット)できます。--format
フラグを docker 御マンドの出力に使い、出力形式を変えるには コマンドとログ出力の成形 セクションをご覧ください。また、読みやすさのために jq ユーティリティ も使って JSON 出力を見やすくしています。
コピーでコンテナの効率化¶
コンテナの起動時、薄い書き込み可能なレイヤーが、他のレイヤー上に追加されます。コンテナのファイルシステムに対するあらゆる変更は、そこ(のレイヤー)に保存されます。コンテナが変更しないファイルは、この書き込み可能なレイヤーにコピーされません。つまり、書き込み可能なレイヤーは可能な限り小さくします。
コンテナ内に存在するファイルを変更すると、ストレージ ドライバは overlay2
、 overlay
、 auts
ドライバでは、コピー オン ライト処理の大まかな手順は以下の通りです。
イメージレイヤーで更新するファイルを検索する。この処理は、最も新しいレイヤーから始まり、一度に1つのレイヤーずつ、ベースレイヤーまで処理する。対象が見つかると、後の処理を高速化するため、キャッシュに追加する。
ファイルが見つかると、最初にファイルをコピーする
copy_up
処理を開始し、その見つかったファイルをコンテナの書き込み可能なレイヤーにコピーします。あらゆる変更は、このコピーしたファイルに対して行われます。そして、コンテナからは下位のレイヤーに存在していた、読み込み専用のファイルを見られません。
Btrfs、ZFS 、その他のドライバは、コピー オン ライトを異なる方法で扱います。各ドライバの手法については、後述する詳細で読めます。
多くのデータを書き込むコンテナは、そうでないコンテナと比べて、容量をたくさん消費します。これは、コンテナの書き込み可能な最上位レイヤー内で、多くの書き込み処理が行われるためです。注意点として、ファイルのパーミッションや所有者の変更のような、ファイルのメタデータの変更でも、結果的に copy_up
処理を行いますので、書き込み可能なレイヤーにファイルが重複して存在します。
Tip
Tip: 書き込みが多いアプリケーションにはボリュームを使う
書き込みが多いアプリケーションでは、コンテナ内にデータを保存すべきではありません。書き込みが多いデータベース ストレージのようなアプリケーション、特に読み込み専用のレイヤーに以前からあらかじめデータが存在している場合は、問題が起こりうるのが分かっています。
その代わりに、 Docker ボリュームを使います。これは、実行中のコンテナとは独立し、効率的な I/O となるよう設計されています。さらに、ボリュームはコンテナ間で共有でき、コンテナの書き込み可能なレイヤーの容量も増えません。ボリュームについて学ぶには ボリュームの使用 を参照ください。
copy_up
処理によって、顕著なパフォーマンスのオーバーヘッドを招く可能性があります。このオーバーヘッドとは、使用しているストレージ ドライバに依存し異なります。大きなファイル、多くのレイヤー、深いディレクトリ階層は、より顕著な影響を与える可能性があります。これを軽減するため、各 copy_up
処理は、対象ファイルを変更した初回のみ行われます。
コピー オン ライトがどのように行われるかを確認するため、以下の手順では、先ほど構築した acme/my-final-image:1.0
イメージを元にしたコンテナを5つ起動し、とれだけ場所を取るか確認します。
.. From a terminal on your Docker host, run the following docker run commands. The strings at the end are the IDs of each container.
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 40ebdd7634162eb42bdb1ba76a395095527e9c0aa40348e6c325bd0aa289423c a5ff32e2b551168b9498870faf16c9cd0af820edf8a5c157f7b80da59d01a107 3ed3c1a10430e09f253704116965b01ca920202d52f3bf381fbb833b8ae356bc 939b3bf9e7ece24bcffec57d974c939da2bdcc6a5077b5459c897c1e2fa37a39 cddae31c314fbab3f7eabeb9b26733838187abc9a2ed53f97bd5b04cd7984a5a
docker ps
コマンドに--size
オプションをつけ、5つのコンテナが実行中なのを確認し、それぞれのコンテナの容量も調べます。$ docker ps --size --format "table {{.ID}}\t{{.Image}}\t{{.Names}}\t{{.Size}}" CONTAINER ID IMAGE NAMES SIZE cddae31c314f acme/my-final-image:1.0 my_container_5 0B (virtual 7.75MB) 939b3bf9e7ec acme/my-final-image:1.0 my_container_4 0B (virtual 7.75MB) 3ed3c1a10430 acme/my-final-image:1.0 my_container_3 0B (virtual 7.75MB) a5ff32e2b551 acme/my-final-image:1.0 my_container_2 0B (virtual 7.75MB) 40ebdd763416 acme/my-final-image:1.0 my_container_1 0B (virtual 7.75MB)
上で表示した出力は、全てのコンテナがイメージの読み込み専用レイヤー (7.75MB) を共有していますが、コンテナのファイルシステムには何もデータがないため、コンテナに対する追加容量は使われていません。
注釈
高度なトピック:コンテナ用に使うメタデータとログの保存場所
注意:この手順は Docker デーモンのファイル保存場所にアクセスする必要があるため、 Linux マシンが必要です。 Docker Desktop for Mac や Docker Desktop for Windows では動作しません。
docker ps
の出力は、コンテナの書き込み可能なレイヤーによって消費される、ディスク容量の情報を提供します。しかし、各コンテナ用のメタデータとログファイルを保管する情報を含みません。 より詳細を把握するには、 Docker デーモンの保存場所(デフォルトでは/var/lib/docker
)を調査します。$ sudo du -sh /var/lib/docker/containers/* 36K /var/lib/docker/containers/3ed3c1a10430e09f253704116965b01ca920202d52f3bf381fbb833b8ae356bc 36K /var/lib/docker/containers/40ebdd7634162eb42bdb1ba76a395095527e9c0aa40348e6c325bd0aa289423c 36K /var/lib/docker/containers/939b3bf9e7ece24bcffec57d974c939da2bdcc6a5077b5459c897c1e2fa37a39 36K /var/lib/docker/containers/a5ff32e2b551168b9498870faf16c9cd0af820edf8a5c157f7b80da59d01a107 36K /var/lib/docker/containers/cddae31c314fbab3f7eabeb9b26733838187abc9a2ed53f97bd5b04cd7984a5a
これらの各コンテナは、ファイルシステム上に 36k の容量を使っています。
コンテナごとの保存場所
確認をするため、以下のコマンドを実行すると、コンテナ
my_container_1
、my_container_2
、my_container_3
内の書き込み可能なレイヤー上に、「hello」の文字を書きます。$ for i in {1..3}; do docker exec my_container_$i sh -c 'printf hello > /out.txt'; done
それからもう一度
docker ps
コマンドを実行すると、それぞれのコンテナが 5 バイトづつ新しく消費しているのがわかります。このデータはコンテナごとにユニークであり、共有されません。コンテナの読み込み専用のレイヤーは影響をうけず、全てのコンテナは共有されたままです。$ docker ps --size --format "table {{.ID}}\t{{.Image}}\t{{.Names}}\t{{.Size}}" CONTAINER ID IMAGE NAMES SIZE cddae31c314f acme/my-final-image:1.0 my_container_5 0B (virtual 7.75MB) 939b3bf9e7ec acme/my-final-image:1.0 my_container_4 0B (virtual 7.75MB) 3ed3c1a10430 acme/my-final-image:1.0 my_container_3 5B (virtual 7.75MB) a5ff32e2b551 acme/my-final-image:1.0 my_container_2 5B (virtual 7.75MB) 40ebdd763416 acme/my-final-image:1.0 my_container_1 5B (virtual 7.75MB)
上の例が示すのは、コピー オン ライト ファイルシステムが、コンテナをいかに効率化しているかです。コピー オン ライトは容量を確保するだけでなく、コンテナ起動時の時間も短縮します。コンテナ(あるいは、同じイメージから複数のコンテナ)の作成時、Docker が必要とするのは
もしも Docker が新しいコンテナの作成時、毎回元になるイメージの全体をコピーしていれば、コンテナの起動時間やディスク使用量が著しく増えるでしょう。これは、仮想マシンごとに1つまたは複数の仮想ディスクを必要とする、仮想マシンの処理と似ています。 vfs ストレージ は CoW ファイルシステムや他の最適化を提供しません。このストレージ ドライバの使用時は、作成するコンテナごとにイメージデータを丸コピーします。