一探 Docker 镜像的究竟
一. 引入
我们经常拿 Docker 容器与虚拟机作对比,Dokcer 容器跟虚拟机的不同之处在于,在使用 Docker 的时候,实质上并没有生成一个真实存在的“容器”。Docker 帮助用户启动的,其实就是应用本身,只不过在创建这些进程时,Docker 为它们加上重重限制。包括启用 Namespace 配置对容器之间进行隔离,以及设置指定的 Cgroups 参数来为进程设置资源限制等。
当然,仅仅是这样还不够,既然一个宿主机可以存在多个容器且它们之间又是隔离的,那它们的文件系统是怎么样的呢,这就关乎到一个重要角色,使 Docker 被大众接受的关键——Docker 镜像。
二. 文件系统的隔离
首先,我们自然会想到,容器里的应用进程,看到的理应是一份完全独立的文件系统。因为只有这样,容器在操作自己的文件目录时才不会影响到宿主机以及其他的容器。
在 Linux 中,有一个概念叫做 Mount Namespace,它提供了文件系统的隔离,通过隔离文件系统挂载点来实现。而 Dokcer 正是利用了这个为各个容器提供了文件系统隔离。Docker 在容器进程启动之前,就会对它整个根目录 “/” 重新挂载,因此对于容器来说,自己使用的就是一个完全独立的文件系统。
当然,仅仅是这样还不够,总不能让容器的文件系统都空空荡荡的吧。因此除了隔离各个容器本身的文件系统,Docker 还会在容器的根目录下挂载一个完整的操作系统的文件系统,比如 Ubuntu。这样该容器根目录下的内容,就是 Ubuntu 的所有目录和文件了,也就是我们无比熟悉的 /bin, /etc, /data, /usr 等
而这个挂载在容器根目录、用来为容器进程提供隔离后执行环境的文件系统,就是所谓的容器镜像。
镜像与操作系统
既然一个镜像包含了一个操作系统的文件,配置,目录等,那它是不是就是一个操作系统了呢?
答案为否,因为镜像不包含操作系统的内核,而一台宿主机上的所有容器,其实都是共享当前所在机器的操作系统内核。这也是容器相比虚拟机,所呈现出来的一个缺点,当我们在操作容器内的应用的时候,特别是与内核进行交互的时候,需要注意我们是直接地影响了宿主机的操作系统内核,因此需要格外小心。
三. 为什么我们需要镜像
我们都知道镜像是 Docker 一个非常重要的特性,那么我们为什么需要镜像呢?
1. 解决环境一致性问题
在容器发展初期,用户在使用容器平台向集群部署应用的时候,通常需要进行一个打包的操作,不同于 Docker 简单的镜像构建操作,在那个时候,开发者们必须为每种语言、每种框架,甚至每个版本的应用都维护一个打好的包,同时由于上云后环境的不同,还需要做许多配置工作。
而 Docker 镜像的出现,彻底改变了这种状况,正如前面所说的,镜像不只是包含了应用所需的语言,框架等环境,而是直接囊括了整个操作系统的文件系统,达到了本地环境和云端环境的高度一致的目的。而开发者们要做的,只是创建一个容器,并让镜像在这个容器中运行即可。
也就是说,镜像达成了操作系统级别的运行环境一致性,化解了本地开发与远端环境之间的差异问题。
2. 分层机制
在实际的开发中,如果我们每开发一个应用或者在现有应用中做一些改动,都需要重新去构建一个镜像,那么这也是一个比较麻烦的事情。
因此 Docker 镜像采用了一种比较创新的方式,就是引入层的概念。既然我们希望避免每做一些改动都保存一个镜像,那就利用一种增量的思想,去维护增量的内容。也就是说,用户制作镜像的每一步操作,都会生成一个层。
比方说在 Dockerfile 中,大多数指令都会生成一个层,最终完成的镜像实际上是一层一层地叠加起来的。与此同时,层之间又是可以共享的,假设本地拥有一个五层的镜像 A,而 A 镜像和 B 镜像共享了前面的五层,则当我们从镜像仓库中拉取 B 镜像时,只拉取本地所没有的最后一层就可以了,而不需要把整个 B 镜像连根拉一遍。
分层机制带来的好处也影响了 Docker 后面的发展,在 17.05 版本之后,Docker 引入 Dockerfile 多阶段构建机制,使得镜像的体积能够大幅度降低。