title: Docker之理解image,container和storage driver
date: 2015-11-27 11:23:40
tags: docker
本篇文查主要介绍镜像,容器的文件系统,写时复制(CoW)机制和数据卷
为了高效的使用Docker
,必须理解Docker的文件系统,了解容器,镜像是怎么存储的,以及在对容器进行读写操作时,其文件系统发生了哪些变化。
本文的图片来自于Docker
官网,地址为:docker docs
Docker
的镜像是由一系列只读层组成的一个栈,上面的层依赖其下面的层,这些层从外面看起来是一个整体。栈底的镜像被称作基础镜像(base image),所有上面的层都基于这个基础镜像。
下面是ubuntu:15.04
镜像各层之间的关系:
当你在一个容器中进行了某些操作比如添加了一个文件,然后调用docker commit
操作创建新的镜像时,Docker
会在镜像栈的最上面创建一个新的层,这个层包含了新添加的文件。
或者,通过Dockerfile
创建新的镜像时,通过FROM
指令指定的就是基础镜像。此后的每条指令都会创建一个新的层,层中包含了这条指令对镜像的修改。
容器container
不仅包含镜像的所有层,它还在最上面添加了一个可读层称作容器层container layer
。下面是一个基于ubuntu:15.04
运行起来的容器的层之间的关系:
容器与镜像的主要区别就在于这个可写层(writable layer),对容器的所有写操作无论是添加新内容还是修改原来的内容都会保存在这个可读层中。如果容器被删除,writable layer也会被删除,但镜像层不变。
正是因为每个镜像都有自己的可写层,所以容器之间可以共享同一个镜像的各层。下面是多个容器使用同一个镜像的例子:
存储驱动器需要管理所有的镜像层和容器只读层,而进行这些管理除了需要分层机制外还需要写时复制策略(CoW)。
CoW策略有两个关键词:“分享”share
和“复制”copy
。这个策略是指,所有需要某个文件的进程都共用同一个文件实例而不是各自拥有一份副本,即分享。一旦有进程需要改写某个文件,系统就给他一个文件副本供它操作,即复制。只有需要写的进程才会拥有一份源文件的拷贝,其他进程仍然共享源文件。
Docker
在镜像管理和容器中都使用了写时复制策略,这不仅缩小了镜像对磁盘空间的利用率,也加快了容器的启动速度。
所有镜像和容器的层都存储在宿主机中,并由storage driver
管理。
当我们在本地执行docker pull
命令时,可以看到下载了多个文件,每个文件都是一个镜像层,每一个镜像层都有一个标识符(UUID),这些层加在一起组成了一个完整的镜像。
上面已经提到,镜像之间可以共享镜像层的,也就是说,当执行pull
操作时,如果要拉取的镜像与本地已有的镜像共享某些层,这些层不会重复下载,而是只下载本地没有的层。
在基于已有镜像创建新镜像时同样如此。比如,在一个空文件夹下创建如下Dockerfile
:
FROM ubuntu:15.04
RUN echo "hello" > /tmp/newfile
基于此Dockerfile创建新镜像,运行docker build -t changed-ubuntu .
,得到新的镜像changed-ubuntu,在运行docker history
命令查看changed-ubuntu,可以看到如下信息:
IMAGE CREATED CREATED BY SIZE COMMENT
03b964f68d06 About a minute ago /bin/sh -c echo "Hello world" > /tmp/newfile 12 B
013f3d01d247 6 weeks ago /bin/sh -c #(nop) CMD ["/bin/bash"] 0 B
2bd276ed39d5 6 weeks ago /bin/sh -c sed -i 's/^#\s*\(deb.*universe\)$/ 1.879 kB
13c0c663a321 6 weeks ago /bin/sh -c echo '#!/bin/sh' > /usr/sbin/polic 701 B
6e6a100fa147 6 weeks ago /bin/sh -c #(nop) ADD file:49710b44e2ae0edef4 131.4 MB
通过创建时间我们可以看出,刚才的docker build
命令仅创建了最上面的层,其大小仅有12B,而它下面的各层其实就是组成ubuntu:15.04镜像的各层,我们可以通过比较UUID确定这一点。新镜像和ubuntu:15.04之间的关系可以用下面的图来表示:
也就是说,事实上changed-ubuntu镜像仅占用了12B的磁盘空间。
上面已经提到,每个容器都有属于自己的读写层,读写层下面的层都是只读层,不能被修改。此外,只读层是可以共享的,也就是说一个镜像的各层可以被多个容器同时使用。
当容器中发生写操作时,使用写时复制策略,大体步骤如下:
- 从上至下搜索要写的文件
- 将找到的文件复制(copy up
)一份到读写层中
- 修改复制后的文件
如果一个文件被复制到了读写层中,其源文件仍然存在,但是会被读写层中的文件覆盖掉。也就说,源文件并不改变,但其在当前容器中不再发挥作用,而是由复制到读写层的文件代替它的作用。
将文件复制到读写层的操作copy up
会带来不小的开销,尤其是对大文件的操作,层数比较多或是文件在文档树比较深的层次时,开销会更大。索性copy up
操作仅在第一次修改文件时才会发生,此后对文件的操作不会引起copy up
操作,而是直接对复制到读写层的文件进行操作。
Docker
的写时复制策略不仅减小了容器的磁盘开销,而且加快了容器的启动速度。当我们启动一个容器时,Docker
仅创建了一个读写层writable layer
,而镜像中的只读层是共享的。尤其当多个容器基于一个镜像的时候,这种优势尤其明显。
当容器被删除时,读写层也被删除,所有没有写入数据卷的内容也都会被删除。数据卷是挂载在容器中的一个目录或文件。
数据卷并不由存储驱动(storage driver)管理,写入数据卷的内容绕过存储驱动器直接写在宿主机上。你可以向容器挂在任意数量的数据卷,一个数据卷也可以在多个容器间共享。
下面的图展示了数据卷和容器之间的关系:
当容器被删除时,数据卷中的内容会继续保存在宿主机上。