qdhxhz

镜像是 Docker 容器的基石,容器是镜像的运行实例,有了镜像才能启动容器。为什么我们要讨论镜像的内部结构?

如果只是使用镜像,当然不需要了解,直接通过 docker 命令下载和运行就可以了。

但如果我们想创建自己的镜像,或者想理解 Docker 为什么是轻量级的,就非常有必要学习这部分知识了。

一、最小的镜像

1、运行hello-world镜像

hello-world 是 Docker 官方提供的一个镜像,通常用来验证 Docker 是否安装成功。

我们先通过 docker pull 从 Docker Hub 下载它。

docker images 命令查看镜像的信息。

hello-world 镜像竟然还不到 14KB! 通过 docker run 运行。

其实我们更关心 hello-world 镜像包含哪些内容。

2. hello-world镜像内容

Dockerfile 是镜像的描述文件,定义了如何构建Docker镜像。Dockerfile的语法简洁且可读性强,后面我们会专门讨论如何编写Dockerfile。

hello-world 的 Dockerfile 内容如下:

只有短短三条指令。

#1、此镜像是从白手起家,从 0 开始构建。
FROM scratch
#2、将文件“hello”复制到镜像的根目录。
COPY hello /
#3、容器启动时,执行 /hello
CMD ["/hello"]

镜像 hello-world 中就只有一个可执行文件 “hello”,其功能就是打印出 “Hello from Docker ......” 等信息。

/hello 就是文件系统的全部内容,连最基本的 /bin,/usr, /lib, /dev 都没有。

hello-world 虽然是一个完整的镜像,但它并没有什么实际用途。通常来说,我们希望镜像能提供一个基本的操作系统环境,用户可以根据

需要安装和配置软件。这样的镜像我们称作 base 镜像。我们下一节讨论 base 镜像。


二、base 镜像

1.base镜像含义

base 镜像有两层含义:

1、不依赖其他镜像,从 scratch 构建。
2、其他镜像可以之为基础进行扩展。

所以,能称作 base 镜像的通常都是各种 Linux 发行版的 Docker 镜像,比如 Ubuntu, Debian, CentOS 等。

2.base镜像内容

我们以 CentOS 为例考察 base 镜像包含哪些内容。

下载镜像:docker pull centos

查看镜像信息

镜像大小 231MB。等一下!一个 CentOS 才 200MB ?平时我们安装一个 CentOS 至少都有几个 GB,怎么可能才 200MB !

相信这是几乎所有 Docker 初学者都会有的疑问,包括我自己。下面我们来解释这个问题。

Linux 操作系统由内核空间用户空间组成。如下图所示:

内核空间是kernel,Linux 刚启动时会加载 bootfs 文件系统,之后 bootfs 会被卸载掉。用户空间的文件系统是 rootfs,包含我们熟悉的

/dev, /proc, /bin 等目录。对于 base 镜像来说,底层直接用 Host 的 kernel,自己只需要提供 rootfs 就行了。

而对于一个精简的 OS,rootfs 可以很小,只需要包括最基本的命令、工具和程序库就可以了。相比其他 Linux 发行版,CentOS 的 rootfs

已经算臃肿的了,alpine 还不到 10MB。我们平时安装的 CentOS 除了 rootfs 还会选装很多软件、服务、图形桌面等,需要好几个 GB 就

不足为奇了。下面是 CentOS 镜像的 Dockerfile 的内容:

第二行 ADD 指令添加到镜像的 tar 包就是 CentOS 7 的 rootfs。在制作镜像时,tar包会自动解压到 / 目录下,生成 /dev, /porc, /bin 等目录。

不同 Linux 发行版的区别主要就是 rootfs。

比如 Ubuntu 14.04 使用 upstart 管理服务,apt 管理软件包;而 CentOS 7 使用 systemd 和 yum。这些都是用户空间上的区别,

Linux kernel 差别不大。所以 Docker 可以同时支持多种 Linux 镜像,模拟出多种操作系统环境。

上图 Debian 和 BusyBox(一种嵌入式 Linux)上层提供各自的 rootfs,底层共用 Docker Host 的 kernel。

这里需要说明的是:

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

下一节我们讨论镜像的分层结构。


三、镜像的分层结构

Docker 支持通过扩展现有镜像,创建新的镜像。

1、镜像分层示例

实际上,Docker Hub 中 99% 的镜像都是通过在 base 镜像中安装和配置需要的软件构建出来的。比如我们现在构建一个新的镜像,Dockerfile 如下:

① 新镜像不再是从 scratch 开始,而是直接在 Debian base 镜像上构建。
② 安装 emacs 编辑器。
③ 安装 apache2。
④ 容器启动时运行 bash。

构建过程如下图所示:

可以看到,新镜像是从 base 镜像一层一层叠加生成的。每安装一个软件,就在现有镜像的基础上增加一层

2、镜像分层好处

问什么 Docker 镜像要采用这种分层结构呢?

最大的一个好处就是 - 共享资源

比如:有多个镜像都从相同的 base 镜像构建而来,那么 Docker Host 只需在磁盘上保存一份 base 镜像;同时内存中也只需加载一份 base

镜像,就可以为所有容器服务了。而且镜像的每一层都可以被共享,我们将在后面更深入地讨论这个特性。

这时可能就有人会问了:如果多个容器共享一份基础镜像,当某个容器修改了基础镜像的内容,比如 /etc 下的文件,这时其他容器的 /etc

是否也会被修改?

答案是不会!

修改会被限制在单个容器内

这就是我们接下来要学习的容器 Copy-on-Write 特性。

3、Copy-on-Write 特性

可写的容器层

当容器启动时,一个新的可写层被加载到镜像的顶部。这一层通常被称作“容器层”,“容器层”之下的都叫“镜像层”。

所有对容器的改动 - 无论添加、删除、还是修改文件都只会发生在容器层中。

只有容器层是可写的,容器层下面的所有镜像层都是只读的

下面我们深入讨论容器层的细节。

镜像层数量可能会很多,所有镜像层会联合在一起组成一个统一的文件系统。如果不同层中有一个相同路径的文件,比如 /a,上层的 /a 会

覆盖下层的 /a,也就是说用户只能访问到上层中的文件 /a。在容器层中,用户看到的是一个叠加之后的文件系统。

添加文件

在容器中创建文件时,新文件被添加到容器层中。

读取文件

在容器中读取某个文件时,Docker 会从上往下依次在各镜像层中查找此文件。一旦找到,打开并读入内存。

修改文件

在容器中修改已存在的文件时,Docker 会从上往下依次在各镜像层中查找此文件。一旦找到,立即将其复制到容器层,然后修改之。

删除文件

在容器中删除文件时,Docker 也是从上往下依次在镜像层中查找此文件。找到后,会在容器层中记录下此删除操作。

只有当需要修改时才复制一份数据,这种特性被称作 Copy-on-Write。可见容器层保存的是镜像变化的部分,不会对镜像本身进行任何修改。

这样就解释了我们前面提出的问题:*容器层记录对镜像的修改,所有镜像层都是只读的,不会被容器修改,所以镜像可以被多个容器共享

理解了镜像的原理和结构,下一节我们学习如何构建镜像。


四、构建镜像

1、为何要构建镜像

对于 Docker 用户来说,最好的情况是不需要自己创建镜像。几乎所有常用的数据库、中间件、应用软件等都有现成的 Docker 官方镜像或

其他人和组织创建的镜像,我们只需要稍作配置就可以直接使用。

使用现成镜像的好处除了省去自己做镜像的工作量外,更重要的是可以利用前人的经验。特别是使用那些官方镜像,因为 Docker 的工程师

知道如何更好的在容器中运行软件。

当然,某些情况下我们也不得不自己构建镜像,比如:

1. 找不到现成的镜像,比如自己开发的应用程序。
2. 需要在镜像中加入特定的功能,比如官方镜像几乎都不提供 ssh。

所以本节我们将介绍构建镜像的方法。同时分析构建的过程也能够加深我们对前面镜像分层结构的理解。

2、构建镜像方法

Docker 提供了两种构建镜像的方法:

1. docker commit 命令
2. Dockerfile 构建文件

3、docker commit构建镜像

docker commit 命令是创建新镜像最直观的方法,其过程包含三个步骤:

1. 运行容器
2. 修改容器
3. 将容器保存为新的镜像

举个例子:在 ubuntu base 镜像中安装 vi 并保存为新镜像。

1)、第一步:运行容器

-it 参数的作用是以交互模式进入容器,并打开终端。6e2d389d4576 是容器的内部 ID。

2)、第二步:安装 vi

确认 vi 没有安装。开始安装 apt-get install -y vim

3)、第三步:保存新镜像

在新窗口中查看当前运行的容器。

gifted_stallman 是 Docker 为我们的容器随机分配的名字。

执行 docker commit 命令将容器保存为镜像。

新镜像命名为 ubuntu-with-vi

查看新镜像的属性。

从 size 上看到镜像因为安装了软件而变大了。从新镜像启动容器,验证 vi 已经可以使用。

以上演示了如何用 docker commit 创建新镜像。然而,Docker 并不建议用户通过这种方式构建镜像。原因如下:

1. 这是一种手工创建镜像的方式,容易出错,效率低且可重复性弱。比如要在 debian base 镜像中也加入 vi,还得重复前面的所有步骤。
2. 更重要的:使用者并不知道镜像是如何创建出来的,里面是否有恶意程序。也就是说无法对镜像进行审计,存在安全隐患。

既然 docker commit 不是推荐的方法,我们干嘛还要花时间学习呢?

原因是:即便是用 Dockerfile(推荐方法)构建镜像,底层也 docker commit 一层一层构建新镜像的。学习 docker commit 能够帮助我们

更加深入地理解构建过程和镜像的分层结构。

下一节我们学习如何通过 Dockerfile 构建镜像。


五、Dockerfile 构建镜像

Dockerfile 是一个文本文件,记录了镜像构建的所有步骤。

1、Dockerfile 构建镜像

1)创建Dockerfile文件

touch Dockerfile

2)用 Dockerfile 创建上节的 ubuntu-with-vi,其内容则为:

3)构建镜像

docker build -t ubuntu-with-vi-dockerfile . 

ubuntu-with-vi-dockerfile是构建镜像所取的名字

运行 docker build 命令,-t 将新镜像命名为 ubuntu-with-vi-dockerfile,命令末尾的 . 指明 build context 为当前目录。

Docker 默认会从 build context 中查找 Dockerfile 文件,我们也可以通过 -f 参数指定 Dockerfile 的位置。

4)镜像构建成功

通过 docker images 查看镜像信息。

可以看到新镜像已经构建成功,而且大小跟之前docker commit 构建的大小是一样大的。


参考

本博客所有内容均来自 《每天5分钟玩转 Docker 容器技术》书籍



相关文章:

猜你喜欢