Device Mapper ストレージ・ドライバの使用

Device Mapper は、Linux 上で多くの高度なボリューム管理技術を支えるカーネル・ベースのフレームワークです。Docker の devicemapper ストレージ・ドライバは、シン・プロビジョニングとスナップショット機能のために、イメージとコンテナ管理にこのフレームワークを活用します。このセクションでは、Device Mapper ストレージ・ドライバを devicemapper と表記します。、カーネルのフレームワークを Device Mapper として言及します。

注釈

devicemapper ストレージ・ドライバを使うには、 RHEL か CentOS Linux 上で商用サポート版 Docker Engine (CS-Engine) を実行 する必要があります。

AUFS の代替

当初の Docker は、Ubuntu と Debian Linux 上で AUFS をストレージのバックエンドに使っていました。Docker が有名になるにつれ、多くの会社が Red Hat Enterprise Linux 上で使いたいと考え始めました。残念ながら、AUFS は Linux カーネル上流のメインラインではないため、RHEL は AUFS を扱いませんでした。

この状況を変えるべく、Red Hat 社の開発者らが AUFS をカーネルのメインラインに入れられるよう取り組みました。しかしながら、新しいストレージ・バックエンドを開発する方が良い考えであると決断したのです。さらに、ストレージのバックエンドには、既に存在していた Device Mapper 技術を基盤としました。

Red Hat 社は Docker 社と協同で新しいドライバの開発に取り組みました。この協調の結果、ストレージ・バックエンドの取り付け・取り外しが可能な(pluggable)Docker エンジンを再設計しました。そして、 devicemapper は Docker がサポートする2つめのストレージ・ドライバとなったのです。

Device Mapper は Linux カーネルのバージョン 2.6.9 以降、メインラインに組み込まれました。これは、RHEL ファミリーの Linux ディストリビューションの中心部です。つまり、 devicemapper ストレージ・ドライバは安定したコードを基盤としており、現実世界における多くのプロダクションへのデプロイをもたらします。また、強力なコミュニティのサポートも得られます。

イメージのレイヤ化と共有

devicemapper ドライバは、全てのイメージとコンテナを自身の仮想デバイスに保管します。これらのデバイスとは、シン・プロビジョニングされ、コピー・オン・ライト可能であり、スナップショットのデバイスです。Device Mapper 技術はファイル・レベルというよりも、ブロック・レベルで動作します。つまり、 devicemapper ストレージ・ドライバのシン・プロビジョニング(thin-provisioning)とコピー・オン・ライト(copy-on-write)処理は、ファイルではなくブロックに対して行います。

注釈

また、スナップショットは シン・デバイス(thin device)仮想デバイス(virtual device) としても参照されます。つまり、 devicemapper ストレージ・ドライバにおいては、どれも同じものを意味します。

devicemapper でイメージを作る高度な手順は、以下の通りです。

  1. devicemapper ストレージ・ドライバはシン・プール(thin pool)を作成します。

ブロック・デバイスかループ用にマウントした薄いファイル(sparse files)上に、このプールを作成します。

  1. 次に ベース・デバイス(base device) を作成します。

ベース・デバイスとは、ファイルシステムのシン・デバイス(thin device)です。どのファイルシステムが使われているかを調べるには、 docker info コマンドを実行し、 Backing filesystem 値を確認します。

  1. それぞれの新しいイメージ(とイメージ・レイヤ)は、このベース・デバイスのスナップショットです。

これらがシン・プロビジョニングされたコピー・オン・ライトなスナップショットです。つまり、これらは初期状態では空っぽですが、データが書き込まれる時だけ容量を使います。

devicemapper では、ここで作成されたイメージのスナップショットが、コンテナ・レイヤになります。イメージと同様に、コンテナのスナップショットも、シン・プロビジョニングされたコピー・オン・ライトなスナップショットです。コンテナのスナップショットに、コンテナ上での全ての変更が保管されます。 devicemapper は、コンテナに対してデータを書き込む時、このプールから必要に応じて領域を割り当てます。

以下のハイレベルな図は、ベース・デバイスのシン・プールと2つのイメージを表します。

ベース・デバイス

細かく図を見ていきますと、スナップショットは全体的に下向きなのが分かるでしょう。各イメージ・レイヤは下にあるレイヤのスナップショットです。各イメージの最も下にあるレイヤは、プール上に存在するベース・デバイスのスナップショットです。このベース・デバイスとは Device Mapper のアーティファクト(artifact;成果物の意味)であり、Docker イメージ・レイヤではありません。

コンテナとは、ここから作成したイメージのスナップショットです。下図は2つのコンテナです。一方は Ubuntu イメージをベースにし、もう一方は Busybox イメージをベースにしています。

2つの Device Mapper 上のコンテナ

devicemapper からの読み込み

devicemapper ストレージ・ドライバが、どのように読み書きしているか見ていきましょう。下図は、サンプル・コンテナが単一のブロック( 0x44f )を読み込むという、ハイレベルな手順です。

Device Mapper 上のコンテナ
  1. アプリケーションがコンテナ内のブロック 0x44f に対して読み込みを要求します。

コンテナは、イメージの薄い(thin)スナップショットであり、データを持っていません。そのかわりに、下層のイメージ層(スタック)にあるイメージのスナップショット上の、どこにデータが保管されているかを示すポインタ(PTR)を持っています。

  1. ストレージ・ドライバは、スナップショットのブロック 0xf33 と関連するイメージ・レイヤ a005... のポインタを探します。
  1. devicemapper はブロック 0xf33 の内容を、イメージのスナップショットからコンテナのメモリ上にコピーします。
  1. ストレージ・ドライバはアプリケーションがリクエストしたデータを返します。

書き込み例

devicemapper ドライバで新しいデータをコンテナに書き込むには、オンデマンドの割り当て(allocate-on-demand) を行います。コピー・オン・ライト処理により、既存のデータを更新します。Device Mapper はブロック・ベースの技術のため、これらの処理をブロック・レベルで行います。

例えば、コンテナ内の大きなファイルに小さな変更を加える時、 devicemapper ストレージ・ドライバはファイル全体コピーをコピーしません。コピーするのは、変更するブロックのみです。各ブロックは 64KB です。

新しいデータの書き込み

コンテナに 56KB の新しいデータを書き込みます。

  1. アプリケーションはコンテナに 56KB の新しいデータの書き込みを要求します。
  1. オンデマンドの割り当て処理により、コンテナのスナップショットに対して、新しい 64KB のブロックが1つ割り当てられます。

書き込み対象が 64KB よりも大きければ、複数の新しいブロックがコンテナに対して割り当てられます。

  1. 新しく割り当てられたブロックにデータを書き込みます。

既存のデータを上書き

既存のデータに対して初めて変更を加える場合、

  1. アプリケーションはコンテナ上にあるデータの変更を要求します。
  1. 更新が必要なブロックに対して、コピー・オン・ライト処理が行われます。
  1. 処理によって新しい空のブロックがコンテナのスナップショットに割り当てられ、そのブロックにデータがコピーされます。
  1. 新しく割り当てられたブロックの中に、変更したデータを書き込みます。

コンテナ内のアプリケーションは、必要に応じた割り当てやコピー・オン・ライト処理を意識しません。しかしながら、アプリケーションの読み書き処理において、待ち時間を増やすでしょう。

Device Mapper を Docker で使う設定

複数のディストリビューションにおいて、devicemapper は標準の Docker ストレージ・ドライバです。ディストリビューションには RHEL や派生したものが含まれます。現時点では、以下のディストリビューションがドライバをサポートしています。

  • RHEL/CentOS/Fedora
  • Ubuntu 12.04
  • Ubuntu 14.04
  • Debian

Docker ホストは devicemapper ストレージ・ドライバを、デフォルトでは loop-lvm モードで設定します。このモードは、イメージとコンテナのスナップショットが使うシン・プール(thin pool)を構築するために、スパース・ファイル(sparse file;まばらなファイル)を使う指定です。このモードは、設定に変更を加えることなく、革新的な動きをするように設計されています。しかしながら、プロダクションへのデプロイでは、 loop-lvm モードの下で実行すべきではありません。

どのようなモードで動作しているか確認するには docker info コマンドを使います。

$ sudo docker info
Containers: 0
Images: 0
Storage Driver: devicemapper
 Pool Name: docker-202:2-25220302-pool
 Pool Blocksize: 65.54 kB
 Backing Filesystem: xfs
 ...
 Data loop file: /var/lib/docker/devicemapper/devicemapper/data
 Metadata loop file: /var/lib/docker/devicemapper/devicemapper/metadata
 Library Version: 1.02.93-RHEL7 (2015-01-28)
 ...

この実行結果から、Docker ホストは devicemapper ストレージ・ドライバの処理に loop-lvm モードを使っているのが分かります。実際には、 データ・ループ・ファイル (data loop file)メタデータ・ループ・ファイル (Metadata loop file) のファイルが /var/lib/docker/devicemapper/devicemapper 配下にあるのを意味します。これらがループバックにマウントされているパース・ファイルです。

プロダクション用に direct-lvm モードを設定

プロダクションへのデプロイに適した設定は direct lvm モードです。このモードはシン・プールの作成にブロック・デバイスを使います。以下の手順は、Docker ホストが devicemapper ストレージ・ドライバを direct-lvm 設定で使えるようにします。

ご用心

既に Docker ホスト上で Docker デーモンを使っている場合は、イメージをどこかに保存する必要があります。そのため、処理を進める前に、それらのイメージを Docker Hub やプライベート Docker Trusted Registry に送信しておきます。

以下の手順は 90GB のデータ・ボリュームと 4GB のメタデータ・ボリュームを作成し、ストレージ・プールの基礎として使います。ここでは別のブロック・デバイス /dev/sdd を持っており、処理するための十分な空き容量があると想定しています。デバイスの識別子とボリューム・サイズは皆さんの環境とは異なるかもしれません。手順を進める時は、自分の環境にあわせて適切に置き換えてください。また、手順は Docker デーモンが停止した状態から始めるのを想定しています。

  1. 設定対象の Docker ホストにログインします。
  1. Engine のデーモンが実行中であれば、停止します。
  1. LVM(論理ボリューム・マネジメント)のバージョン2をインストールします。
$ yum install lvm2
  1. 物理ボリュームにブロック・デバイス /dev/sdd を作成します。
$ pvcreate /dev/sdd
  1. docker ボリューム・グループを作成します。
$ vgcreate docker /dev/sdd
  1. thinpool という名前のシン・プール(thin pool)を作成します。

この例では、 docker ボリューム・グループの論理データ(data logical)は 95% の大きさとします。残りの容量は、データもしくはメタデータによって空き容量が少なくなった時の一時的な退避用に使います。

$ lvcreate --wipesignatures y -n thinpool docker -l 95%VG
$ lvcreate --wipesignatures y -n thinpoolmeta docker -l 1%VG
  1. プールをシン・プールに変換します。
$ lvconvert -y --zero n -c 512K --thinpool docker/thinpool --poolmetadata docker/thinpoolmeta
  1. lvm プロフィールを経由してシン・プールを自動拡張するよう設定します。
$ vi /etc/lvm/profile/docker-thinpool.profile
  1. thin_pool_autoextend_threshold 値を指定します。

ここで指定する値は、先ほどの lvm 領域がどの程度まで到達したら、領域をどこまで自動拡張するかをパーセントで指定します(100 = 無効化です)。

thin_pool_autoextend_threshold = 80
  1. シン・プールの自動拡張が発生するタイミングを指定します。

シン・プールの領域を増やす空き容量のタイミングをパーセントで指定します(100 = 無効化です)。

thin_pool_autoextend_percent = 20
  1. 確認をします。 docker-thinpool.profile は次のように表示されます。

/etc/lvm/profile/docker-thinpool.profile ファイルの例:

activation {
    thin_pool_autoextend_threshold=80
    thin_pool_autoextend_percent=20
}
  1. 新しい lvm プロフィールを適用します。
$ lvchange --metadataprofile docker-thinpool docker/thinpool
  1. lv (論理ボリューム)をモニタしているのを確認します。
$ lvs -o+seg_monitor
  1. Docker Engine を起動していた場合は、グラフ・ドライバを直接削除します。

Docker インストール時のイメージとコンテナからグラフ・ドライバを削除します。

$ rm -rf /var/lib/docker/*
  1. Engine デーモンが devicemapper オプションを使うように設定します。

設定には2つの方法があります。デーモンの起動時にオプションを指定するには、次のようにします。

--storage-driver=devicemapper --storage-opt=dm.thinpooldev=/dev/mapper/docker-thinpool --storage-opt dm.use_deferred_removal=true

あるいは daemon.json 設定ファイルで起動時に指定も可能です。例:

{
        "storage-driver": "devicemapper",
        "storage-opts": [
                "dm.thinpooldev=/dev/mapper/docker-thinpool",
                "dm.use_deferred_removal=true"
        ]
}
  1. Docker Engine デーモンを起動します。
$ systemctl start docker

Docker Engine デーモンを起動したら、シン・プールとボリューム・グループの空き容量を確認します。ボリューム・グループは自動拡張しますので、容量を使い尽くす可能性があります。論理ボリュームを監視するには、オプションを指定せず lvs を使うか、 lvs -a でデータとメタデータの大きさを確認します。ボリューム・グループの空き容量を確認するには vgs コマンドを使います。

先ほど設定したシン・プールの閾値を越えたかどうかを確認するには、次のようにログを表示します。

journalctl -fu dm-event.service

シン・プールで問題を繰り返す場合は、 dm.min_free_spaces オプションで Engine の挙動を調整できます。この値は最小値に近づいた時、警告を出して操作させなくします。詳しい情報は ストレージ・ドライバのオプション をご覧ください。

ホスト上の devicemapper 構造の例

lsblk コマンドを使えば、先ほど作成したデバイス・ファイルと、その上に devicemapper ストレージ・ドライバによって作られた pool (プール)を確認できます。

$ sudo lsblk
NAME                       MAJ:MIN RM  SIZE RO TYPE MOUNTPOINT
xvda                       202:0    0    8G  0 disk
└─xvda1                    202:1    0    8G  0 part /
xvdf                       202:80   0   10G  0 disk
├─vg--docker-data          253:0    0   90G  0 lvm
│ └─docker-202:1-1032-pool 253:2    0   10G  0 dm
└─vg--docker-metadata      253:1    0    4G  0 lvm
  └─docker-202:1-1032-pool 253:2    0   10G  0 dm

下図は、先ほどの例で扱ったイメージの更新を、 lsblk コマンドの詳細とあわせて表しています。

ディスク構造上のイメージ

この図では、プールは Docker-202:1-1032-pool と名付けられ、先ほど作成した datametadata デバイスにわたっています。この devicemapper のプール名は、次のような形式です。

Docker-MAJ:MIN-INO-pool

MAJNININO は、デバイスのメジャー番号、マイナー番号、i ノード番号です。

Device Mapper はブロック・レベルで処理を行うため、イメージ・レイヤとコンテナ間の差分を見るのは、少し大変です。しかしながら、2つの鍵となるディレクトリがあります。 /var/lib/docker/devicemapper/mnt ディレクトリには、イメージとコンテナのマウント・ポイントがあります。 /var/lib/docker/devicemapper/metadata ディレクトリには、それぞれのイメージとコンテナのスナップショットを格納する1つのファイルがあります。このファイルには、各スナップショットのメタデータが JSON 形式で含まれています。

Device Mapper と Docker 性能

オンデマンドの割り当て(allocate-on-demand)とコピー・オン・ライト(copy-on-write)処理が、コンテナ全体の性能に対して影響があるのを理解するのは重要です。

オンデマンドの割り当てが性能に与える影響

devicemapper ストレージ・ドライバは、オンデマンドの割り当て処理時、コンテナに対して新しいブロックを割り当てます。この処理が意味するのは、コンテナの中でアプリケーションが何か書き込みをするごとに、プールから1つまたは複数の空ブロックを探し、コンテナの中に割り当てます。

全てのブロックは 64KB です。64KB より小さな書き込みの場合でも、64KB のブロックが1つ割り当てられます。これがコンテナの性能に影響を与えます。特にコンテナ内で多数の小さなファイルを書き込む場合に影響があるでしょう。しかしながら、一度ブロックがコンテナに対して割り当てられたら、以降の読み込みは対象のブロックを直接処理できます。

コピー・オン・ライトが性能に与える影響

コンテナ内のデータを初めて更新する度に、毎回 devicemapper ストレージ・ドライバがコピー・オン・ライト処理を行います。このコピーとは、イメージのスナップショット上のデータを、コンテナのスナップショットにコピーするものです。この処理が、コンテナの性能に対して留意すべき影響を与えます。

コピー・オン・ライト処理は 64KB 単位で行います。そのため、1GB のファイルのうち 32KB を更新する場合は、コンテナのスナップショット内にある 64KB のブロックをコピーします。これはファイル・レベルのコピー・オン・ライト処理に比べて、著しい性能向上をもたらします。ファイル・レベルであれば、コンテナ・レイヤに含まれる 1GB のファイル全体をコピーする必要があるからです。

しかしながら、現実的には、コンテナが多くの小さなブロック(64KB以下)に書き込みをするのであれば、 devicemapper は AUFS を使うよりも性能が劣ります。

Device Mapper の性能に対するその他の考慮

devicemapper ストレージ・ドライバの性能に対して、他にもいくつかの影響を与える要素があります。

  • 動作モード :Docker が devicemapper ストレージ・ドライバを使用する時、デフォルトのモードは loop-lvm です。このモードはスパース・ファイル(space files;薄いファイル)を使うため、性能を損ないます。そのため、loop-lvmプロダクションへのデプロイに推奨されていません 。プロダクション環境で推奨されるモードは direct-lvm です。これはストレージ・ドライバが直接 raw ブロック・デバイスに書き込みます。
  • 高速なストレージ :ベストな性能を出すためには、 データ・ファイルメタデータ・ファイル を、 SSD のような高速なストレージ上に配置すべきです。あるいは、 SAN や NAS アレイといった、ダイレクト・アタッチ・ストレージでも同様でしょう。
  • メモリ使用量 : Docker ストレージ・ドライバの中で、メモリ使用効率が最も悪いのが devicemapper です。同じコンテナのコピーをn個起動する時、n個のファイルをメモリ上にコピーします。これは、Docker ホスト上のメモリに対して影響があります。そのため、 PaaS や他の高密度な用途には、devicemapper ストレージ・ドライバがベストな選択肢とは言えません。

最後に1点、データ・ボリュームは最上かつ最も予測可能な性能を提供します。これは、ストレージ・ドライバを迂回し、シン・プロビジョニングやコピー・オン・ライト処理を行わないためです。そのため、データ・ボリューム上で重たい書き込みを行うのに適しています。