Docker 镜像实质和容器运行原理

⚠ 转载请注明出处:作者:ZobinHuang,更新日期:July 10 2021


知识共享许可协议

    本作品ZobinHuang 采用 知识共享署名-非商业性使用-禁止演绎 4.0 国际许可协议 进行许可,在进行使用或分享前请查看权限要求。若发现侵权行为,会采取法律手段维护作者正当合法权益,谢谢配合。

1. Docker 镜像究竟是啥?

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


    基于上述镜像的概念,Docker 镜像也很好理解,简单意义上也是封装了应用运行环境和应用程序本身,只要我们的宿主机上有 Docker Engine,我们的 Docker 镜像文件就能够被加载,并且使用 Docker容器 将 Docker 容器所封装的内容 run 起来 (run 起来的原理将在下一章阐述)。但是这样说显得十分模凌两可,并且没有 cover Docker Image 的本质。Docker 镜像有一个很特别和神奇的特点:分层。下面进行介绍。

    如上图所示,是一个 Docker 镜像的本质。首先我们看 Base Image —— 这是某个 Linux Distribution (e.g. CentOS, Ubuntu) 的 rootfs,包含了操作系统最基本的命令、配置文件等,注意到此部分并没有包含操作系统的内核,所有的系统调用都会被宿主机的内核所接收并执行,因此 Base Image 仍然是轻量级的。然后在 Base Image 作为一个 Layer,在其之上可以继续构建包含运行程序所需的环境的镜像,以我们上图为例,Layer 2 是一个包括 Golang 编程环境的 Layer,我们暂时称其为 Golang Layer。注意到 Golang Layer 是基于 Base Image 构建的,其会对 Base Image 所包含的文件进行增加/删除/修改等操作,但是这些增删改操作并不会实际上去修改 Base Image 的内容,而是在 Golang Layer 中记录这部分增删改的信息,然后再打包成为一个镜像,即 Golang Image,这样在外界看来,这就是一个完整的包含 Golang 运行环境的镜像。如此递归,直到最终构建出我们的应用程序镜像。

    读者可能会疑问,为什么上述的例子中是在 Golang Layer 中记录对 Base Image 的文件增删改信息,而不是直接对 Base Image 所包含的文件进行修改呢?这就要考虑 Docker 的一个特性:不同的 Image 之间可以共享 Layer。如下图所示,如果几个不同的 Images 的底层都是使用 CentOS 作为 Layer 1,那么它们将可以共享这一个 Layer。思考我们在使用 "docker pull" 命令下载这些 Images 的场景,我们在下载第一个 Image 的时候 (say Image_1),就会把它所需要的所有 Layer 下载下来,之后在下载 Image_2 的时候,由于 Layer 1 和 Layer 2 都和 Image_1 完全一致,因此我们能跳过这两个 Layer 的下载,直接下载 Layer 3,就能够在本地获得运行 Image_2 所需的所有文件。

    这样来看,Docker Image 的分层机制就是加速了 docker pull 的过程,并且很大程度地节约了本地的存储空间。这一切的本质都是源于上文所说的:Layer 中的信息是记录下它对它下面所依赖的 Layer 所包含的文件的增删改情况,然后最终打包成 Image 的时候对外暴露为一个统一的环境。这种操作的实现原理的名称是联合文件系统 (Union File System)[2]。我们可以从下面要讲的 Container 是如何基于 Image run 起来的原理来理解这一点。

2. Docker 容器是怎么基于镜像 run 起来的?

    如上左图所示,考虑我们是用虚拟机运行一个镜像所需要的步骤。首先,由于我们是在虚拟机中运行镜像,因此这个镜像需要包含操作系统内核在内的完整运行环境。然后,我们需要将镜像克隆一份,放到虚拟机中跑起来,也就是一个镜像的实例,我们在虚拟机中可以对这个实例直接进行增删改的操作。显然,如果我们是把我们的应用程序封装在这样一个镜像中来发布的话,那么即使我们只是进行了一些很小的更新,我们都需要把一个包含操作系统内核的镜像交付给我们的客户,这样明显是低效率的。

    如上右图所示,考虑我们是使用 Docker Container 来将我们在上一章节所描述的 Docker Image 运行起来,Docker 支持基于一个 Docker 镜像运行若干个 Docker 容器,其原理就是各个 Docker 容器对镜像所作出的修改实际上是保存在一个 R/W Thin Layer 中的,并不会直接写回 Docker 镜像。另外,通过 R/W Thin Layer 的设计,我们在启动一个 Docker 容器的时候也不需要进行像上一段所描述的对镜像的拷贝,而是直接创建这层 R/W Thin Layer 即可,需要增删改文件时,再去 Read-only 的镜像中读取即可。综上两点所述,通过各个容器维护自己的这层 R/W Thin Layer,Docker 就能实现轻量级地并行运行多个镜像实例 (i.e. 容器)

    当我们完成在容器内的操作,并且打算将我们的工作成果打包成为一个新的镜像时,我们容器的这层 R/W Thin Layer 也就成为了新的镜像里最顶层的 Layer。在下一篇文章中,我们将体验如何 commit 我们自己的镜像。

附录:参考源

  1. docker.com, About storage drivers
  2. Wikipedia, UnionFS