本文参阅自Docker官方的一篇文档:《Understand images, containers, and storage drivers》。原文有些长,有些啰嗦,所以我翻译精简了一下。如果你的英语还可以,强烈建议读原文。

Docker的口号是“Build, Ship and Run Any App, Anywhere.”,大概意思就是编译好一个应用后,可以在任何地方运行,不会像传统的程序一样,一旦换了运行环境,往往就会出现缺这个库,少那个包的问题。那么Docker是怎么做到这点的呢?简单说就是它在编译应用的时候把这个应用依赖的所有东西都编译到了镜像里面(有点像程序的静态编译——只是像而已)。我们把这个编译好的静态的东西叫docker镜像(Image),然后当docker deamon(docker的守护进程/服务进程)运行这个镜像的时候,我们称其为docker容器(Container)。好吧,你可以简单理解docker镜像和docker容器的关系就像是程序和进程的关系一样(当然实质是不一样的)。等读完此文,你会对它们的细节有一个更深入的了解,也会对docker有一个非常深入的了解(然而,如果你想深入学习docker,这还是远远不够滴...^_^)。


1. Images和Layers

每个Docker镜像(Image)都引用了一些只读的(read-only)层(layer),不同的文件系统layer也不同。这些layer堆叠在一起构成了容器(Container)的根文件系统(root filesystem)。下图是Ubuntu 15.04的镜像,共由4个镜像层(image layer)组成:
image-layers.jpg

Docker的storage driver负责堆叠这些layer,并对外提供一个统一的视图。

当你基于Ubuntu 15.04创建一个新的容器的时候,你其实只是在它的上层又增加了一个新的、薄的、可写层。这个新增的可写层称为容器层(container layer)。当这个新的容器运行时,所有的改动(比如创建新文件、修改已有文件、删除文件等)都会写到这一层。下图展示了一个新容器的结构:

container-layers.jpg

新旧版本Content addressable storage的变化:“Content addressable storage”是什么意思呢?其实主要是说layer的在磁盘存储时地址/名字选取的策略。Docker 1.10之前会给每个layer随机生成一个UUID作为其标识,这个UUID也是存储该layer层数据的目录名字。Docker1.10版本引入了一种新的方式——内容哈希(content hash),这种方式更加安全,而且通过内部实现避免了ID冲突的问题,还可以保证pull、push、load、save等操作之后数据的完整性(integrity)。除此以外,它可以让不同Image之间更好的共享一些公共的layer,哪怕他们不是从同一个Dockerfile构建来的(layer共享的问题后面会讲)。

下图展示了新版本的这个变化:

container-layers-cas.jpg

我们可以看到所有的镜像层的ID都是由密码散列而来,而容器层的ID依旧是随机生成的UUID。这个变化看似只是名字的变化,但其实二者是不兼容的。所以旧版本升级到新版本后,数据也必须做迁移才可以使用。当然官方提供了迁移工具,原文也给了例子,这里就不翻译了,有兴趣的去看原文。

2. Container和Layers

容器和镜像的主要区别就是顶部的那个可写层(即之前说的那个“container layer”)。容器运行时做的所有操作都会写到这个可写层里面,当容器删除的时候,这个可写层也会被删掉,但底层的镜像依旧保持不变。所以,不同的容器都有自己的可写层,但可以共享同一个底层镜像。下图展示了多个容器共享同一个Ubuntu 15.04镜像。

sharing-layers.jpg

Docker的storage driver负责管理只读的镜像层和可写的容器层,当然不同的driver实现的方式也不同,但其后都有两项关键技术:可堆叠的镜像层(stackable image layer)和写时拷贝技术(copy-on-write, CoW)。

3. 写时拷贝策略

系统内需要同一份数据的进程共享同一份数据实例,只有当某个进程需要对这些数据做修改时,它才会把数据拷贝一份做修改,而这个修改也只对它自己可见,其它的进程依旧使用的是原来的数据。Docker使用这个技术极大的减小了镜像对于磁盘空间的占用以及容器的启动时间(如果某个容器的镜像层已经被其他容器启动了,那它就只需启动自己的容器层就可以了)。比如对于AUFS和OverlayFS这两个storage driver,他们的CoW流程大致如下:

  • 从最新的layer自上而下,一层一层寻找需要更新的镜像层。
  • 当找到需要更新的文件的时候,就执行“copy-up”操作。这个操作其实就是把这个文件拷贝到自己的可写层/容器层去。
  • 修改自己可写层的这个拷贝。

当然,当文件比较大,或者镜像的层数比较多等情况下,这个“copy-up”操作成为性能负担。不过好在对于同一个文件的多次修改只会拷贝一次。

4. 数据持久化

刚开始的时候,docker一般只适用于无状态的计算场景使用。但随着发展,docker通过data volume技术也可以做到数据持久化了。data volume就是我们将主机的某个目录挂载到容器里面,这个data volume不受storage driver的控制,所有对这个data volume的操作会绕过storage driver直接其操作,其性能也只受本地主机的限制。而且我们可以挂载任意多个data volume到容器中,不同容器也可以共享同一个data volume。

下图展示了一个Docker主机上面运行着两个容器.每一个容器在主机上面都有着自己的地址空间(/var/lib/docker/...),除此以外,它们还共享着主机上面的同一个/data目录。

shared-volume.jpg

更多关于data volume的信息,请参阅 Manage data in containers.