了解Docker层


27

我们的代码块如下Dockerfile

RUN yum -y update
RUN yum -y install epel-release
RUN yum -y groupinstall "Development Tools"
RUN yum -y install python-pip git mysql-devel libxml2-devel libxslt-devel python-devel openldap-devel libffi-devel openssl-devel

有人告诉我,我们应该团结起来使用这些RUN命令,以减少创建的docker层:

RUN yum -y update \
    && yum -y install epel-release \
    && yum -y groupinstall "Development Tools" \
    && yum -y install python-pip git mysql-devel libxml2-devel libxslt-devel python-devel openldap-devel libffi-devel openssl-devel

我是Docker的新手,不确定我是否完全了解指定多个RUN命令的这两个版本之间的区别。一个人什么时候可以将一个RUN命令组合成一个命令,什么时候拥有多个RUN命令才有意义?


Answers:


35

泊坞窗映像实际上是文件系统层的链接列表。Dockerfile中的每个指令都会创建一个文件系统层,该层描述执行相应指令之前和之后文件系统中的差异。该docker inspect子命令可以在docker映像上使用,以揭示其作为文件系统层链接列表的性质。

图像中使用的层数很重要

  • 推或拉图像时,它会影响并发上载或下载的次数。
  • 启动容器时,由于将各层组合在一起以产生容器中使用的文件系统;涉及的层越多,性能越差,但是不同的文件系统后端受到的影响也不同。

这对应该如何构建图像有几个影响。我可以提供的第一个也是最重要的建议是:

建议#1确保涉及源代码的构建步骤尽可能早地出现在Dockerfile中,并且不要与使用a &&或a的先前命令绑定在一起;

这样做的原因是,所有先前的步骤都将被缓存,并且不需要一遍又一遍地下载相应的层。这意味着您可能需要更快的构建和更快的发行。有趣的是,很难充分利用docker缓存。

我的第二条建议不太重要,但从维护角度来看,它非常有用:

建议2:请勿在Dockerfile中编写复杂的命令,而应使用要复制和执行的脚本。

遵循此建议的Dockerfile看起来像

COPY apt_setup.sh /root/
RUN sh -x /root/apt_setup.sh
COPY install_pacakges.sh /root/
RUN sh -x /root/install_packages.sh

等等。绑定多个命令的建议&&范围有限。使用脚本编写脚本要容易得多,在脚本中可以使用函数等来避免冗余或出于文档目的。

人们对预处理器感兴趣,并愿意避免这些COPY步骤带来的小开销,并且实际上是在动态生成Dockerfile,其中

COPY apt_setup.sh /root/
RUN sh -x /root/apt_setup.sh

序列被替换为

RUN base64 --decode … | sh -x

其中是的base64编码版本apt_setup.sh

我的第三条建议是给那些想要以更长的构建成本为代价来限制层的大小和数量的人的。

建议#3使用with-idiom避免文件存在于中间层中,但不存在于生成的文件系统中。

在结果文件系统中不存在由某​​些docker指令添加并由后续指令删除的文件,但在构成docker镜像的docker层中有两次提到该文件。一次,在添加指令的结果层中添加名称和完整内容,一次在删除该指令的结果中,作为删除通知。

例如,假设我们暂时需要一个C编译器和一些映像,并考虑

# !!! THIS DISPLAYS SOME PROBLEM --- DO NOT USE !!!
RUN apt-get install -y gcc
RUN gcc --version
RUN apt-get --purge autoremove -y gcc

(一个更现实的示例将使用编译器构建一些软件,而不是仅使用--version标志声明其存在。)

Dockerfile片段创建了三层,第一层包含完整的gcc套件,因此,即使最终文件系统中不存在该套件,相应的数据也仍然以相同的方式成为映像的一部分,并且需要在每次下载,上传和解压缩时最终的图像是。

with-idiom是在功能编程到分离资源所有权和资源使用它从逻辑释放常见形式。这是很容易转这个成语的shell脚本,我们可以改写先前的命令,下面的脚本,与被用来COPY & RUN作为建议#2。

# with_c_compiler SIMPLE-COMMAND
#  Execute SIMPLE-COMMAND in a sub-shell with gcc being available.

with_c_compiler()
(
    set -e
    apt-get install -y gcc
    "$@"
    trap 'apt-get --purge autoremove -y gcc' EXIT
)

with_c_compiler\
    gcc --version

复杂的命令可以转换为功能,以便将其提供给with_c_compiler。也可以链接几个with_whatever函数的调用,但可能不是很理想。(使用外壳程序更深奥的功能,当然可以使with_c_compiler接受复杂的命令成为可能,但是在所有方面,最好将这些复杂的命令包装成函数。)

如果我们想忽略建议2,则生成的Dockerfile代码段将是

RUN apt-get install -y gcc\
 && gcc --version\
 && apt-get --purge autoremove -y gcc

由于混淆,它不太容易阅读和维护。了解shell脚本变体如何强调重要部分,gcc --version而链式&&变体则将其掩埋在噪声中间。


1
您可以在使用一个脚本并在一个RUN语句中使用多个命令来生成框大小的结果吗?
030

1
对我来说,将映像库的配置(即OS的东西)甚至libs与您编写的源代码的设置混合在一起似乎是一个坏主意。您说:“确保涉及源代码的构建步骤尽可能晚。” 使该零件成为完全独立的工件有什么问题吗?
JimmyJames

1
@ 030“盒子”大小是什么意思?我不知道您指的是哪个盒子。
迈克尔·勒·巴比尔·格吕讷瓦尔德

1
我的意思是泊坞窗图片大小
030

1
@JimmyJames这在很大程度上取决于您的部署方案。如果我们假设已编译程序,则“正确的做法”是将其打包并安装该软件包依赖项和软件包本身,这是两个截然不同的即将完成的步骤。这样可以最大程度地利用docker缓存,并避免重复下载具有相同文件的层。我发现共享构建食谱来构建docker镜像比构建图像的长依赖链要容易得多,因为后者会使重建更加困难。
Michael Le BarbierGrünewald'17

13

您在Dockerfile中创建的每条指令都会创建一个新的图像层。每层都带来了附加数据,这些数据并不总是图像的一部分。例如,如果您在一个层中添加文件,但随后又在另一层中将其删除,则最终图像的大小将以特殊的“变白”文件的形式包括所添加的文件大小,尽管您将其删除了。

假设您有以下Dockerfile:

FROM centos:6

RUN yum -y update 
RUN yum -y install epel-release

生成的图像大小将为

bigimage     latest        3c5cbfbb4116        2 minutes ago    407MB

相反,对于“相似的” Dockerfile:

FROM centos:6

RUN yum -y update  && yum -y install epel-release

生成的图像大小将为

smallimage     latest        7edeafc01ffe        3 minutes ago    384MB

如果您在单个RUN语句中清理yum缓存,则大小将变得更小。

因此,您想在可读性/易于维护和层数/图像大小之间保持平衡。


4

这些RUN语句代表每一层。想象一下,有人下载了一个软件包,然后安装并想要删除它。如果使用三个RUN语句,则图像大小将不会缩小,因为存在单独的图层。如果使用一条RUN语句运行所有命令,则可以减小磁盘映像的大小。

By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.