:ruby:`ストレージ ドライバ <storage driver>` について
:ruby:`ストレージ ドライバ <storage driver>` を効率的に使うには、Docker がどのようしてイメージを構築・保存するのかを知り、そして、コンテナによってどのようにしてイメージが使われるのかを知るのは重要です。この知識を使えば、この先、アプリケーションがデータを保持しながら、パフォーマンスの問題をさけるための、ベストな方法を選択できるようになります。
ストレージ ドライバと Docker ボリュームの比較
:ruby:`イメージ レイヤー <image layer>` へのデータ保管と、コンテナの :ruby:`書き込み可能なレイヤー <writable layer>` にデータを保管するため、Docker はストレージ ドライバを使います。コンテナを削除すると、コンテナの書き込み可能なレイヤーは :ruby:`保持 <persist>` されません。ですが、実行時に生成される :ruby:`一時的なデータ <ephemeral data>` の保管には適しています。ストレージ ドライバは容量効率を最適化していますが、(ストレージ ドライバに依存しますが)本来の(ネイティブな)ファイルシステム性能よりも書き込み速度が低くなります。特に :ruby:`コピー オン ライト <copy-on-write>` ファイルシステムを使用するストレージ ドライバでは顕著です。データベースのような書き込みが集中するアプリケーションでは、パフォーマンスのオーバヘッド(性能上、余計な時間やリソースを必要)に影響があります。あらかじめデータが存在する :ruby:`読み込み専用のレイヤー <read-only laer>` の場合は、なおさらです。
頻繁なデータの書き込みには Docker ボリュームを使うと、データをコンテナの利用期間を超えて保持されるようになります。また、データはコンテナ間でも共有可能になります。データの保持と性能改善に ボリュームを使う方法は ボリュームのセクション をご覧ください。
:ruby:`イメージ <image>` と :ruby:`レイヤー <layer>`
Docker :ruby:`イメージ <image>` とは、積み重なる :ruby:`レイヤー <layer>` の組み合わせです。それぞれのレイヤーは、イメージの Dockerfile 内の命令に相当します。それぞれのレイヤーは最後の1つを除き、読み込み専用です。以下の Dockerfile を見てみましょう。
# 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 命令は、イメージの :ruby:`メタデータ <metadata>` を変更するだけで、新しいレイヤーを作成しません。 COPY 命令は、 Docker クライアントが現在いる(カレント)ディレクトリにあるファイルを追加します。1つめの RUN 命令は、 make コマンドを使ってアプリケーションを構築し、それから、構築結果を新しいレイヤーに書き込みます。最終的に、 CMD 命令で、その(イメージを使って実行する)コンテナ内で何のコマンドを実行するか指定しますが、イメージのメタデータを変更するだけであり、新しいイメージを作成しません。
それぞれのレイヤーは、直前のレイヤーからの差分(違い)だけが集まったものです。そのため注意点としては、ファイルの追加と削除の結果、どちらも新しいレイヤーが作成されます。先の例では、 $HOME/.cache ディレクトリを削除しましたが、それ以前のレイヤーには残ったままであり、イメージの合計容量に加えられます。効率的なイメージのために Dockerfile を最適化する方法は、 Dockerfile を書くベスト プラクティス と マルチステージ ビルドを使う のセクションをご覧ください。
レイヤーとは、各レイヤーの一番上に積み上げられます。新しいコンテナを作成すると、元のレイヤー上に新しい :ruby:`書き込み可能なレイヤー <writable layer>` を追加します。このレイヤーは、たいてい「 :ruby:`コンテナ レイヤー <container layer>` 」と呼ばれます。新しいファイルの書き込み、既存ファイルの変更、ファイルの削除といった、実行中のコンテナに対する全ての変更は、この :ruby:`薄い <thin>` 書き込み可能なコンテナ レイヤーに書き込まれます。下図は 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.
これらのレイヤーが、相互にやりとりできるようにする手法の詳細を、 ストレージ ドライバ が扱います。いろいろなストレージドライバが利用できますが、利用状況によってメリットとデメリットがあります。
:ruby:`コンテナ <container>` と :ruby:`レイヤー <layer>`
:ruby:`コンテナ <container>` と :ruby:`レイヤー <layer>` の重要な違いは、最上部の書き込み可能なレイヤーです。新しいデータの書き込みや既存データの変更など、全ての書き込みは、この書き込み可能なレイヤー内に保存されます。また、コンテナが削除されると、その(コンテナが使用していた)書き込み可能なレイヤーも削除されます。ただし、元のイメージは変更されず、そのまま残ります。
それぞれのコンテナは、自身の書き込み可能なコンテナ レイヤーを持ちます。また、このコンテナ レイヤーに全ての変更が保存されます。そのため、複数のコンテナが同じ元イメージを共有しながらアクセスでき、さらに、それぞれが自身のデータ状態を持てます。下図は、複数のコンテナが同じ Ubuntu 15.04 イメージを共有するのを表します。
注釈
複数のコンテナが、まったく同じデータに対し共有アクセスする必要がある場合は、 Docker ボリュームを使います。ボリュームについて学ぶには ボリュームのセクション をご覧ください。
Docker はストレージ・ドライバを利用して、イメージ・レイヤと書き込み可能なコンテナ・レイヤの各内容を管理します。 さまざまなストレージ・ドライバでは、異なる実装によりデータを扱います。 しかしどのようなドライバであっても、積み上げ可能な(stackable)イメージ・レイヤを取り扱い、コピー・オン・ライト(copy-on-write; CoW)方式を採用します。
ディスク上のコンテナ容量
実行しているコンテナの、おおよその :ruby:`容量 <size>` を表示するには、 docker ps -s コマンドが使えます。容量に関連する2つの列があります。
size(容量):各コンテナの書き込み可能なレイヤーが使用する、(ディスク上の)データ量virtual size``(仮想容量):コンテナによって使われている読み込み専用イメージが使用するデータ量に、コンテナの書き込み可能レイヤー ``size(容量)を加えたもの。複数のコンテナは、複数もしくは全ての読み込み専用イメージデータを共有する場合があります。同じイメージを使い、2つのコンテナ起動すると、読み込み専用データの 100% を共有します。一方で、共通するレイヤーを持つものの異なるイメージを使い、2つのコンテナを起動すると、共通するレイヤのみ共有します。つまり、仮想容量を合計できません。容量が少なくない可能性があるため、合計ディスク容量は多く見積もられます。
全ての実行しているコンテナが、ディスク上で使用している合計ディスク容量は、おおよそ各コンテナの size と virtual size 値を合計した値です。完全に同じイメージから複数コンテナを起動した場合、各コンテナのディスク上での合計容量は、「合計」( コンテナの size )に、1つのイメージ容量( virtual size - size )を加えたものです。
コンテナが次の手法でディスク容量を確保する場合は、(容量として)カウントしません。
ロギング ドライバ によって保存されるファイルは、ディスク容量を使用しない。ただし、コンテナが大容量のログデータを生成し、ログローテートを設定していなけrば、問題になる可能性がある
コンテナによって使われるボリュームとバインド マウント
コンテナ用の設定ファイルは、非常に小さいため、ディスク容量を使わない
ディスクに書き込まれるメモリ(スワップ機能が有効な場合)
実験的な checkpoint/restore 機能を使っている場合のチェックポイント
:ruby:`コピー オン ライト <copy-on-write>` (CoW) 方式
:ruby:`コピー オン ライト <copy-on-write>` とは、ファイルの共有とコピーの効率を最大化するための方式です。ファイルやディレクトリがイメージ内の :ruby:`低位のレイヤー <lower layer>` に存在し、それを他のレイヤー(書き込み可能なレイヤーを含みます)が読み込む必要がある場合は、その既存のファイルを使います。他のレイヤーから、そのファイルを始めて変更する時(イメージの構築時や、コンテナの実行中の場合)は、そのレイヤーの中にファイルがコピーされます。これが I/O を最小化し、以降に続く各レイヤーの容量も最小化します。これらの利点については、以降で詳しく説明します。
コピーでコンテナの効率化
コンテナの起動時、薄い書き込み可能なレイヤーが、他のレイヤー上に追加されます。コンテナのファイルシステムに対するあらゆる変更は、そこ(のレイヤー)に保存されます。コンテナが変更しないファイルは、この書き込み可能なレイヤーにコピーされません。つまり、書き込み可能なレイヤーは可能な限り小さくします。
コンテナ内に存在するファイルを変更すると、ストレージ ドライバは :ruby:`コピー オン ライト <copy-on-write>` 処理を行います。この関連する具体的な手順は、指定されたストレージドライバに依存します。 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 が必要とするのは :ruby:`薄い書き込み可能なレイヤー <thin writable container layer>` の作成だけです。
もしも Docker が新しいコンテナの作成時、毎回元になるイメージの全体をコピーしていれば、コンテナの起動時間やディスク使用量が著しく増えるでしょう。これは、仮想マシンごとに1つまたは複数の仮想ディスクを必要とする、仮想マシンの処理と似ています。 vfs ストレージ は CoW ファイルシステムや他の最適化を提供しません。このストレージ ドライバの使用時は、作成するコンテナごとにイメージデータを丸コピーします。
関連情報
参考
- About storage drivers