上一篇,我们主要分析了 Docker 沙盒机制的实现原理。但是光有沙盒还不够,Docker 为了实现“一致性”,引入了 Docker 镜像这一项伟大的创举,使得容器的传递和迁移更加简单,这一篇我们来用一个扒一扒 Docker 镜像的具体原理。
回忆一下,前文提到的一个容器启动的过程如下:
- 配置 Linux Namespace
- 设置 Cgroups 参数
- 切换进程更目录
镜像的内容构成
步骤 3 切换到的进程根目录的内容就是为容器进程提供隔离的执行环境的文件系统,着整个文件系统就是镜像中的全部文件了,在 Linux 操作系统的语境下,这样的一个完整系统,被称为根文件系统,简称 rootfs。
不过需要注意的是,rootfs 包含的是一个完整操作系统(如Ubuntu、CentOS)的全部文件,但是也仅仅是文件而已,在 Linux 系统中,操作系统内核和 rootfs 是分开存放的,操作系统只有在开机时才会加载某个固定版本的内核镜像。
换而言之,就是说,虽然我们在打包镜像时可以选择各种不同的操作系统,但是在具体运行镜像时,不管是什么操作系统,同一台宿主机上运行的所有容器都共享宿主机操作系统的内核,这就意味着,和内核相关的操作,相当于会被应用于宿主机以及宿主机上所有的容器,带来额外的安全隐患,而在虚拟机时代,这样的隐患是不存在的。
Docker 镜像的设计中还有一个创举,不是单纯的将所有文件打包到一个镜像再一股脑儿挂载到某个目录,而是引入了分层挂载的概念,用户在制作镜像时,每一步操作都会生成一个层,基础镜像再加上所有的层,以一种增量的方式构成了打包的新镜像。
镜像如何被挂载
联系上文内存,步骤 3 实际上可以划分为挂载文件和切换根目录两个操作,具体步骤如下:
- 创建只读层(即容器镜像中的内容)
- 创建读写层(writeLayer)
- 创建 Init 层
- 创建挂载点(mnt),并把只读层、读写层和 Init 层挂载到挂载点
- 将挂载点作为容器的根目录
只读层中包含的就是 Docker 镜像中的文件,挂载方式是 ro+wh,无法被修改,但是可以被其它层遮挡覆盖。
读写层在挂载初期是空的,挂载方式为rw,可以进行读写操作,在容器中做了任何写操作,修改产生的内容就会以增量的方式出现在读写层中,如果对只读层中的 rootfs 中的文件做了删除或修改,读写层会产生对应的新文件,由于挂载时是依次挂载的,后挂载的读写层里的改动会遮挡覆盖掉只读层的文件,以实现真实的读写效果。
Init 层是 Docker 生成的一个内部层,用来存放 /etc/*.conf 等本来属于镜像一部分的配置文件,这些修改往往只是对当前容易有效,为了防止 commit 时把这些修改也提交,故而没有放在读写层,而是单独了一层。
这三类层挂载在一起,就是一个容器的完整文件系统了。