【Docker 那些事儿】容器为什么傲娇?全靠镜像撑腰
🌟 前言
<font color=#008000 >Docker 镜像是 Docker 容器的基石,容器是镜像的运行实例,有了镜像才能启动容器。 Docker 镜像是一个只读的模板,一个独立的文件系统,包括运行一个容器所需的数据,可以用来创建容器。</font>
1. base 镜像
base(基础) 镜像是指完全从零开始构建的镜像, 它不会依赖其他镜像,甚至会成为被依赖的镜像,其他镜像以它为基础进行扩展。 通常 base 镜像都是 Linux 的系统镜像, 如 Ubuntu、CentOS、Debian 等。 下面通过 Docker 拉取一个 base 镜像并查看, 这里以 CentOS 为例, 示例代码如下:
从以上示例中可以看出,一个 CentOS 镜像大小只有 202MB,但在安装系统时,一个 CentOS 大概有几 GB,这与操作系统有关。 先观察 Linux 原本的操作系统结构,如图所示👇
Kernel 是内核空间。bootfs 文件系统在 Linux 启动时加载。rootfs 是包含操作命令的文件系统。 base 镜像的创建过程中,Kernel、 bootfs 与 rootfs 都会加载,然后 bootfs 文件系统 (包括 Kernel) 被卸载掉,镜像只保留 rootfs 文件系统,供用户进行操作。bootfs 与 Kernel 将与宿主机共享。 另外,为了增加 Docker 的灵活性,base 镜像提供的都是最小安装的 Linux 系统。Linux 系统不同的发行版之间最大的区别就是 rootfs 的不同,例如,Ubuntu 系统的应用程序管理器是 apt,而 CentOS 是 yum。 由此可见,只要提供不同的 rootfs 文件系统就可以同时支持多种操作系统,如图所示👇
从上图中可以看到,两个不同的 Linux 发行版提供了各自的 rootfs 文件系统,而它们共用的是底层宿主机的 Kernel。 假设宿主机的系统是 Ubuntu 16.04,Kernel 版本是 4.4.0,无论 base 镜像原本的发行版 Kernel 版本如何,在这台宿主机上都是 4.4.0。 下面通过示例来验证,示例代码如下:
从上述示例中可以看出,base 镜像与宿主机的 Kernel 版本都是 3.10。 base 镜像的 Kernel 是与宿主机共享的,其版本与宿主机一致,并且不能进行修改。
2. 镜像的本质
Docker 镜像是一个只读的文件系统,由一层一层的文件系统组成,每一层仅镜像的本质包含前一层的差异部分,这种层级文件系统被称为 UnionFS。 大多数 Docker 镜像都在 base 镜像的基础上进行创建,每进行一次新的创建就会在镜像上构建一个新的 UnionFS。查看 ubuntu:15.04 镜像的层级结构,示例代码如下:
通常,对 Docker 的操作命令都是以 “docker” 开头。pull 是下载镜像的命令,在英文中是 “拉” 的意思,所以下载镜像又叫作 拉取镜像。 以上示例中,第 5 行到第 8 行是每一层 UnionFS 的 ID 号,第 9 行是整个镜像的 ID 号,这个 ID 号可以用来操控镜像。 然后,查看镜像,示例代码如下:
在以上示例中,不仅可以看到先前下载的 Ubuntu15.04 镜像,还可以看到其他镜像,说明
docker images
是查看本地所有镜像的命令。而查看到的信息中,除了镜像名称,还有版本号、镜像 ID 号、创建时间以及镜像大小。 接着,通过命令查看镜像的构建过程,示例代码如下:这里使用 “history” 与镜像 ID 号组合的命令查看镜像构建过程,所显示的信息包括镜像 ID 号、创建时间、由什么命令创建以及镜像大小。 从以上示例中的信息可以看出,ubuntu:15.04 镜像由四个只读层 (Read Layer) 构建而成,每一层都是由一条命令构成的,最终得到 ID 号为 dlb55fd07600 的镜像,但以用户的视角只能看到最上层。 当用户将这个镜像放在容器中运行时,四层之上会创建出一个可读可写层(Read-Write Layer),用户对 Docker 的操作都通过可读可写层进行。如果用户修改了一个已存在的文件,那该文件将会从可读可写层下的只读层复制到可读可写层,该文件的只读版本仍然存在,只是已经被可读可写层中该文件的副本所隐藏。 可读可写层又叫作容器层,只读层又叫作镜像层,容器层之下均为镜像层,层级结构如图所示👇
镜像的这种分层机制最大的一个好处就是:共享资源。 例如,有很多个镜像都基于一个基础镜像构建而来,那么在本地的仓库中就只需要保存一份基础镜像,所有需要此基础镜像的容器都可以共享它,而且镜像的每一层都可以被共享,从而节省磁盘空间。 因为有了分层机制,本地保存的基础镜像都是只读的文件系统,不用担心对容器的操作会对镜像有什么影响。 为了将零星的数据整合起来,人们提出了镜像层 (Image Layer) 这个概念,如图所示👇 下图所示为一个镜像层,我们能够发现,一个层并不仅仅包含文件系统的改变,它还能包含其他重要的信息。
元数据 (Metadata) 就是关于这个层的额外信息,包括 Docker 运行时的信息与父镜像层的信息,并且只读层与可读可写层都包含元数据,如图所示👇
除此之外,每一层还有一个指向父镜像层的指针。如果没有这个指针,说明它处于最底层,是一个基础镜像,如图所示👇
3. 查找本地镜像
Docker 本地镜像通常是储存在服务器上的,下面验证本地镜像的储存路径,示例代码如下:
从以上示例中可以看到,Docker 本地镜像储存路径是
/var/Iib/Docker
。 在本地查看镜像时,通常使用docker images
命令,示例代码如下:从以上示例中可以看到,结果显示中有多项镜像信息,下面对信息进行解释。
🍇 REPOSITORY
镜像仓库,即一些关联镜像的集合。 例如,Ubuntu 的每个镜像对应着不同的版本。与 Docker Registry 不同,镜像仓库提供 Docker 镜像的存储服务。 即 Docker Registry 中有很多镜像仓库,镜像仓库中有很多镜像 (相互独立)。
🍇 TAG
镜像的标签,常用来区分不同的版本,默认标签为 latest。
🍇 IMAGE ID
镜像的 ID 号,镜像的唯一标识,常用于操作镜像 (默认值只列出前 12 位)。
🍇 CREATED
镜像创建的时间。
🍇 SIZE
镜像的大小。
🍇 参数用法
在 docker images
命令后加上不同的参数就形成了不同的查询方式,导致不同的查询结果。
下面介绍各参数的含义以及用法。
-a
表示显示所有本地镜像,默认不显示中间层镜像,这是工作中经常使用到的参数,用来从本地镜像中寻找符合生产条件的镜像。 示例代码如下:
-q
表示只显示本地所有镜像 ID 号。 示例代码如下:
-no-trunc
表示使用不截断的模式显示,并显示完整的镜像 ID 号。 示例代码如下:
4. 构建镜像
Docker 的官方镜像库 Docker Hub 发布了成千上万的公共镜像供全球用户使用。用户可以直接拉取(下载)所需要的镜像,提高了工作效率。但是在很多工作环境中,一旦对镜像有特殊需求,就需要我们手动去构建镜像。 本文章将会介绍基于
docker commit
命令与 Dockerfile 两种方式来构建自己的 Docker 镜像。
🍑 使用 docker commit 命令构建镜像
使用
docker commit
命令将容器的可读可写层转换为一个只读层,这样就把一个容器转换成了一个不可变的镜像,如图所示👇下面我们给一个 Centos 的镜像安装一个 Vim 服务,设置开机启动,并将其构建成一个新的镜像,以免每次启动容器都要再次安装 Vim。 首先启动一个 Centos 的容器,示例代码如下:
从以上示例中可以看到,容器启动之后,主机名发生了改变,说明用户直接进入了容器,再进行操作就是对容器的操作。 然后,在容器中安装 Vim,示例代码如下:
安装完成之后,退出容器,示例代码如下:
使用 exit 命令退出容器之后, 该容器将默认关闭。下面使用
docker commit
命令在 CentOS 镜像的基础上创建新的镜像,示例代码如下:在命令中需要用镜像 ID 号来指定基础镜像,并不需要将 ID 号都输入进去,只要输入几个字符使 ID 号与其他镜像不冲突即可。 此时可以看到刚刚构建的新镜像,代码如下:
从以上示例中可以看到, 新镜像的大小是 326MB,而此前的 CentOS 镜像只有 202MB, 这是因为在安装 Vim 时还安装了许多依赖包。 然后, 查看镜像中是否已经自动安装了 Vim, 示例代码如下:
从以上示例中可以看到,新镜像已经包含了 Vim。 这种构建新镜像的方式在工作中并不常见,原因如下。 (1)效率低下,如果要给 Ubuntu 镜像也添加一个 Vim,需要将上述全部过程重复一遍。 (2)不透明,用户使用时不知道镜像是如何构建的,难以对镜像做出正确的判断。
🍑 使用 Dockerfile 构建镜像
镜像可以基于 Dockerfile 构建。Dockerfile 是一个描述文件,包含若干条命令,每条命令都会为基础文件系统创建新的层次结构,这正好弥补了
docker commit
构建镜像效率低下的缺点。 Dockerfile 定义容器内部环境中发生的事情。网络接口和磁盘驱动器等资源的访问在此环境内虚拟化,与系统的其余部分隔离。 Dockerfile 主要使用docker build
命令,根据 Dockerfile 文件中的指令,执行若干次docker commit
命令构建镜像,每次执行docker commit
命令时都会生成一个新的层,因此许多新的层会被创建,如图所示👇
🍑 Dockerfile 常用命令
下面介绍 Dockerfile 中常用的命令,完整说明见官方文档。
FROM
指定源镜像,必须是已经存在的镜像,必须是 Dockerfile 中第一条非注释的命令,因为其后的所有指令都使用该镜像。
MAINTAINER
指定作者信息。
RUN
在当前容器中运行指定的命令。
EXPOSE
指定运行容器时要使用的端口。可以使用多个 EXPOSE 命令。
CMD
指定容器启动时运行的命令,Dockerfile 可以出现多个 CMD 指令,但只有最后一个生效。CMD 可以被启动容器时添加的命令覆盖。
ENTRYPOINT
CMD 或容器启动时添加的命令会被当做参数传递给 ENTRYPOINT。
COPY
文件或目录复制到当前容器中。
ADD
将文件或者目录复制到当前容器中,源文件如果是归档(压缩)文件,则会被自动解压到目标位置。
VOLUME
为容器添加容器卷,可以存在于一个或多个目录,用来提供共享存储。该命令会在容器数据卷部分详细介绍。
WORKDIR
在容器内设置工作目录。
ENV
设置环境变量。
USER
指定容器以什么用户身份运行,默认是 root。
🍑 运行一个 Dockerfile
下面演示使用 Docker file 创建
centos/vim
,示例代码如下:这里在宿主机的 root 目录下创建了一个 Dockerfile 文件。 接着,向 Docker file 文件中添加内容, 示例代码如下:
添加完成之后,保存并退出。 有了 Dockerfile 文件之后即可创建新的镜像,示例代码如下:
通过
docker build
命令执行 Dockerfile 文件,-t 用来指定新镜像名为centos/vim-Dockerfile
,命令行末尾的.
表示 Dockerfile 文件在当前目录,Docker 默认从指定的目录寻找 Dockerfile 文件,也可以使用 -f 参数指定 Dockerfile 文件的位置。构建完成之后,查看镜像是否构建成功,示例代码如下:
从以上示例中可以看到,新镜像已经构建成功。 使用 Dockerfile 构建镜像基本可以分为以下五步。 (1) 选择一个基础镜像,运行一个临时容器。 (2) 执行一条命令,对容器做修改。 (3) 执行类似
docker commit
的操作,生成一个新的镜像。 (4) 删除临时容器,再基于刚刚构建好的新镜像运行一个临时容器。 (5) 重复 (2) (3) (4) 步,直到执行完 Dockerfile 中的所有指令。centos/vim-Dockerfile
由 CentOS 基础镜像和RUN yum -y install vim
构成,现在两个镜像都包含了 ID 号为 lel148e4cc2c 的只读层,如图所示👇以上结论可以使用
docker history
命令验证,docker history
命令专门用来查看镜像的结构,示例代码如下:这里可以看到 CentOS 镜像中确实包含了 ID 号为 1e1148e4cc2c 的只读层。 接着再查看新镜像
centos/vim-Dockerfile
的结构,示例代码如下:从以上示例中可以看到,两个镜像都含有一个相同的只读层,并且这个只读层是共享的。 Docker 构建镜像时有缓存机制,如果构建镜像层时该镜像层已经存在,就直接使用,无须重新构建。 下面为先前的 Dockerfile 文件添加一点内容,安装一个 ntp 服务,重新构建一个新的镜像,示例代码如下:
这里多加了一条安装 ntp 服务的命令。 添加完成后,开始创建镜像,示例代码如下:
在示例的第 6 行代码中可以看到,Docker 没有重新安装 Vim,而是直接使用了先前安装过的缓存。 Dockerfile 文件是从上至下依次执行的,上层依赖于下层。无论什么时候,只要某一层发生变化,其上面所有层的缓存都会失效。 改变先前的 Dockerfile 文件中两条 RUN 命令的上下顺序,观察 Docker 还会不会使用缓存机制,示例代码如下:
将 Dockerfile 中两条 RUN 命令的顺序互换之后,开始创建镜像,示例代码如下:
由以上验证可知,将两条 RUN 命令交换顺序导致镜像层次发生改变,Docker 会重建镜像层。由此可见 Docker 的镜像层级结构特性:只有下面的层次内容、顺序完全一致才会使用缓存机制。 如果在构建镜像时不想使用缓存,可以在
docker build
命令中添加--no-cache
参数,否则默认使用缓存。 除了在使用 Dockerfile 构建镜像时有缓存机制,在从仓库拉取镜像时也会有缓存机制,即已经拉取到本地的镜像层可以被多个镜像共同使用,可以说是一次拉取多次使用,前提是下层镜像完全相同。 通常使用 Dockerfile 构建镜像时,如果由于某些原因镜像构建失败,我们能够得到前一个指令成功执行构建出的镜像,继而可以运行这个镜像查找指令失败的原因,这对调试 Dockerfile 有极大的帮助。 从 Docker Hub 拉取的 CentOS 镜像是最小化的,其中没有 vim 命令。下面测试错误构建 Docker 镜像的结果,示例代码如下:将 Dockerfile 中任意一条 RUN 命令改为错误的,再开始创建镜像,示例代码如下:
在示例中,由于第三步报错,镜像没有创建成功。但也生成了一个新镜像,这个镜像是第二步操作构建的,通常可以通过这个新镜像排查错误,示例代码如下:
Docker 容器技术中,编写 Dockerfile 文件是非常重要的部分,下面总结编写 Dockerfile 文件的一些小技巧,相信可以帮助大家更好地使用 Docker 与 Dockerfile。
(1)容器中只运行单个应用。
从技术角度讲,在一个容器中可以实现整个 LNMP (Linux+Nginx+MySQL+PHP) 架构。但这样做有很大的弊端。首先,镜像构建的时间会非常长,每次修改都要重新构建; 其次,镜像文件会非常大,大大降低容器的灵活性。
(2)将多个 RUN 指令合并成一个。
众所周知,Docker 镜像是分层的,Dockerfile 中的每一条指令都会创建一个新的镜像层,镜像层是只读的。 Docker 镜像层类似于洋葱,想要更改内层,需要将外层全部撕掉。
(3)基础镜像的标签尽量不要使用 latest。
当镜像的标签没有指定时,默认使用 latest 标签。 当镜像更新时,latest 标签会指向不同的镜像,可能会对服务产生影响。
(4)执行 RUN 命令后删除多余文件。
假设执行了更新 yum 源的命令,会自动下载解压一些软件包,但是在运行容器的时候不需要这些包。最好将它们删除,因为这些软件包会使镜像 SIZE 变大。
(5)合理调整 COPY 与 RUN 的顺序。
将变化少的部分放在 Dockerfile 文件的前面,充分利用镜像缓存机制。
(6)选择合适的基础镜像。
最好选择满足环境需要而且体积小巧的镜像,比如 Alpine 版本的 node 镜像。 Alpine 是一个极小化的 Linux 发行版,只有 5.5MB,非常适合作为基础镜像。
版权声明: 本文为 InfoQ 作者【飞向星的客机】的原创文章。
原文链接:【http://xie.infoq.cn/article/b5dacd0d314f186084f8cea54】。文章转载请联系作者。
评论