容器映像_构建微小的容器映像

容器映像

几年前Docker爆炸时,它带动了容器和容器映像。 尽管Linux容器在此之前就已存在,但Docker使用户易于使用的命令行界面和使用Dockerfile格式的易于理解的方式来构建映像变得容易上手。 但是,尽管可能很容易进入,但仍有一些细微差别和技巧来构建可用的容器映像,甚至功能强大,但尺寸仍然很小。

第一遍:自己清理

其中一些示例涉及与传统服务器相同的清除方式,但更严格地遵循。 较小的映像大小对于快速移动映像至关重要,并且在磁盘上存储不必要数据的多个副本会浪费资源。 因此,与在具有大量专用存储的服务器上相比,应更经常地使用这些技术。

这种清理的一个示例是从映像中删除缓存的文件以恢复空间。 考虑一下由dnf在安装和未清除元数据和yum缓存的情况下安装dnf Nginx基本映像之间的大小差异:


   
   
# Dockerfile with cache
FROM fedora:28
LABEL maintainer Chris Collins <collins.christopher@gmail.com>

RUN dnf install -y nginx

-----

# Dockerfile w/o cache
FROM fedora:28
LABEL maintainer Chris Collins <collins.christopher@gmail.com>

RUN dnf install -y nginx \
        && dnf clean all \
        && rm -rf /var/cache/yum

-----

[chris@krang] $ docker build -t cache -f Dockerfile .  
[chris@krang] $ docker images --format "{{.Repository}}: {{.Size}}"
| head -n 1
cache: 464 MB

[chris@krang] $ docker build -t no-cache -f Dockerfile-wo-cache .
[chris@krang] $ docker images --format "{{.Repository}}: {{.Size}}"  | head -n 1
no-cache: 271 MB

那是大小上的显着差异。 带有dnf缓存的版本几乎是没有元数据和缓存的图像大小的两倍。 软件包管理器缓存,Ruby gem临时文件, nodejs缓存,甚至下载的源tarball都是清理的理想选择。

层次-潜在的陷阱

不幸的是(或者幸运的是,稍后将看到),基于各层与容器的工作方式,您不能简单地在Dockerfile中添加RUN rm -rf /var/cache/yum行并每天调用它。 Dockerfile的每条指令都存储在一个层中,并且层与层之间的更改应用在最上面。 因此,即使您要执行此操作:


   
   
RUN dnf install -y nginx
RUN dnf clean all
RUN rm -rf /var/cache/yum

...您仍将最终得到三层,其中一层包含所有缓存,还有两个中间层“从图像中删除”缓存。 但是缓存实际上仍然存在,就像将文件系统挂载到另一个之上一样,文件也在那里–您只是看不到或访问它们。

您会注意到,上一节中的示例将缓存清理链接在生成缓存的同一Dockerfile指令中:


   
   
RUN dnf install -y nginx \
        && dnf clean all \
        && rm -rf /var/cache/yum

这是一条指令,最终成为图像内的单个层。 这样,您将丢失一点Docker(* ahem *)缓存,从而使映像的重建时间稍长一些,但是缓存的数据不会最终出现在最终映像中。 作为一个不错的折衷方案,仅链接相关命令(例如yum installyum clean all ,或下载,提取和删除源tarball等)可以节省大量最终图像大小,同时仍然允许您利用Docker缓存可加快开发速度。

但是,这一层“陷阱”比它最初出现的要微妙得多。 因为图像层记录了每一层的变化 ,一层又一层地记录,所以不仅存在文件的总和,而且文件的任何变化。 例如, 即使更改文件的模式也会在新层中创建该文件的副本。

例如,下面的docker images的输出显示有关两个映像的信息。 第一个layer_test_1是通过将单个1GB文件添加到基本CentOS映像而创建的。 第二个图像layer_test_2FROM layer_test_1创建的,除了使用chmod u+x更改了1GB文件的模式外,什么也没做。


   
   
layer_test_2        latest       e11b5e58e2fc           7 seconds ago           2.35 GB
layer_test_1        latest       6eca792a4ebe           2 minutes ago           1.27 GB

如您所见,新映像比第一个映像大1GB以上。 尽管layer_test_1只是前两层layer_test_2 ,仍有漂浮隐藏在第二图像内的额外的1GB的文件。 在映像构建过程中随时删除,移动或更改任何文件时,都是如此。

专用图像与灵活图像

轶事:随着我的办公室在Ruby on Rails应用程序投入大量资金,我们开始拥抱容器的使用。 我们要做的第一件事就是为所有团队创建一个正式的Ruby基本映像。 为了简单起见(在“这是我们在服务器上进行的方式”下的苦难),我们使用rbenv将最新的四个版本的Ruby安装到映像中,从而使我们的开发人员可以将所有应用程序迁移到一个容器中图片。 这产生了一个非常大但灵活的(我们认为)的图像,涵盖了我们所合作的各个团队的所有基础。

原来这是浪费的工作。 维护特定映像的单独的,稍作修改的版本所需的工作很容易实现自动化,并且在引入重大更改之前,选择具有特定版本的特定映像实际上有助于识别即将报废的应用程序,从而给下游造成严重破坏。 这也浪费了资源:当我们开始分割不同版本的Ruby时,我们最终得到了多个图像,这些图像共享一个基础,并且如果它们共存于服务器上,则仅占用很少的额外空间,但与之相比要小得多。安装了多个版本的巨型映像。

这并不是说构建灵活的映像无济于事,但是在这种情况下,从一个通用的基础上创建目标构建映像最终可以节省存储空间和维护时间,并且每个团队都可以修改其设置,但是他们需要在保持收益的同时进行修改普通基础图像

从头开始:将所需内容添加到空白图像

Dockerfile一样友好且易于使用,有可用的工具可提供灵活性,以创建非常小的Docker兼容的容器映像,而不会占用整个操作系统的负担,甚至包括与标准Docker基本映像一样小的映像。

我之前已经写过有关Buildah的文章 ,我将再次提及它,因为它足够灵活,可以使用主机上的工具从头开始创建映像,以安装打包的软件并处理该映像。 这样,这些工具就无需再包含在映像本身中。

Buildah取代了docker docker build命令。 有了它,您可以将容器映像的文件系统挂载到主机上,并使用主机上的工具与其进行交互。

让我们从上面的Nginx示例中尝试Buildah(暂时忽略缓存):


   
   
#!/usr/bin/env bash
set -o errexit

# Create a container
container =$ ( buildah from scratch )

# Mount the container filesystem
mountpoint =$ ( buildah mount $container )

# Install a basic filesystem and minimal set of packages, and nginx
dnf install --installroot $mountpoint   --releasever 28 glibc-minimal-langpack nginx --setopt install_weak_deps = false -y

# Save the container to an image
buildah commit --format docker $container nginx

# Cleanup
buildah unmount $container

# Push the image to the Docker daemon’s storage
buildah push nginx:latest docker-daemon:nginx:latest

您会注意到我们不再使用Dockerfile来构建映像,而是使用了一个简单的Bash脚本,并且我们从头开始(或空白)映像进行构建。 Bash脚本将容器的根文件系统安装到主机上的安装点,然后使用主机命令安装软件包。 这样,包管理器甚至不必存在于容器内。

无需额外的克鲁夫特,所有额外的基础图像中的东西,像dnf ,例如,在图像中只有304 MB,比上面一个Dockerfile内置Nginx的图像更小的100多MB的重量。


   
   
[ chris @ krang ] $ docker images | grep nginx
docker.io / nginx      buildah      2505d3597457     4 minutes ago         304 MB

注意:由于将映像推送到Docker守护程序的命名空间中的方式,映像名称docker.io附加了docker.io ,但它仍然是使用上述构建脚本在本地构建的映像。

如果您认为基本映像本身已经约为300 MB,那么这100 MB已经是一笔巨大的节省。 使用软件包管理器安装Nginx也会带来大量依赖。 对于使用主机上的工具从源代码编译的内容,节省的成本甚至更高,因为您可以选择确切的依赖项,而不必提取不需要的任何其他文件。

如果您想尝试这种方法, Tom Sweeney撰写了一篇更深入的文章,《 使用Buildah创建小型容器》 ,您应该查看一下。

使用Buildah来构建不具有完整操作系统和随附构建工具的映像时,所生成的映像要比您本来可以创建的映像小得多。 对于某些类型的图像,我们可以进一步采用这种方法,并包含应用程序本身来创建图像。

仅使用静态链接的二进制文件创建图像

遵循导致我们放弃管理并在图像内部构建工具的相同哲学,我们可以走得更远。 如果我们足够专业并且放弃生产容器内部故障排除的想法,我们是否需要Bash? 我们需要GNU核心实用程序吗? 我们真的需要基本的Linux文件系统吗? 您可以使用允许您使用静态链接库创建二进制文件的任何编译语言来执行此操作-将程序所需的所有库和函数复制并存储在二进制文件本身中。

这是Golang社区中一种相对流行的做事方式,因此我们将使用Go应用程序进行演示。

下面的Dockerfile需要一个小的Go Hello-World应用程序,并将其编译为FROM golang:1.8的映像:


   
   
FROM golang:1.8

ENV GOOS=linux
ENV appdir=/go/src/gohelloworld

COPY ./ /go/src/goHelloWorld
WORKDIR /go/src/goHelloWorld

RUN go get
RUN go build -o /goHelloWorld -a

CMD ["/goHelloWorld"]

包含二进制文件,源代码和基础图像层的结果图像的大小为716 MB。 但是,我们的应用程序实际上唯一需要的是已编译的二进制文件。 其他所有东西都是未使用的杂物,随我们的图像一起运送。

如果在编译时使用CGO_ENABLED=0禁用cgo ,则可以创建一个不包装C库的二进制文件来实现某些功能:

 GOOS=linux CGO_ENABLED=0 go build -a goHelloWorld.go 

可以将生成的二进制文件添加到空白或“临时”图像中:


   
   
FROM scratch
COPY goHelloWorld /
CMD ["/goHelloWorld"]

让我们比较一下两者之间图像大小的差异:


   
   
[ chris@krang ] $ docker images
REPOSITORY      TAG             IMAGE ID                CREATED                 SIZE
goHello     scratch     a5881650d6e9            13 seconds ago          1.55 MB
goHello     builder     980290a100db            14 seconds ago          716 MB

那是巨大的差异。 从golang:1.8生成的图像( golang:1.8带有goHelloWorld二进制文件)(上面标记为“ builder”)比仅包含二进制文件的临时图像大460倍。 带有二进制文件的整个暂存映像仅为1.55 MB。 这意味着,如果使用构建器映像,我们将运送大约713 MB的不必要数据。

如上所述,这种创建小图像的方法在Golang社区中经常使用,并且对此主题的博客文章也不少。 凯尔西·高塔Kelsey Hightower)撰写了一篇有关该主题的文章 ,其中涉及更多细节,包括处理与C库不同的依赖关系。

考虑壁球,如果它对你有用

除了将所有命令链接到层以节省空间外,还有另一种方法:压缩图像。 压缩图像时,实际上是在导出图像,删除所有中间层,并使用图像的当前状态保存一个图层。 这具有将图像缩小到小得多的优点。

压扁层以前需要一些创造性的变通办法来平整图像-导出容器的内容并将其重新导入为单层图像,或使用诸如docker-squash类的工具。 从版本1.13开始,Docker引入了一个方便的标志--squash ,以在构建过程中完成相同的任务:


   
   
FROM fedora:28
LABEL maintainer Chris Collins <collins.christopher@gmail.com>

RUN dnf install -y nginx
RUN dnf clean all
RUN rm -rf /var/cache/yum

[chris@krang] $ docker build -t squash -f Dockerfile-squash --squash .
[chris@krang] $ docker images --format "{{.Repository}}: {{.Size}}"  | head -n 1
squash: 271 MB

使用docker squash这种多层Dockerfile,我们最终与另一271MB的形象,因为我们与链接的指令例子一样。 这对于此用例非常有用,但是存在潜在的陷阱。

“什么? 另一个陷阱?”

好吧,就像以前一样,这以另一种方式引起了问题。

走得太远:太压扁,太小,太专业

图像可以共享图层。 基数可能是x兆字节,但是只需要拉/存储一次,每个图像都可以使用它。 所有图像共享层的有效大小是基础层加上每个特定更改的差异。 这样,成千上万个图像可能只比单个图像占用更多的图像。

这是压扁或过度专业化的缺点。 将图像压缩为单个图层时,您将失去与其他图像共享图层的机会。 每个图像最终都和其单层的总大小一样大。 如果仅使用几个图像并从中运行许多容器,这可能对您来说效果很好,但是如果您有许多不同的图像,从长远来看,它最终可能会浪费您的空间。

回顾一下Nginx壁球示例,我们可以看到在这种情况下这没什么大不了的。 最后,我们安装了Fedora,安装了Nginx,没有缓存,并且压扁就可以了。 Nginx本身并不是非常有用。 通常,您需要进行自定义才能做一些有趣的事情,例如配置文件,其他软件包,也许还有一些应用程序代码。 每一个最终都会在Dockerfile中成为更多指令。

使用传统的映像构建,您将拥有一个带有Fedora的基本映像层,一个装有Nginx的第二层(带有或不带有缓存),然后每个自定义项将是另一层。 使用Fedora和Nginx的其他图像可以共享这些层。

需要一张图片:


   
   
[   App 1 Layer (  5 MB) ]          [   App 2 Layer (6 MB) ]
[   Nginx Layer ( 21 MB) ] ------------------^
[ Fedora  Layer (249 MB) ]  

但是,如果您压缩图像,那么即使Fedora基础层也将被压缩。 任何基于Fedora的压缩图像都必须附带其自己的Fedora内容, 每个图像还要增加249 MB

 [ Fedora + Nginx + App 1 (275 MB)]      [ Fedora + Nginx + App 2 (276 MB) ]   

如果您构建许多高度专业化的超微型图像,这也会成为一个问题。

与生活中的一切一样,节制是关键。 同样,由于图层的工作原理,随着容器图像变得更小,更专业化,并且不再与其他相关图像共享基础图层,您会发现收益递减。

具有较小定制的图像可以共享基础层。 如上所述,该基础大小可能是x兆字节,但是只需将其拉/存储一次,每个图像都可以使用它。 所有图像的有效尺寸是基础层加上每个特定更改的差异。 这样,成千上万个图像可能只比单个图像占用更多的图像。


   
   
[ specific app   ]      [ specific app 2 ]
[ customizations ]--------------^
[ base layer     ]

如果图像缩小过大,并且变化或专业化太多,则最终可能会出现许多图像,这些图像都不共享基本层,并且所有磁盘都占用自己的空间。

  [ specific app 1 ]     [ specific app 2 ]      [ specific app 3 ] 

结论

有多种不同的方法来减少处理容器映像所花费的存储空间和带宽,但是最有效的方法是减小映像本身的大小。 无论您是简单地清理缓存(避免将它们孤立成中间层),还是将所有层压缩为一层,或者在空映像中仅添加静态二进制文件,都值得花一些时间来查看容器映像中可能存在膨胀的地方,以及将它们缩小到有效尺寸。

接下来要读什么

翻译自: https://opensource.com/article/18/7/building-container-images

容器映像

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值