为了更有效地使用存储驱动,你必须理解Docker是如何构建和存储镜像的。然后,你需要对镜像是如何被容器使用作个了解。最后,你需要一段关于镜像和容器共同使用的技术的简洁的介绍。
每个Docker镜像引用一个或多个代表文件系统差异的只读数据层。数据层彼此堆叠来组成容器的根文件系统。下面的图表表示Ubuntu 15.04镜像由4个堆叠的数据层组成。
Docker存储驱动负责堆叠这些数据层和提供一个单独的统一视图。
当你创建一个容器,同时也在底层堆栈顶部创建了一个新的,薄的,可写的数据层。这个数据层也称为”容器数据层(container layer)“。所有对运行容器的更改 – 如写新文件,更新文件和删除文件 – 都是写到这个数据层。下面的图表显示基于Ubuntu 15.04镜像的容器。
Docker 1.10引入了一个新的内容寻址存储模型。这是一个全新的方法来定位硬盘上的镜像和数据层数据。之前的版本,镜像和数据层通过使用随机生成的UUID来引用。在这个新的模型使用了安全哈希(secure content hash)来代替。
新的模式提高了安全性,提供了一个内置的方式来避免ID冲突,并且在pull,push,load,save后保证数据完整性。同时也通过允许镜像(即使它们不是由相同的Dockerfile构建)自由地共享它们的数据层来获取更好的使用体验。
在使用新模式前需要注意的是:
那些使用早期Docker版本创建和拉取的镜像,在与新模式一起使用前需要进行迁移。迁移操作涉及计算新的安全checksum,这个操作是在你首次启动新的Docker版本时自动完成的。迁移完成后,所有的镜像都会具有全新的安全ID。
虽然迁移是自动和透明的,但是要使用比较多的计算资源。意味着当你有许多镜像需要计算时要花费比较长的时间。在这期间Docker daemon不会响应其它请求。
Docker为此把迁移工具单独出来,允许你在升级Docker之前先把镜像迁移好。这样可以避免长时间的停机时间。
迁移工具以容器方式运行,可以到这里下载https://github.com/docker/v1.10-migrator/releases。
如果你使用的是默认的docker数据路径,手动迁移命令如下:
$ sudo docker run --rm -v /var/lib/docker:/var/lib/docker docker/v1.10-migrator
如果你用的是devicemapper存储驱动,你需要添加–privileged参数来让容器可以访问存储设备。
下面是在Docker 1.9.1,AUFS存储驱动的环境下使用迁移工具的示例.Docker主机运行在配置为1 vCPU, 1GB RAM以及单独的8G SSD的t2.micro AWS EC2实例。Docker数据目录(/var/lib/docker)占用2GB空间。
$ docker images REPOSITORY TAG IMAGE ID CREATED SIZE jenkins latest 285c9f0f9d3d 17 hours ago 708.5 MB mysql latest d39c3fa09ced 8 days ago 360.3 MB mongo latest a74137af4532 13 days ago 317.4 MB postgres latest 9aae83d4127f 13 days ago 270.7 MB redis latest 8bccd73928d9 2 weeks ago 151.3 MB centos latest c8a648134623 4 weeks ago 196.6 MB ubuntu 15.04 c8be1ac8145a 7 weeks ago 131.3 MB $ sudo du -hs /var/lib/docker 2.0G /var/lib/docker $ time docker run --rm -v /var/lib/docker:/var/lib/docker docker/v1.10-migrator Unable to find image 'docker/v1.10-migrator:latest' locally latest: Pulling from docker/v1.10-migrator ed1f33c5883d: Pull complete b3ca410aa2c1: Pull complete 2b9c6ed9099e: Pull complete dce7e318b173: Pull complete Digest: sha256:bd2b245d5d22dd94ec4a8417a9b81bb5e90b171031c6e216484db3fe300c2097 Status: Downloaded newer image for docker/v1.10-migrator:latest time="2016-01-27T12:31:06Z" level=debug msg="Assembling tar data for 01e70da302a553ba13485ad020a0d77dbb47575a31c4f48221137bb08f45878d from /var/lib/docker/aufs/diff/01e70da302a553ba13485ad020a0d77dbb47575a31c4f48221137bb08f45878d" time="2016-01-27T12:31:06Z" level=debug msg="Assembling tar data for 07ac220aeeef9febf1ac16a9d1a4eff7ef3c8cbf5ed0be6b6f4c35952ed7920d from /var/lib/docker/aufs/diff/07ac220aeeef9febf1ac16a9d1a4eff7ef3c8cbf5ed0be6b6f4c35952ed7920d" <snip> time="2016-01-27T12:32:00Z" level=debug msg="layer dbacfa057b30b1feaf15937c28bd8ca0d6c634fc311ccc35bd8d56d017595d5b took 10.80 seconds" real 0m59.583s user 0m0.046s sys 0m0.008s
Unix time命令放在docker run命令前面来统计其运行时间。正如你所看到了,迁移大小为2GB的7个镜像总体时间将近1分钟。不过这个时间包括了拉取镜像/docker/v1.10-migrator镜像的时间(大约为3.5秒)。同样的操作在一个配置为40 vCPUs, 160GB RAM和一个8GB SSD的m4.10xlarge EC2 instance实例花费的时间少得多:
real 0m9.871s user 0m0.094s sys 0m0.021s
这个示例表明了迁移操作耗费的时间受机器硬件配置的影响。
容器和镜像的主要区别是顶部的可写数据层。所有对容器进行文件添加或文件更新的操作都会存储到这个可写数据层。当容器被删除后,这个可写数据层也被删除了。而底层的镜像仍然不变。
由于每个容器有自己的可写容器数据层,并且所有的更改都储存到这个数据层,意味着多个容器可以共享访问同一个底层镜像且有它们自己的数据状态。下面的图表显示多个容器共享一个相同的Ubuntu 15.04镜像。
Docker存储驱动负责激活和管理镜像数据层和可写容器数据层。不同的存储驱动处理这两个数据层的方式有所不同。Docker镜像和容器管理背后两个关键技术是可堆叠镜像数据层和写时拷贝(copy-on-write)。
写时拷贝策略与共享和复制类似。需要相同数据的系统进程共享该数据,而不是各自拥有自己的副本。在某些时候,如果一个进程需要更新或写入数据,操作系统就为该进程拷贝一份数据使用。只有需要写入数据的系统有权限访问数据副本。所有其它进程继续使用原始的数据。
Docker对镜像和容器都使用了写时拷贝技术。写时拷贝策略优化了镜像硬盘占用和容器启动时间的性能。接下来我们来看看写时拷贝技术是如何通过共享和复制影响镜像和容器的。
现在我们来了解镜像数据层和写入拷贝技术。所有的镜像和容器数据层存储在由存储驱动管理的Docker主机本地存储区域内。在基于Linux的Docker主机这个目录是/var/lib/docker/。
当使用docker pull和docker push拉取和推送镜像时,docker客户端将输出镜像数据层报告。下面的命令是从Docker Hub拉取ubuntu:15.04镜像。
$ 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
从输出中我们看到命令实际上拉取了4个镜像数据层。上面的每一行列出了一个镜像数据层和它的UUID或加密散列。这4个数据层混合组成了ubuntu:15.04 Docker镜像。
每一个数据层都存储在Docker主机本地存储区域内的它们自己的目录。
Docker 1.10之前的版本把数据层存储在与它们ID相同名称的目录中。不过对于使用docker 1.10和之后的版本拉取镜像的情况并非如此。例如,下面的命令显示从Docker Hub拉取一个镜像,并列出Docker 1.9.1的一个目录文件列表。
$ docker pull ubuntu:15.04 15.04: Pulling from library/ubuntu 47984b517ca9: Pull complete df6e891a3ea9: Pull complete e65155041eed: Pull complete c8be1ac8145a: Pull complete Digest: sha256:5e279a9df07990286cce22e1b0f5b0490629ca6d187698746ae5e28e604a640e Status: Downloaded newer image for ubuntu:15.04 $ ls /var/lib/docker/aufs/layers 47984b517ca9ca0312aced5c9698753ffa964c2015f2a5f18e5efa9848cf30e2 c8be1ac8145a6e59a55667f573883749ad66eaeef92b4df17e5ea1260e2d7356 df6e891a3ea9cdce2a388a2cf1b1711629557454fd120abd5be6d32329a0e0ac e65155041eed7ec58dea78d90286048055ca75d41ea893c7246e794389ecf203
注意看四个目录是如何与下载的镜像的数据层ID匹配的。现在比较下由docker 1.10完成同样的操作的表现。
$ 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 $ ls /var/lib/docker/aufs/layers/ 1d6674ff835b10f76e354806e16b950f91a191d3b471236609ab13a930275e24 5dbb0cbe0148cf447b9464a358c1587be586058d9a4c9ce079320265e2bb94e7 bef7199f2ed8e86fa4ada1309cfad3089e0542fec8894690529e4c04a7ca2d73 ebf814eccfe98f2704660ca1d844e4348db3b5ccc637eb905d4818fbfb00a06a
我们看到四个目录与镜像数据层ID并不匹配。尽管docker 1.10之前与之后的版本镜像管理有不同之处,不过所有的docker版本仍然能在镜像之间共享数据层。例如,如果你拉取一个与已经拉取下来的镜像拥有一些共同的数据层的镜像,Docker会检查到这个并只拉取本地没有的数据层。在这之后,两个镜像共享一些相同的数据层。
你可以自己做试验来说明。对你刚拉取下来的ubuntu:15.04镜像做一个更改,然后基于这个更改构建一个新的镜像。做这个操作的其中一个方法是使用Dockerfile和docker build命令。
1.在一个空目录创建一个以ubuntu:15.04镜像开始的Dockerfile。
FROM ubuntu:15.04
2.添加一个内容为”hello world”在/tmp目录的”newfile”文件。Dockerfile类似如下:
FROM ubuntu:15.04 RUN echo "Hello world" > /tmp/newfile
3.保存Dockerfile并关闭文件。
4.在Dockerfile相同目录的终端,执行如下命令:
$ docker build -t changed-ubuntu . Sending build context to Docker daemon 2.048 kB Step 1 : FROM ubuntu:15.04 ---> 3f7bcee56709 Step 2 : RUN echo "Hello world" > /tmp/newfile ---> Running in d14acd6fad4e ---> 94e6b7d2c720 Removing intermediate container d14acd6fad4e Successfully built 94e6b7d2c720
上面显示新镜像的ID为94e6b7d2c720。
5.执行docker images来检查新的changed-ubuntu镜像是否在Docker主机本地存储区域。
REPOSITORY TAG IMAGE ID CREATED SIZE changed-ubuntu latest 03b964f68d06 33 seconds ago 131.4 MB ubuntu 15.04 013f3d01d247 6 weeks ago 131.3 MB
6.执行docker history命令来查看哪些数据层用来创建这个新的changed-ubuntu镜像。
$ docker history changed-ubuntu
IMAGE CREATED CREATED BY SIZE COMMENT 94e6b7d2c720 2 minutes ago /bin/sh -c echo "Hello world" > /tmp/newfile 12 B 3f7bcee56709 6 weeks ago /bin/sh -c #(nop) CMD ["/bin/bash"] 0 B <missing> 6 weeks ago /bin/sh -c sed -i 's/^#s*(deb.*universe)$/ 1.879 kB <missing> 6 weeks ago /bin/sh -c echo '#!/bin/sh' > /usr/sbin/polic 701 B <missing> 6 weeks ago /bin/sh -c #(nop) ADD file:8e4943cd86e9b2ca13 131.3 MB
docker history命令输出显示新的94e6b7d2c720镜像数据层在顶部。你知道这个数据层是由于Dockerfile中的echo “Hello world” > /tmp/newfile命令添加的。下面的4个镜像数据层与组成ubuntu:15.04的数据层是一样的。
注意到新的changed-ubuntu镜像没有它自己每个数据层的拷贝。从如下图表看到,新的镜像与ubuntu:15.04镜像4个底层数据层共享。
docker history命令也显示了每个镜像数据层的大小。如你所见,94e6b7d2c720数据层只消耗了12字节的空间。意味着我们刚才创建的changed-ubuntu镜像只占用了docker主机12字节的空间 – 94e6b7d2c720数据层以下的所有数据层都以存在docker主机上并与其它镜像共享。
镜像数据层的共享使得docker镜像和容器如此的节省空间。
你早先学到了一个容器与镜像的区别是容器多了一个可写数据层。下面的图表显示了基于ubuntu:15.04的容器的数据层:
所有对容器的更改都会存储到这个薄的可写容器数据层。其它的数据层是不能修改的只读的镜像数据层。意味着多个容器能安全地共享一个底层镜像。下面的图表显示多个容器镜像一个ubuntu:15.04镜像。每一个容器有它自己的可写数据层。
当容器内的一个存在的文件被修改时,docker使用存储驱动来完成写时拷贝操作。操作的细节取决于存储驱动程序。对于AUFS和OverlayFS存储驱动,写时拷贝的操作类似如下:
Btrfs, ZFS和其它驱动处理写时拷贝有所不同。你可以之后阅读这些驱动的详细说明。
一个copy-up操作可能导致明显的性能开销。开销的不同取决于使用的存储驱动。不过,大文件,大量数据层和尝试目录树会影响更显着。幸运的是,操作只发生在第一次修改任何特定文件时。随后对同一个文件的修改不会引起一个copy-up操作,而是对存在于容器数据层的这个文件直接修改。
让我们看看如果我们根据我们之前创建的更改的ubuntu映像启动5个容器会发生什么:
1.从Docker主机上的终端,运行以下docker run命令5次。
$ docker run -dit changed-ubuntu bash 75bab0d54f3cf193cfdc3a86483466363f442fba30859f7dcd1b816b6ede82d4 $ docker run -dit changed-ubuntu bash 9280e777d109e2eb4b13ab211553516124a3d4d4280a0edfc7abf75c59024d47 $ docker run -dit changed-ubuntu bash a651680bd6c2ef64902e154eeb8a064b85c9abf08ac46f922ad8dfc11bb5cd8a $ docker run -dit changed-ubuntu bash 8eb24b3b2d246f225b24f2fca39625aaad71689c392a7b552b78baf264647373 $ docker run -dit changed-ubuntu bash 0ad25d06bdf6fca0dedc38301b2aff7478b3e1ce3d1acd676573bba57cb1cfef
这将根据更改的ubuntu映像启动5个容器。 随着每个容器的创建,Docker添加一个可写层,并为其分配一个随机UUID。 这是从docker run命令返回的值。
2.运行docker ps命令以验证5个容器是否正在运行。
$ docker ps CONTAINER ID IMAGE COMMAND CREATED STATUS PORTS NAMES 0ad25d06bdf6 changed-ubuntu "bash" About a minute ago Up About a minute stoic_ptolemy 8eb24b3b2d24 changed-ubuntu "bash" About a minute ago Up About a minute pensive_bartik a651680bd6c2 changed-ubuntu "bash" 2 minutes ago Up 2 minutes hopeful_turing 9280e777d109 changed-ubuntu "bash" 2 minutes ago Up 2 minutes backstabbing_mahavira 75bab0d54f3c changed-ubuntu "bash" 2 minutes ago Up 2 minutes boring_pasteur
上面的输出显示了5个正在运行的容器,它们都共享更改的ubuntu映像。 每个CONTAINER ID在创建每个容器时从UUID派生。
3.列出本地存储区的内容。
$ sudo ls /var/lib/docker/containers 0ad25d06bdf6fca0dedc38301b2aff7478b3e1ce3d1acd676573bba57cb1cfef 9280e777d109e2eb4b13ab211553516124a3d4d4280a0edfc7abf75c59024d47 75bab0d54f3cf193cfdc3a86483466363f442fba30859f7dcd1b816b6ede82d4 a651680bd6c2ef64902e154eeb8a064b85c9abf08ac46f922ad8dfc11bb5cd8a 8eb24b3b2d246f225b24f2fca39625aaad71689c392a7b552b78baf264647373
Docker的写时拷贝策略不仅减少了容器所消耗的空间量,而且还减少了启动容器所需的时间。 在开始时,Docker只需为每个容器创建可写层。 下图显示了这5个容器共享更改的ubuntu映像的一个只读(RO)副本。
如果Docker在每次启动一个新容器时都必须创建底层映像堆栈的整个副本,那么容器启动时间和磁盘空间将大大增加。
当容器被删除时,写入到容器中的未存储在数据卷中的任何数据与容器一起被删除。
数据卷是Docker主机文件系统中直接挂载到容器中的目录或文件。 数据卷不受存储驱动程序控制。 对数据卷的读取和写入绕过存储驱动程序,并以本机主机速度运行。 你可以将任意数量的数据卷装载到容器中。 多个容器还可以共享一个或多个数据卷。
下图显示了运行两个容器的单个Docker主机。 每个容器存在于Docker主机本地存储区(/var/lib/docker/ …)内的自己的地址空间内。 Docker主机上的/data还有一个共享数据卷。 它直接安装在两个容器中。
数据卷驻留在Docker主机上的本地存储区域之外,进一步增强了它们与存储驱动程序控制的独立性。 当容器被删除时,存储在数据卷中的任何数据都会保留在Docker主机上。