変更履歴
2014/04/20 公開
2014/04/27 構成情報ファイルの説明追加
2014/06/15 dm-thinprovisiongのデバイスメタデータファイル変更
背景
先だって、「Linuxコンテナ(LXC)の基礎をまとめ直す」というコラムに、「来るべきDockerの波に向けて、まずは、コンテナの基礎を理解しましょう!」的な話を書きました。この中で、比較的に原始的なコンテナ利用法として、「RHEL6.2のlibvirtからLinuxコンテナを利用」という記事を紹介しています。
この記事では、busyboxを使った簡易httpサーバのコンテナを起動していますが、この手順に従うと(気づく人は)容易に気づくのが、コンテナに見せるファイルシステムの準備がいかに面倒か、という事実です。コンテナから見えるルートファイルシステムは、基本的には、ホスト上の特定のディレクトリにchrootした状態に過ぎません。したがって、この特定のディレクトリの下には、アプリケーションの実行に必要なあらゆるファイル(動的ライブラリなど)を突っ込んでおく必要があります。(先の記事では、この手順が面倒なので、動的ライブラリが一切不要なbusyboxでお茶を濁していたりします。)
また、苦労して作成したファイルシステムをどうメンテナンスするかも悩ましくなります。まさか、tarballに固めてバックアップですか?
実はDockerでは、このあたりの問題を解決、というか、うまくやりくりできるように、「イメージリポジトリ」や「スナップショット」などの独自の概念を取り入れています。概念的には面白そうなのですが、インフラエンジニアであれば、当然(!)、イメージの実体がどこに格納されていて、どういう技術でスナップショットが作成されているのか気になりますよね。(というか、それを知らずに使っていたら、トラブルでイメージが破損した時にどうやって対応してよいのか、最悪、どうやって個別ファイルのサルベージに取り組んでいいのか分かりませんよね。。。。)
なお、Dockerのディスクイメージの実装には、いくつかの方式があります。ここでは、あくまで、RHEL7での実装を前提とします。RHEL6/CentOS6なども同じ方式です。
参考資料
・これから始める「DockerでかんたんLAMP環境 for CentOS」
・thin-provisioning.txt
・Resizing Docker containers with the Device Mapper plugin
・LVM thin povisioning
環境準備
RHEL7Betaを最小構成でインストールしたら、EPELからdocker-ioを導入して、dockerサービスを起動します。「bridge-utils, net-tools」は必須ではないですが、無いと不便なので入れています。
# yum -y install bridge-utils net-tools # yum -y install http://download.fedoraproject.org/pub/epel/beta/7/x86_64/epel-release-7-0.1.noarch.rpm # yum -y install docker-io # systemctl enable docker.service # systemctl start docker.service
ここで、サービスを起動したら間髪入れずに、次のコマンドを叩きます。
# ps -efww ... root 2237 2203 22 17:10 ? 00:00:00 mkfs.ext4 -E discard,lazy_itable_init=0,lazy_journal_init=0 /dev/mapper/docker-253:0-15516-base ... # lsblk NAME MAJ:MIN RM SIZE RO TYPE MOUNTPOINT ... loop0 7:0 0 100G 0 loop └docker-253:0-15516-pool 253:2 0 100G 0 dm └docker-253:0-15516-base 253:3 0 10G 0 dm loop1 7:1 0 2G 0 loop └docker-253:0-15516-pool 253:2 0 100G 0 dm └docker-253:0-15516-base 253:3 0 10G 0 dm
なにやら謎の10GBのデバイス「/dev/mapper/docker-253:0-15516-base」がext4でフォーマットされています。実は、このデバイスは、Dockerが扱う全てのイメージのベースとなるデバイスです。Dockerで新規のイメージを作成すると、この後で説明する、Thin-Provisioningの機能により、このベースイメージのスナップショットを作成して、それを新規イメージとして利用します。そのため、Dockerのイメージはすべて、10GBに揃えられているわけです。
それでは、ここで、CentOS6のイメージをダウンロードして、ローカルリポジトリに登録します。
# docker pull centos
ここでもダウンロードが始まったら、別の端末から、おもむろに次のコマンドを叩きます。
# lsblk NAME MAJ:MIN RM SIZE RO TYPE MOUNTPOINT ... loop0 7:0 0 100G 0 loop └docker-253:0-15516-pool 253:2 0 100G 0 dm ├docker-253:0-15516-base 253:3 0 10G 0 dm └docker-253:0-15516-539c0211cd76cdeaedbecf9f023ef774612e331137ce7ebe4ae1b61088e7edbe 253:4 0 10G 0 dm /var/lib/docker/devicemapper/mnt/539c0211cd76cdeaedbecf9f023ef774612e331137ce7 loop1 7:1 0 2G 0 loop └docker-253:0-15516-pool 253:2 0 100G 0 dm ├docker-253:0-15516-base 253:3 0 10G 0 dm └docker-253:0-15516-539c0211cd76cdeaedbecf9f023ef774612e331137ce7ebe4ae1b61088e7edbe 253:4 0 10G 0 dm /var/lib/docker/devicemapper/mnt/539c0211cd76cdeaedbecf9f023ef774612e331137ce7 # df ファイルシス 1K-ブロック 使用 使用可 使用% マウント位置 ... /dev/dm-4 10190100 75716 9573712 1% /var/lib/docker/devicemapper/mnt/539c0211cd76cdeaedbecf9f023ef774612e331137ce7ebe4ae1b61088e7edbe /dev/dm-5 10190100 42864 9606564 1% /var/lib/docker/devicemapper/mnt/0b443ba0395813ef287c27f5ff953121a69ab23c467ffbefcefaec3f255e0693
またもや謎の10GBのデバイスができて、さらにマウントされています。これが、先のベースイメージから作成したスナップショットで、これをマウントしてダウンロードしたCentOS6のイメージを展開しているわけです。
ダウンロードが完了すると、lsblkコマンドでは見えなくなりますが、その実体はちゃんと残っています。この実体を見つけるために、Device MapperによるThin-Provisioning機能を少し勉強することにしましょう。
Device MapperによるThin-Provisioning機能
ものすごーく、簡単に言うと、データ用のデバイスとメタデータ用の(小さな)デバイスを用意して、これを1つのストレージプールとして扱う機能です。このプールから任意のサイズの論理デバイスを切り出して、使用することが可能になります。
この時、切り出した論理デバイスに対して、その論理サイズ分のブロックを事前に確保することはしません。デバイスの使用量が増えるにしたがって、順次、プールのデバイスから領域を割り当てていきます。そのため、例えば、データ用デバイスが100GBであっても、(実使用量が100GBを超えない限り)、論理サイズ10GBのデバイスを10個以上切り出すことが可能です。また、スナップショット機能を利用して、既存のデバイスをコピーして、新規デバイスを作成することも可能です。論理的には完全に独立したコピーとなるので、スナップショットであることを忘れて、別々のデバイスとして利用することが可能です。
この機能は、Device Mapperのカーネルモジュールとして提供されています。
# lsmod | grep dm_thin dm_thin_pool 55788 2 dm_persistent_data 61832 1 dm_thin_pool dm_bio_prison 15501 1 dm_thin_pool dm_mod 102999 15 dm_log,dm_persistent_data,dm_mirror,dm_bufio,dm_thin_pool
ネイティブに取り扱う際は、dmsetupコマンドを駆使する必要がありますが、最近のLVMは、これのWrapper機能を持っているので、LVMのコマンドを通して利用することが可能です。RHEL6でLVM経由でThin-Provisioningを使用する際は、こちらのドキュメントを参照ください。
ただし!
Dockerでは、どういうわけだか、LVMを使わずに、ネイティブな方法でThin-Provisiongのデバイスを利用しています。そのため、Dockerがこっそり用意するThin-Provisiongデバイスを覗きこむためには、dmsetupコマンドを駆使する必要があります。そこで、ちょっとだけ練習をしてみます。
まず、Dockerが使っているThin-Provisioningデバイスがあるとややこしいので、Dockerを停止しておきます。
# systemctl stop docker
1GBのデータデバイス用と10MBのメタデータデバイス用のイメージファイルを用意して、ループバックデバイスに紐付けます。
# fallocate -l $((1024*1024*1024)) /root/data_device.img # fallocate -l $((1024*1024*10)) /root/metadata_device.img # losetup -f /root/data_device.img # losetup -f /root/metadata_device.img # losetup NAME SIZELIMIT OFFSET AUTOCLEAR RO BACK-FILE /dev/loop0 0 0 0 0 /root/data_device.img /dev/loop1 0 0 0 0 /root/metadata_device.img
ここで次のコマンドを実行すると、用意したデバイスを使って、Thin-Provisioning用のプール「mypool」を作成して、さらにそこから、論理デバイスを切り出します。
# dmsetup create mypool --table "0 $((1024*1024*1024/512)) thin-pool /dev/loop1 /dev/loop0 512 8192" # dmsetup message /dev/mapper/mypool 0 "create_thin 0"
最後の「0」はこの論理デバイスにユニークな「デバイスID」になります。この時点では、論理デバイスのサイズは確定していません。この後、サイズとデバイスIDを指定して論理デバイスを有効化すると、実際に使用できるようになります。この時、デバイス名を指定すると、デバイスファイル「/dev/mapper/<デバイス名>」が割り当てられます。
# dmsetup create mydevice --table "0 $((1024*1024*100/512)) thin /dev/mapper/mypool 0"
次のようにlsblkコマンドで、デバイスの様子が確認できます。
# lsblk NAME MAJ:MIN RM SIZE RO TYPE MOUNTPOINT ... loop0 7:0 0 1G 0 loop └mypool 253:2 0 1G 0 dm └mydevice 253:3 0 100M 0 dm loop1 7:1 0 10M 0 loop └mypool 253:2 0 1G 0 dm └mydevice 253:3 0 100M 0 dm
できた論理デバイスは、普通にフォーマットして使うことができます。
# mkfs.ext4 /dev/mapper/mydevice # mount /dev/mapper/mydevice /mnt # date > /mnt/test.txt
使い終わったら無効化しておきます。
# umount /mnt # dmsetup remove mydevice
もちろん、再度、有効化して利用することも可能です。
# dmsetup create mydevice --table "0 $((1024*1024*100/512)) thin /dev/mapper/mypool 0" # mount /dev/mapper/mydevice /mnt
という感じで、Thin-Provisioningデバイスの使い方が分かった所で、定義したプールを消して後片付けしておきましょう。
# umount /mnt # dmsetup remove mydevice # dmsetup remove mypool # losetup -d /dev/loop0 # losetup -d /dev/loop1
先ほど停止したDockerサービスも再開しておきます。
# systemctl start docker
Dockerが利用するThin-Provisioningデバイス
それでは、Dockerが内部的に使用しているThin-Provisioningデバイスを確認してみましょう。まず、10秒スリープするだけのコンテナを起動します。
# docker run centos sleep 10
この時、別の端末でデバイスの様子を確認すると、次のようになっています。
# losetup NAME SIZELIMIT OFFSET AUTOCLEAR RO BACK-FILE /dev/loop0 0 0 1 0 /var/lib/docker/devicemapper/devicemapper/data /dev/loop1 0 0 1 0 /var/lib/docker/devicemapper/devicemapper/metadata # lsblk ... loop0 7:0 0 100G 0 loop └─docker-253:0-15516-pool 253:2 0 100G 0 dm └─docker-253:0-15516-91711c50926a5ea1379c9b4a129f7883a18dbf4318d8e4a2b23a043deb1e8869 253:4 0 10G 0 dm /var/lib/docker/devicemapper/mnt/91711c50926a5ea1379c9b4a12 loop1 7:1 0 2G 0 loop └─docker-253:0-15516-pool 253:2 0 100G 0 dm └─docker-253:0-15516-91711c50926a5ea1379c9b4a129f7883a18dbf4318d8e4a2b23a043deb1e8869 253:4 0 10G 0 dm /var/lib/docker/devicemapper/mnt/91711c50926a5ea1379c9b4a12 # df ファイルシス 1K-ブロック 使用 使用可 使用% マウント位置 ... /dev/dm-4 10190100 356596 9292832 4% /var/lib/docker/devicemapper/mnt/91711c50926a5ea1379c9b4a129f7883a18dbf4318d8e4a2b23a043deb1e8869
先に練習でThin-Provisioningデバイスを作成した時の様子を思い出すと、何がおきているかはすぐに分かります。まず、下記にある「data」「metadata」がプール用のデータデバイスとメタデータデバイスのイメージファイルです。
# ls -lh /var/lib/docker/devicemapper/devicemapper/ 合計 963M -rw-------. 1 root root 100G 4月 20 19:02 data -rw-------. 1 root root 1.5K 4月 20 19:02 json -rw-------. 1 root root 2.0G 4月 20 19:02 metadata
このプール内にある論理デバイスは、作成時にデバイス番号が割り振られていますが、その情報は、上記のjsonファイルに記録されています。これは、Dockerが独自に記録しています。ちょいと、整形して出力すると次のようになります。
# cat /var/lib/docker/devicemapper/devicemapper/json | python -mjson.tool { "Devices": { "": { "device_id": 0, "initialized": true, "size": 10737418240, "transaction_id": 1 }, "0b443ba0395813ef287c27f5ff953121a69ab23c467ffbefcefaec3f255e0693": { "device_id": 8, "initialized": true, "size": 10737418240, "transaction_id": 13 }, ... "f6f690c42f33b7f55f9886f06552ea694b873cea4f898f97816337fdc6e4bdca-init": { "device_id": 11, "initialized": true, "size": 10737418240, "transaction_id": 16 } } }
謎のUUIDっぽいものは、この後で見るDockerとしてのイメージのIDです。それぞれについて、Thin-PorvisioningデバイスとしてのIDと論理サイズなどが記録されています。
2014/06/15 追記
Docker1.0では、上記のjsonファイルは、イメージごとに分割して、「/var/lib/docker/devicemapper/metadata/」というファイルに保存されるように変更されています。
では、DockerとしてのイメージIDは、どのように管理されているのでしょうか? dockerコマンドで確認すると先頭の12バイトだけ表示されます。
# docker images REPOSITORY TAG IMAGE ID CREATED VIRTUAL SIZE centos centos6 0b443ba03958 3 days ago 297.6 MB centos latest 0b443ba03958 3 days ago 297.6 MB centos 6.4 539c0211cd76 12 months ago 300.6 MB
リポジトリ「centos」に属する3つのイメージがありますが、これらの元の情報は、実は次のJsonファイルです。こちらからは、ID全体が分かります。
# cat /var/lib/docker/repositories-devicemapper | python -mjson.tool { "Repositories": { "centos": { "6.4": "539c0211cd76cdeaedbecf9f023ef774612e331137ce7ebe4ae1b61088e7edbe", "centos6": "0b443ba0395813ef287c27f5ff953121a69ab23c467ffbefcefaec3f255e0693", "latest": "0b443ba0395813ef287c27f5ff953121a69ab23c467ffbefcefaec3f255e0693" } } }
ではここで、既存のイメージを修正した独自イメージを作成してみます。次はCentOSのコンテナを起動して、「/root/file.txt」を作成した後に60秒間スリープします。
# docker run centos /bin/bash -c 'echo "Hello, World!" > /root/file.txt; sleep 60'
起動中のコンテナが使用しているイメージは、コンテナが終了すると削除されるので、スリープしている間にイメージのスナップショットを取って保存します。
# docker ps CONTAINER ID IMAGE COMMAND CREATED STATUS PORTS NAMES d11a82703373 centos:centos6 "/bin/bash -c 'echo 6 seconds ago Up 5 seconds dreamy_mcclintock # docker commit d11a82703373 myrepo:hello_world 99672cda56ab113015005706fe952bab5898f1d5ec58bc0863d54b58f613cba7 # docker images REPOSITORY TAG IMAGE ID CREATED VIRTUAL SIZE myrepo hello_world 99672cda56ab 2 minutes ago 297.6 MB centos centos6 0b443ba03958 3 days ago 297.6 MB centos latest 0b443ba03958 3 days ago 297.6 MB centos 6.4 539c0211cd76 12 months ago 300.6 MB
無事に保存できました。それでは、このイメージの中身をdmsetupコマンドでダイレクトに覗いてみましょう。まず、イメージIDを確認します。
# cat /var/lib/docker/repositories-devicemapper | python -mjson.tool { "Repositories": { "centos": { "6.4": "539c0211cd76cdeaedbecf9f023ef774612e331137ce7ebe4ae1b61088e7edbe", "centos6": "0b443ba0395813ef287c27f5ff953121a69ab23c467ffbefcefaec3f255e0693", "latest": "0b443ba0395813ef287c27f5ff953121a69ab23c467ffbefcefaec3f255e0693" }, "myrepo": { "hello_world": "99672cda56ab113015005706fe952bab5898f1d5ec58bc0863d54b58f613cba7" } } }
このイメージIDから、Thin-ProvisioningデバイスのIDを確認します。
# cat /var/lib/docker/devicemapper/devicemapper/json | python -mjson.tool ... "99672cda56ab113015005706fe952bab5898f1d5ec58bc0863d54b58f613cba7": { "device_id": 23, "initialized": true, "size": 10737418240, "transaction_id": 28 }, ...
ここで確認したデバイスIDとサイズの情報を変数にセットしておきましょう。先のlsblkコマンドの結果から、プールの名称は「docker-253:0-15516-pool」とわかるので、これも変数にセットしておきます。
# device_id=23 # size=10737418240 # pool=docker-253:0-15516-pool
これらの情報を利用して、この論理デバイスを手動で有効化してマウントしてみましょう。
# dmsetup create myvol --table "0 $(($size / 512)) thin /dev/mapper/$pool $device_id" # mount /dev/mapper/myvol /mnt # ls /mnt id lost+found rootfs
マウントしたファイルシステムの「rootfs」がコンテナから見えるルートファイルシステムです。次のように、先ほど作成したファイルがちゃんと入っています。
# cat /mnt/rootfs/root/file.txt Hello, World!
最後にファイルシステムをアンマウントして、論理デバイスを無効化しておきます。
# umount /mnt # dmsetup remove myvol
考察
これで、Dockerが扱うディスクイメージは、すべてがまとめて、巨大なイメージファイル「/var/lib/docker/devicemapper/devicemapper/(data|metadata)」から作ったプールに詰め込まれていることが分かりました。デフォルトで、
・プールサイズは100GB
・(すべてのイメージの元になる)ベースイメージは10GB
と決め打ちされているので、個々のイメージはすべて10GBに固定されており、かつプールが一杯になるとそれ以上コンテナが作成できなくなるという問題があります。しかも最初のプール用のファイルがスパースファイルになっているので、性能上の問題もありそうです。本番利用する際は、プール用のデバイスは、「/dev/sdb」などの物理デバイスをアサインして利用する方がよいかも知れません。このあたりのカスタマイズについては、こちらの記事を参考にしてください。
補足情報
Dockerのイメージには、コンテナを起動する際の構成情報が付随しています。これら構成情報は、イメージの実体とは別に、「/var/lib/docker/graph/
次は「dockerfile/nginx」の構成情報ですが、コンテナ起動時に最初に実行するコマンドである「Entrypoint」などの情報が含まれていることが分かります。("container_config"の方が実行時に影響するの構成情報になるようです。)
# docker images dockerfile/nginx REPOSITORY TAG IMAGE ID CREATED VIRTUAL SIZE dockerfile/nginx latest 2ba101254ac8 25 hours ago 393.6 MB [root@rhel7rc2 ~]# image_id=2ba101254ac8 && cat /var/lib/docker/graph/$image_id*/json | python -mjson.tool { "Size": 0, "architecture": "amd64", "config": { "AttachStderr": false, "AttachStdin": false, "AttachStdout": false, "Cmd": null, "CpuShares": 0, "Dns": null, "Domainname": "", "Entrypoint": [ "nginx" ], "Env": [ "HOME=/root", "PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin" ], "ExposedPorts": { "80/tcp": {} }, "Hostname": "100ddd371955", "Image": "8b8e289e3b40ffc21a4709ccc6b01a28d28c87da0137f1e497c69ed30088be45", "Memory": 0, "MemorySwap": 0, "NetworkDisabled": false, "OnBuild": [], "OpenStdin": false, "PortSpecs": null, "StdinOnce": false, "Tty": false, "User": "", "Volumes": { "/data": {}, "/etc/nginx/sites-enabled": {}, "/var/log/nginx": {} }, "VolumesFrom": "", "WorkingDir": "/etc/nginx" }, "container": "15c6538c6f265962cccc27b0e7c7fc94a6fbdd6ec2deb41d3edc0f30e4c685f2", "container_config": { "AttachStderr": false, "AttachStdin": false, "AttachStdout": false, "Cmd": [ "/bin/sh", "-c", "#(nop) EXPOSE [80]" ], "CpuShares": 0, "Dns": null, "Domainname": "", "Entrypoint": [ "nginx" ], "Env": [ "HOME=/root", "PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin" ], "ExposedPorts": { "80/tcp": {} }, "Hostname": "100ddd371955", "Image": "8b8e289e3b40ffc21a4709ccc6b01a28d28c87da0137f1e497c69ed30088be45", "Memory": 0, "MemorySwap": 0, "NetworkDisabled": false, "OnBuild": [], "OpenStdin": false, "PortSpecs": null, "StdinOnce": false, "Tty": false, "User": "", "Volumes": { "/data": {}, "/etc/nginx/sites-enabled": {}, "/var/log/nginx": {} }, "VolumesFrom": "", "WorkingDir": "/etc/nginx" }, "created": "2014-04-26T02:15:11.344952515Z", "docker_version": "0.8.1", "id": "2ba101254ac858f7926d2ee09ebdef9447acde04c6f0f118a62859825cd5523c", "os": "linux", "parent": "8b8e289e3b40ffc21a4709ccc6b01a28d28c87da0137f1e497c69ed30088be45" }