Docker用户指南(3) – 理解镜像,容器和存储驱动

为了更有效地使用存储驱动,你必须理解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构建)自由地共享它们的数据层来获取更好的使用体验。
在使用新模式前需要注意的是:

  • 1.迁移现有的镜像
  • 2.镜像和数据层文件系统结构
  • 那些使用早期Docker版本创建和拉取的镜像,在与新模式一起使用前需要进行迁移。迁移操作涉及计算新的安全checksum,这个操作是在你首次启动新的Docker版本时自动完成的。迁移完成后,所有的镜像都会具有全新的安全ID。
    虽然迁移是自动和透明的,但是要使用比较多的计算资源。意味着当你有许多镜像需要计算时要花费比较长的时间。在这期间Docker daemon不会响应其它请求。
    Docker为此把迁移工具单独出来,允许你在升级Docker之前先把镜像迁移好。这样可以避免长时间的停机时间。
    迁移工具以容器方式运行,可以到这里下载https://github.com/docker/v1.10-migrator/releases。
    如果你使用的是默认的docker数据路径,手动迁移命令如下:

    1. $ 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空间。

    1. $ docker images
    2.  
    3. REPOSITORY          TAG                 IMAGE ID            CREATED             SIZE
    4. jenkins             latest              285c9f0f9d3d        17 hours ago        708.5 MB
    5. mysql               latest              d39c3fa09ced        8 days ago          360.3 MB
    6. mongo               latest              a74137af4532        13 days ago         317.4 MB
    7. postgres            latest              9aae83d4127f        13 days ago         270.7 MB
    8. redis               latest              8bccd73928d9        2 weeks ago         151.3 MB
    9. centos              latest              c8a648134623        4 weeks ago         196.6 MB
    10. ubuntu              15.04               c8be1ac8145a        7 weeks ago         131.3 MB
    11.  
    12. $ sudo du -hs /var/lib/docker
    13.  
    14. 2.0G    /var/lib/docker
    15.  
    16. $ time docker run --rm -v /var/lib/docker:/var/lib/docker docker/v1.10-migrator
    17.  
    18. Unable to find image 'docker/v1.10-migrator:latest' locally
    19. latest: Pulling from docker/v1.10-migrator
    20. ed1f33c5883d: Pull complete
    21. b3ca410aa2c1: Pull complete
    22. 2b9c6ed9099e: Pull complete
    23. dce7e318b173: Pull complete
    24. Digest: sha256:bd2b245d5d22dd94ec4a8417a9b81bb5e90b171031c6e216484db3fe300c2097
    25. Status: Downloaded newer image for docker/v1.10-migrator:latest
    26. time="2016-01-27T12:31:06Z" level=debug
    27. time="2016-01-27T12:31:06Z" level=debug
    28. <snip>
    29. time="2016-01-27T12:32:00Z" level=debug
    30.  
    31. real    0m59.583s
    32. user    0m0.046s
    33. 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实例花费的时间少得多:

    1. real    0m9.871s
    2. user    0m0.094s
    3. 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镜像。

    1. $ docker pull ubuntu:15.04
    2.  
    3. 15.04: Pulling from library/ubuntu
    4. 1ba8ac955b97: Pull complete
    5. f157c4e5ede7: Pull complete
    6. 0b7e98f84c4c: Pull complete
    7. a3ed95caeb02: Pull complete
    8. Digest: sha256:5e279a9df07990286cce22e1b0f5b0490629ca6d187698746ae5e28e604a640e
    9. 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的一个目录文件列表。

    1. $  docker pull ubuntu:15.04
    2.  
    3. 15.04: Pulling from library/ubuntu
    4. 47984b517ca9: Pull complete
    5. df6e891a3ea9: Pull complete
    6. e65155041eed: Pull complete
    7. c8be1ac8145a: Pull complete
    8. Digest: sha256:5e279a9df07990286cce22e1b0f5b0490629ca6d187698746ae5e28e604a640e
    9. Status: Downloaded newer image for ubuntu:15.04
    10.  
    11. $ ls /var/lib/docker/aufs/layers
    12.  
    13. 47984b517ca9ca0312aced5c9698753ffa964c2015f2a5f18e5efa9848cf30e2
    14. c8be1ac8145a6e59a55667f573883749ad66eaeef92b4df17e5ea1260e2d7356
    15. df6e891a3ea9cdce2a388a2cf1b1711629557454fd120abd5be6d32329a0e0ac
    16. e65155041eed7ec58dea78d90286048055ca75d41ea893c7246e794389ecf203

    注意看四个目录是如何与下载的镜像的数据层ID匹配的。现在比较下由docker 1.10完成同样的操作的表现。

    1. $ docker pull ubuntu:15.04
    2. 15.04: Pulling from library/ubuntu
    3. 1ba8ac955b97: Pull complete
    4. f157c4e5ede7: Pull complete
    5. 0b7e98f84c4c: Pull complete
    6. a3ed95caeb02: Pull complete
    7. Digest: sha256:5e279a9df07990286cce22e1b0f5b0490629ca6d187698746ae5e28e604a640e
    8. Status: Downloaded newer image for ubuntu:15.04
    9.  
    10. $ ls /var/lib/docker/aufs/layers/
    11. 1d6674ff835b10f76e354806e16b950f91a191d3b471236609ab13a930275e24
    12. 5dbb0cbe0148cf447b9464a358c1587be586058d9a4c9ce079320265e2bb94e7
    13. bef7199f2ed8e86fa4ada1309cfad3089e0542fec8894690529e4c04a7ca2d73
    14. ebf814eccfe98f2704660ca1d844e4348db3b5ccc637eb905d4818fbfb00a06a

    我们看到四个目录与镜像数据层ID并不匹配。尽管docker 1.10之前与之后的版本镜像管理有不同之处,不过所有的docker版本仍然能在镜像之间共享数据层。例如,如果你拉取一个与已经拉取下来的镜像拥有一些共同的数据层的镜像,Docker会检查到这个并只拉取本地没有的数据层。在这之后,两个镜像共享一些相同的数据层。
    你可以自己做试验来说明。对你刚拉取下来的ubuntu:15.04镜像做一个更改,然后基于这个更改构建一个新的镜像。做这个操作的其中一个方法是使用Dockerfile和docker build命令。
    1.在一个空目录创建一个以ubuntu:15.04镜像开始的Dockerfile。

    1. FROM ubuntu:15.04

    2.添加一个内容为”hello world”在/tmp目录的”newfile”文件。Dockerfile类似如下:

    1. FROM ubuntu:15.04
    2.  
    3.  RUN echo "Hello world" > /tmp/newfile

    3.保存Dockerfile并关闭文件。
    4.在Dockerfile相同目录的终端,执行如下命令:

    1. $ docker build -t changed-ubuntu .
    2.  
    3.  Sending build context to Docker daemon 2.048 kB
    4.  Step 1 : FROM ubuntu:15.04
    5.   ---> 3f7bcee56709
    6.  Step 2 : RUN echo "Hello world" > /tmp/newfile
    7.   ---> Running in d14acd6fad4e
    8.   ---> 94e6b7d2c720
    9.  Removing intermediate container d14acd6fad4e
    10.  Successfully built 94e6b7d2c720

    上面显示新镜像的ID为94e6b7d2c720。
    5.执行docker images来检查新的changed-ubuntu镜像是否在Docker主机本地存储区域。

    1. REPOSITORY       TAG      IMAGE ID       CREATED           SIZE
    2.  changed-ubuntu   latest   03b964f68d06   33 seconds ago    131.4 MB
    3.  ubuntu           15.04    013f3d01d247   6 weeks ago       131.3 MB

    6.执行docker history命令来查看哪些数据层用来创建这个新的changed-ubuntu镜像。
    $ docker history changed-ubuntu

    1. IMAGE               CREATED              CREATED BY                                      SIZE        COMMENT
    2.  94e6b7d2c720        2 minutes ago       /bin/sh -c echo "Hello world" > /tmp/newfile    12 B
    3.  3f7bcee56709        6 weeks ago         /bin/sh -c #(nop) CMD ["/bin/bash"]             0 B
    4.  <missing>           6 weeks ago         /bin/sh -c sed -i 's/^#\s*\(deb.*universe\)$/   1.879 kB
    5.  <missing>           6 weeks ago         /bin/sh -c echo '#!/bin/sh' > /usr/sbin/polic   701 B
    6.  <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存储驱动,写时拷贝的操作类似如下:

  • 在镜像数据层中搜索要更新的文件。从顶部,每次一个数据层开始搜索。
  • 在找到的第一个文件副本执行复制(copy-up)操作。”copy up“复制文件到容器自己的可写数据层。
  • 在容器的可写数据层修改刚才复制上来的文件。
  • Btrfs, ZFS和其它驱动处理写时拷贝有所不同。你可以之后阅读这些驱动的详细说明。
    一个copy-up操作可能导致明显的性能开销。开销的不同取决于使用的存储驱动。不过,大文件,大量数据层和尝试目录树会影响更显着。幸运的是,操作只发生在第一次修改任何特定文件时。随后对同一个文件的修改不会引起一个copy-up操作,而是对存在于容器数据层的这个文件直接修改。
    让我们看看如果我们根据我们之前创建的更改的ubuntu映像启动5个容器会发生什么:
    1.从Docker主机上的终端,运行以下docker run命令5次。

    1. $ docker run -dit changed-ubuntu bash
    2.  
    3.  75bab0d54f3cf193cfdc3a86483466363f442fba30859f7dcd1b816b6ede82d4
    4.  
    5.  $ docker run -dit changed-ubuntu bash
    6.  
    7.  9280e777d109e2eb4b13ab211553516124a3d4d4280a0edfc7abf75c59024d47
    8.  
    9.  $ docker run -dit changed-ubuntu bash
    10.  
    11.  a651680bd6c2ef64902e154eeb8a064b85c9abf08ac46f922ad8dfc11bb5cd8a
    12.  
    13.  $ docker run -dit changed-ubuntu bash
    14.  
    15.  8eb24b3b2d246f225b24f2fca39625aaad71689c392a7b552b78baf264647373
    16.  
    17.  $ docker run -dit changed-ubuntu bash
    18.  
    19.  0ad25d06bdf6fca0dedc38301b2aff7478b3e1ce3d1acd676573bba57cb1cfef

    这将根据更改的ubuntu映像启动5个容器。 随着每个容器的创建,Docker添加一个可写层,并为其分配一个随机UUID。 这是从docker run命令返回的值。
    2.运行docker ps命令以验证5个容器是否正在运行。

    1. $ docker ps
    2.  CONTAINER ID    IMAGE             COMMAND    CREATED              STATUS              PORTS    NAMES
    3.  0ad25d06bdf6    changed-ubuntu    "bash"     About a minute ago   Up About a minute            stoic_ptolemy
    4.  8eb24b3b2d24    changed-ubuntu    "bash"     About a minute ago   Up About a minute            pensive_bartik
    5.  a651680bd6c2    changed-ubuntu    "bash"     2 minutes ago        Up 2 minutes                 hopeful_turing
    6.  9280e777d109    changed-ubuntu    "bash"     2 minutes ago        Up 2 minutes                 backstabbing_mahavira
    7.  75bab0d54f3c    changed-ubuntu    "bash"     2 minutes ago        Up 2 minutes                 boring_pasteur

    上面的输出显示了5个正在运行的容器,它们都共享更改的ubuntu映像。 每个CONTAINER ID在创建每个容器时从UUID派生。
    3.列出本地存储区的内容。

    1. $ sudo ls /var/lib/docker/containers
    2.  
    3.  0ad25d06bdf6fca0dedc38301b2aff7478b3e1ce3d1acd676573bba57cb1cfef
    4.  9280e777d109e2eb4b13ab211553516124a3d4d4280a0edfc7abf75c59024d47
    5.  75bab0d54f3cf193cfdc3a86483466363f442fba30859f7dcd1b816b6ede82d4
    6.  a651680bd6c2ef64902e154eeb8a064b85c9abf08ac46f922ad8dfc11bb5cd8a
    7.  8eb24b3b2d246f225b24f2fca39625aaad71689c392a7b552b78baf264647373

    Docker的写时拷贝策略不仅减少了容器所消耗的空间量,而且还减少了启动容器所需的时间。 在开始时,Docker只需为每个容器创建可写层。 下图显示了这5个容器共享更改的ubuntu映像的一个只读(RO)副本。

    如果Docker在每次启动一个新容器时都必须创建底层映像堆栈的整个副本,那么容器启动时间和磁盘空间将大大增加。

    数据卷和存储驱动

    当容器被删除时,写入到容器中的未存储在数据卷中的任何数据与容器一起被删除。
    数据卷是Docker主机文件系统中直接挂载到容器中的目录或文件。 数据卷不受存储驱动程序控制。 对数据卷的读取和写入绕过存储驱动程序,并以本机主机速度运行。 你可以将任意数量的数据卷装载到容器中。 多个容器还可以共享一个或多个数据卷。
    下图显示了运行两个容器的单个Docker主机。 每个容器存在于Docker主机本地存储区(/var/lib/docker/ …)内的自己的地址空间内。 Docker主机上的/data还有一个共享数据卷。 它直接安装在两个容器中。

    数据卷驻留在Docker主机上的本地存储区域之外,进一步增强了它们与存储驱动程序控制的独立性。 当容器被删除时,存储在数据卷中的任何数据都会保留在Docker主机上。

    标签:容器Docker 发布于:2019-11-20 05:00:15