Docker 之四 Docker 镜像结构与加载原理

Docker 镜像

镜像是一种轻量级、可执行的独立软件包,用来打包软件运行环境和基于运行环境开发的软件,包含运行某个软件所需的所有内容,其中包括代码、运行时、库、环境变量、配置文件。

Docker 的 Base 镜像

Base 镜像从 scratch 构建,不依赖其他镜像,可作为其他应用镜像的父镜像;其他应用镜像可以在此基础进行扩展,Base 镜像通常都是各种 Linux 发行版的 Docker 镜像,比如 Ubuntu、Debian、CentOS 等。

Docker 镜像加载原理

  • bootfs

    1. 在 Docker 镜像中最底层是 bootfs (boot file sysem) 文件系统,bootfs 主要包含 bootloader 和 kernel。Linux 刚启动时会加载 bootfs 文件系统,bootlader 主要作用是引导加载 kernel。在 Docker 镜像中,bootfs 这一层与典型的 Linux/Unix 系统是一样的,包含 bootloader 和 kernel。当 bootloader 加载完成之后整个内核都存放在内存中,此时内存的使用权已由 bootfs 转交给内核,此时系统也会卸载 bootfs。
  • rootfs

    1. 在 Docker 镜像中用户空间的文件系统是 rootfs,包含 /dev、/proc、/bin 等目录。对于 base 镜像来说,底层直接用 Host 的 kernel,自己只需要提供 rootfs。而对于一个精简的 OS,rootfs 的体积可以很小,只需要包含最基本的命令、工具和程序库就可以。

    2. 不同 Linux 发行版的主要区别就是 rootfs。比如 Ubuntu14.04 使用 upstart 管理服务,apt 管理软件包;而 CentOS7 使用 systemd 和 yum。这些都是用户空间上的区别,Linux kernel 差别不大。因此 Docker 可以同时支持多种发行版的 Linux 镜像,模拟出多种操作系统环境。

    3. 容器只能使用 Host 的 kernel,并且不能修改。所有容器都共用 host 的 kernel,在容器中没办法对 kernel 升级。如果容器对 kernel 版本有要求(比如应用只能在某个 kernel 版本下运行),则不建议用容器,这种场景虚拟机可能更合适。

  • 图解 bootfs、rootfs
    docker-bootfs-rootfs

UnionFS 文件系统

一种为 Linux,FreeBSD 和 NetBSD 操作系统设计的把其他文件系统联合到一个联合挂载点的文件系统服务。它使用 branch 把不同文件系统的文件和目录 “透明地” 覆盖,形成一个单一一致的文件系统。这些 branches 或者是 read-only 或者是 read-write 的,所以当对这个虚拟后的联合文件系统进行写操作的时候,系统是真正写到了一个新的文件中。看起来这个虚拟后的联合文件系统是可以对任何文件进行操作的,但是其实它并没有改变原来的文件,这是因为 unionfs 用到了一个重要的资管管理技术叫写时复制。写时复制(copy-on-write)技术,也叫隐式共享,是一种对可修改资源实现高效复制的资源管理技术。它的思想是,如果一个资源是重复的,但没有任何修改,这时候并不需要立即创建一个新的资源;这个资源可以被新旧实例共享。创建新资源发生在第一次写操作,也就是对资源进行修改的时候。通过这种资源共享的方式,可以显著地减少未修改资源复制带来的消耗,但是也会在进行资源修改的时候增加小部分的开销。

Docker 镜像中的 UnionFS

UnionFS 文件系统是 Docker 镜像的基础,镜像可以通过分层来进行继承,基于 Base 镜像(没有父镜像),可以制作各种具体的应用镜像。简单概括来说,UnionFS 是一种分层、轻量级并且高性能的文件系统,它支持对文件系统的修改作为一次提交来一层层地叠加,同时可以将不同目录挂载到同一个虚拟文件系统下。特性是可以一次同时加载多个文件系统,但从外面看起来,只能看到一个文件系统;联合加载会把各层文件系统叠加起来,这样最终的文件系统包含所有底层的文件和目录。

容器的可写层

当容器启动时,一个新的可写层被加载到镜像的顶部。这一层通常被称作 “容器层”,“容器层” 之下的都叫 “镜像层”。所有对容器的改动,无论添加、删除、还是修改文件都只会发生在容器层中。即只有容器层是可写的,容器层下面的所有镜像层都是只读的。其中镜像层数量可能会很多,所有镜像层会联合在一起组成一个统一的文件系统(UnionFS)。如果不同镜像层中有一个相同路径的文件,比如 /a,上层的 /a 会覆盖下层的 /a,也就是说用户只能访问到最上层中的文件 /a。在容器层中,用户看到的是一个叠加之后的文件系统。下图是容器可写层的图解:
docker-writable-layout

容器可写层的操作

  • 添加文件,在容器中创建文件时,新文件被添加到容器层中。
  • 读取文件,在容器中读取某个文件时,Docker 会从上往下依次在各镜像层中查找此文件。一旦找到,打开并读入内存。
  • 修改文件,在容器中修改已存在的文件时,Docker 会从上往下依次在各镜像层中查找此文件。一旦找到,立即将其复制到容器层,然后再修改。
  • 删除文件,在容器中删除文件时,Docker 也是从上往下依次在镜像层中查找此文件。找到后,会在容器层中记录下此删除操作。
  • 上面的操作中,只有当需要修改时才复制一份数据,这种特性被称作写时复制(copy-on-write)。可见容器层保存的是镜像变化的部分,不会对镜像本身进行任何修改。容器层记录对镜像的修改,所有镜像层都是只读的,不会被容器修改,所以镜像可以被多个容器共享。