docker-images

Docker镜像是一个只读的Docker容器模板,含有启动Docker容器所需的文件系统结构及其内容,因此是启动一个Docker容器的基础。Docker镜像的文件内容以及一些运行Docker容器的配置文件组成了Docker容器的静态文件系统运行环境——rootfs。可以这么理解,Docker镜像是Docker容器的静态视角,Docker容器是Docker镜像的运行状态。

  1. rootfs
    rootfs是Docker容器在启动时内部进程可见的文件系统,即Docker容器的根目录。rootfs通常包含一个操作系统运行所需的文件系统,例如可能包含典型的类Unix操作系统中的目录系统,如/dev、/proc、/bin、/etc、/lib、/usr、/tmp及运行Docker容器所需的配置文件、工具等。在传统的Linux操作系统内核启动时,首先挂载一个只读(read-only)的rootfs,当系统检测其完整性之后,再将其切换为读写(read-write)模式。而在Docker架构中,当Docker daemon为Docker容器挂载rootfs时,沿用了Linux内核启动时的方法,即将rootfs设为只读模式。在挂载完毕之后,利用联合挂载(union mount)技术在已有的只读rootfs上再挂载一个读写层。这样,可读写层处于Docker容器文件系统的最顶层,其下可能联合挂载多个只读层,只有在Docker容器运行过程中文件系统发生变化时,才会把变化的文件内容写到可读写层,并隐藏只读层中的老版本文件。
  2. Docker镜像的主要特点
    为了更好地理解Docker镜像的结构,下面介绍一下Docker镜像设计上的关键技术。
    ● 分层
    Docker镜像是采用分层的方式构建的,每个镜像都由一系列的“镜像层”组成。分层结构是Docker镜像如此轻量的重要原因,当需要修改容器镜像内的某个文件时,只对处于最上方的读写层进行变动,不覆写下层已有文件系统的内容,已有文件在只读层中的原始版本仍然存在,但会被读写层中的新版文件所隐藏。当使用docker commit提交这个修改过的容器文件系统为一个新的镜像时,保存的内容仅为最上层读写文件系统中被更新过的文件。分层达到了在不同镜像之间共享镜像层的效果。
    ● 写时复制
    Docker镜像使用了写时复制(copy-on-write)策略,在多个容器之间共享镜像,每个容器在启动的时候并不需要单独复制一份镜像文件,而是将所有镜像层以只读的方式挂载到一个挂载点,再在上面覆盖一个可读写的容器层。在未更改文件内容时,所有容器共享同一份数据,只有在Docker容器运行过程中文件系统发生变化时,才会把变化的文件内容写到可读写层,并隐藏只读层中的老版本文件。写时复制配合分层机制减少了镜像对磁盘空间的占用和容器启动时间。
    ● 内容寻址
    在Docker 1.10版本后,Docker镜像改动较大,其中最重要的特性便是引入了内容寻址存储(content-addressable storage)的机制,根据文件内容来索引镜像和镜像层。与之前版本对每一个镜像层随机生成一个UUID不同,新模型对镜像层的内容计算校验和,生成一个内容哈希值,并以此哈希值代替之前的UUID作为镜像层的唯一标志。该机制主要提高了镜像的安全性,并在pull、push、load和save操作后检测数据的完整性。另外,基于内容哈希来索引镜像层,在一定程度上减少了ID的冲突并且增强了镜像层的共享。对于来自不同构建的镜像层,只要拥有相同的内容哈希,也能被不同的镜像共享。
    ● 联合挂载
    通俗地讲,联合挂载技术可以在一个挂载点同时挂载多个文件系统,将挂载点的原目录与被挂载内容进行整合,使得最终可见的文件系统将会包含整合之后的各层的文件和目录。实现这种联合挂载技术的文件系统通常被称为联合文件系统(union filesystem)。如图3-11所示,以运行Ubuntu:14.04镜像后容器中的aufs文件系统为例。由于初始挂载时读写层为空,所以从用户的角度看,该容器的文件系统与底层的rootfs没有差别;然而从内核的角度来看,则是显式区分开来的两个层次。当需要修改镜像内的某个文件时,只对处于最上方的读写层进行了变动,不覆写下层已有文件系统的内容,已有文件在只读层中的原始版本仍然存在,但会被读写层中的新版文件所隐藏,当docker commit这个修改过的容器文件系统为一个新的镜像时,保存的内容仅为最上层读写文件系统中被更新过的文件。

    联合挂载是用于将多个镜像层的文件系统挂载到一个挂载点来实现一个统一文件系统视图的途径,是下层存储驱动(如aufs、overlay等)实现分层合并的方式。所以严格来说,联合挂载并不是Docker镜像的必需技术,比如我们在使用Device Mapper存储驱动时,其实是使用了快照技术来达到分层的效果,没有联合挂载这一概念。

Docker镜像的存储组织方式综合考虑镜像的层级结构,以及volume、init-layer、可读写层这些概念,一个完整的、在运行的容器的所有文件系统结构可以用图3-12来描述。从图中我们不难看到,除了echo hello进程所在的cgroups和namespace环境之外,容器文件系统其实是一个相对独立的组织。可读写部分(read-write layer以及volumes)、init-layer、只读层(read-only layer)这3部分结构共同组成了一个容器所需的下层文件系统,它们通过联合挂载的方式巧妙地表现为一层,使得容器进程对这些层的存在一点都不知道。

Docker镜像

  1. registry
    registry用以保存Docker镜像,其中还包括镜像层次结构和关于镜像的元数据。可以将registry简单地想象成类似于Git仓库之类的实体。用户可以在自己的数据中心搭建私有的registry,也可以使用Docker官方的公用registry服务,即Docker Hub[插图]。它是由Docker公司维护的一个公共镜像仓库,供用户下载使用。Docker Hub中有两种类型的仓库,即用户仓库(user repository)与顶层仓库(top-level repository)。用户仓库由普通的Docker Hub用户创建,顶层仓库则由Docker公司负责维护,提供官方版本镜像。理论上,顶层仓库中的镜像经过Docker公司验证,被认为是架构良好且安全的。
  2. repository
    repository即由具有某个功能的Docker镜像的所有迭代版本构成的镜像组。由上文可知,registry由一系列经过命名的repository组成,repository通过命名规范对用户仓库和顶层仓库进行组织。用户仓库的命名由用户名和repository名两部分组成,中间以“/”隔开,即username/repository_name的形式,repository名通常表示镜像所具有的功能,如ansible/ubuntu14.04-ansible;而顶层仓库则只包含repository名的部分,如ubuntu。读者也许会产生疑问,通常将ubuntu视为镜像名称,这里却解释为repository,那么repository和镜像之间是什么关系呢?事实上,repository是一个镜像集合,其中包含了多个不同版本的镜像,使用标签进行版本区分,如ubuntu:14.04、ubuntu:12.04等,它们均属于ubuntu这个repository。一言以蔽之,registry是repository的集合,repository是镜像的集合。
  3. manifest
    manifest(描述文件)主要存在于registry中作为Docker镜像的元数据文件,在pull、push、save和load中作为镜像结构和基础信息的描述文件。在镜像被pull或者load到Docker宿主机时,manifest被转化为本地的镜像配置文件config。新版本(v2, schema 2)的manifest list可以组合不同架构实现同名Docker镜像的manifest,用以支持多架构Docker镜像。
  4. image和layer
    Docker内部的image概念是用来存储一组镜像相关的元数据信息,主要包括镜像的架构(如amd64)、镜像默认配置信息、构建镜像的容器配置信息、包含所有镜像层信息的rootfs。Docker利用rootfs中的diff_id计算出内容寻址的索引(chainID)来获取layer相关信息,进而获取每一个镜像层的文件内容。layer(镜像层)是一个Docker用来管理镜像层的中间概念,本节前面提到镜像是由镜像层组成的,而单个镜像层可能被多个镜像共享,所以Docker将layer与image的概念分离。Docker镜像管理中的layer主要存放了镜像层的diff_id、size、cache-id和parent等内容,实际的文件内容则是由存储驱动来管理,并可以通过cache-id在本地索引到。
  5. Dockerfile
    镜像如此有趣,也许现在读者已经迫不及待地想一试身手,构建属于自己的镜像了。Dockerfile是在通过docker build命令构建自己的Docker镜像时需要使用到的定义文件。它允许用户使用基本的DSL语法来定义Docker镜像,每一条指令描述了构建镜像的步骤