3.1 镜像的内部结构
为什么我们要讨论镜像的内部结构?
如果只是使用镜像,当然不需要了解,直接通过docker命令下载和运行就可以了。
但如果我们想创建自己的镜像,或者想理解Docker为什么是轻量级的,就非常有必要学习这部分知识了。
我们从一个最小的镜像开始吧。
3.1.1 hello-world——最小的镜像
hello-world是Docker官方提供的一个镜像,通常用来验证Docker是否安装成功。我们先通过docker pull从Docker Hub下载它,如图3-1所示。
图3-1
用docker images命令查看镜像的信息,如图3-2所示。
图3-2
hello-world镜像竟然还不到2KB!通过docker run运行,如图3-3所示。
其实我们更关心hello-world镜像包含哪些内容。
Dockerfile是镜像的描述文件,定义了如何构建Docker镜像。Dockerfile的语法简洁且可读性强,后面我们会专门讨论如何编写Dockerfile。
hello-world的Dockerfile内容如图3-4所示。
图3-3
图3-4
只有短短三条指令。
(1)FROM scratch镜像是从白手起家,从0开始构建。
(2)COPY hello/将文件“hello”复制到镜像的根目录。
(3)CMD["/hello"]容器启动时,执行/hello。
镜像hello-world中就只有一个可执行文件“hello”,其功能就是打印出“Hello from Docker ......”等信息。
/hello就是文件系统的全部内容,连最基本的 /bin、/usr、/lib、 /dev都没有。
hello-world虽然是一个完整的镜像,但它并没有什么实际用途。通常来说,我们希望镜像能提供一个基本的操作系统环境,用户可以根据需要安装和配置软件。
这样的镜像我们称作base镜像。
3.1.2 base镜像
base镜像有两层含义:(1)不依赖其他镜像,从scratch构建;(2)其他镜像可以以之为基础进行扩展。
所以,能称作base镜像的通常都是各种Linux发行版的Docker镜像,比如Ubuntu、Debian、CentOS等。
我们以CentOS为例考察base镜像包含哪些内容。
下载镜像:
docker pull centos
查看镜像信息,如图3-5所示。
图3-5
镜像大小不到200MB。
等一下!
一个CentOS才200MB ?
平时我们安装一个CentOS至少都有几个GB,怎么可能才200MB!
相信这是几乎所有Docker初学者都会有的疑问,包括我自己。下面我们来解释这个问题。
Linux操作系统由内核空间和用户空间组成,如图3-6所示。
图3-6
1. rootfs
内核空间是kernel, Linux刚启动时会加载bootfs文件系统,之后bootfs会被卸载掉。
用户空间的文件系统是rootfs,包含我们熟悉的 /dev、/proc、/bin等目录。
对于base镜像来说,底层直接用Host的kernel,自己只需要提供rootfs就行了。
而对于一个精简的OS, rootfs可以很小,只需要包括最基本的命令、工具和程序库就可以了。相比其他Linux发行版,CentOS的rootfs已经算臃肿的了,alpine还不到10MB。
我们平时安装的CentOS除了rootfs还会选装很多软件、服务、图形桌面等,需要好几个GB就不足为奇了。
2. base镜像提供的是最小安装的Linux发行版
CentOS镜像的Dockerfile的内容如图3-7所示。
图3-7
第二行ADD指令添加到镜像的tar包就是CentOS 7的rootfs。在制作镜像时,这个tar包会自动解压到/目录下,生成/dev、/proc、/bin等目录。
注:可在Docker Hub的镜像描述页面中查看Dockerfile。
3.支持运行多种Linux OS
不同Linux发行版的区别主要就是rootfs。
比如Ubuntu 14.04使用upstart管理服务,apt管理软件包;而CentOS 7使用systemd和yum。这些都是用户空间上的区别,Linux kernel差别不大。
所以Docker可以同时支持多种Linux镜像,模拟出多种操作系统环境,如图3-8所示。
图3-8
上图Debian和BusyBox(一种嵌入式Linux)上层提供各自的rootfs,底层共用Docker Host的kernel。
这里需要说明的是:
(1)base镜像只是在用户空间与发行版一致,kernel版本与发行版是不同的。
例如CentOS 7使用3.x.x的kernel,如果Docker Host是Ubuntu 16.04(比如我们的实验环境),那么在CentOS容器中使用的实际上是Host 4.x.x的kernel,如图3-9所示。
图3-9
① Host kernel为4.4.0-31。
② 启动并进入CentOS容器。
③ 验证容器是CentOS 7。
④ 容器的kernel版本与Host一致。
(2)容器只能使用Host的kernel,并且不能修改。
所有容器都共用host的kernel,在容器中没办法对kernel升级。如果容器对kernel版本有要求(比如应用只能在某个kernel版本下运行),则不建议用容器,这种场景虚拟机可能更合适。
3.1.3 镜像的分层结构
Docker支持通过扩展现有镜像,创建新的镜像。
实际上,Docker Hub中99%的镜像都是通过在base镜像中安装和配置需要的软件构建出来的。比如我们现在构建一个新的镜像,Dockerfile如图3-10所示。
图3-10
① 新镜像不再是从scratch开始,而是直接在Debian base镜像上构建。
② 安装emacs编辑器。
③ 安装apache2。
④ 容器启动时运行bash。
构建过程如图3-11所示。
图3-11
可以看到,新镜像是从base镜像一层一层叠加生成的。每安装一个软件,就在现有镜像的基础上增加一层。
为什么Docker镜像要采用这种分层结构呢?
最大的一个好处就是:共享资源。
比如:有多个镜像都从相同的base镜像构建而来,那么Docker Host只需在磁盘上保存一份base镜像;同时内存中也只需加载一份base镜像,就可以为所有容器服务了,而且镜像的每一层都可以被共享,我们将在后面更深入地讨论这个特性。
这时可能就有人会问了:如果多个容器共享一份基础镜像,当某个容器修改了基础镜像的内容,比如 /etc下的文件,这时其他容器的 /etc是否也会被修改?
答案是不会!
修改会被限制在单个容器内。
这就是我们接下来要学习的容器Copy-on-Write特性。
可写的容器层
当容器启动时,一个新的可写层被加载到镜像的顶部。
这一层通常被称作“容器层”,“容器层”之下的都叫“镜像层”,如图3-12所示。
图3-12
所有对容器的改动,无论添加、删除,还是修改文件都只会发生在容器层中。只有容器层是可写的,容器层下面的所有镜像层都是只读的。
下面我们深入讨论容器层的细节。
镜像层数量可能会很多,所有镜像层会联合在一起组成一个统一的文件系统。如果不同层中有一个相同路径的文件,比如 /a,上层的 /a会覆盖下层的 /a,也就是说用户只能访问到上层中的文件 /a。在容器层中,用户看到的是一个叠加之后的文件系统。
(1)添加文件。在容器中创建文件时,新文件被添加到容器层中。
(2)读取文件。在容器中读取某个文件时,Docker会从上往下依次在各镜像层中查找此文件。一旦找到,打开并读入内存。
(3)修改文件。在容器中修改已存在的文件时,Docker会从上往下依次在各镜像层中查找此文件。一旦找到,立即将其复制到容器层,然后修改之。
(4)删除文件。在容器中删除文件时,Docker也是从上往下依次在镜像层中查找此文件。找到后,会在容器层中记录下此删除操作。
只有当需要修改时才复制一份数据,这种特性被称作Copy-on-Write。可见,容器层保存的是镜像变化的部分,不会对镜像本身进行任何修改。
这样就解释了我们前面提出的问题:容器层记录对镜像的修改,所有镜像层都是只读的,不会被容器修改,所以镜像可以被多个容器共享。