当前位置: 首页 > 科技观察

五种方式:构建小巧Docker容器的学问

时间:2023-03-13 06:10:55 科技观察

五种方法:构建微型Docker容器的科学几年前,Docker的爆炸性发展将容器和容器镜像的概念带入了公众视野。尽管之前就存在Linux容器,但Docker凭借其用户友好的命令行界面和易于理解的Dockerfile格式,大大降低了构建镜像的门槛。但必须承认的是,虽然上手难度有所降低,但还是有一些细微的差别和技巧可以帮助我们构建功能强大但体积小的容器镜像。Level1:清理内容下面列出的部分示例采用了类似于传统服务器的清理方式,但具体要求更为严格。镜像的大小对于快速移动至关重要,在磁盘上存储不必要的数据副本无疑会浪费大量资源。因此,我们有必要用技术尽可能地控制容器镜像的“本体”。让我们看看如何从图像中删除缓存文件以节省存储空间。先用dnf安装有无元数据的Nginx,查看两者的镜像大小差异;然后使用yum清理缓存:#DockerfilewithcacheFROMfedora:28LABELmaintainerChrisCollinsRUNdnfinstall-ynginx-----#Dockerfilew/ocacheFROMfedora:28LABELmaintainerChrisCollinsRUNdnfinstall-ynginx\&&dnfcleanall\&&rm-rf/var/cache/yum-----[chris@krang]$dockerbuild-tcache-fDockerfile。[chris@krang]$dockerimages--格式"{{.Repository}}:{{.Size}}"|head-n1cache:464MB[chris@krang]$dockerbuild-tno-cache-fDockerfile-wo-cache.[chris@krang]$dockerimages--format"{{.Repository}}:{{.Size}}"|head-n1no-cache:271MB可以看出两者在体积上存在明显差异。带有dnf缓存的版本几乎是没有元数据和缓存的图像大小的两倍。事实上,工具包管理器缓存、Rubygem临时文件、nodejs缓存,甚至下载的源代码tarball都是清理的主要对象。分层——一个潜在的问题不幸的是(或者幸运的是,如下所述),因为容器是以分层方式使用的,所以你不能简单地将RUNrm-rf/var/cache/yum添加到Dockerfile中然后放手。Dockerfile中的每条指令都存储在一个层中,层之间的更改最终会应用到顶层。因此,即使您执行类似以下操作:RUNdnfinstall-ynginxRUNdnfcleanallRUNrm-rf/var/cache/yum...您仍然会得到三层,一个包含所有缓存,两个中间层从图像中“删除”缓存.然而,缓存在物理上仍然存在,就像当你将一个文件系统挂载在另一个文件系统之上时,文件就在那里——我们只是看不到或访问它们。请注意,上一节中的示例将缓存清理链接到与缓存相同的Dockerfile指令:RUNdnfinstall-ynginx\&&dnfcleanall\&&rm-rf/var/cache/yum这是一个单独的指令,最终将成为层。通过这种方式,您将丢弃一些Docker缓存——这意味着映像重建将花费更长的时间,但缓存的数据仍将存在于最终映像中。作为一个很好的妥协,我们只需要链接相关命令(例如huminstall和humcleanall,或者下载、提取和删除源tarball等)来帮助最终镜像显着瘦身,同时继续使用Docker缓存来加快发展。然而,这里的层次会比上一篇文章中提到的更加微妙。因为镜像的每一层都记录了每一层的具体变化——所以除了添加的文件之外,所有的文件修改都会被包含进来。例如,即使更改了文件模式,图像中也会有新的层来创建该文件的副本。例如,以下dockerimages输出显示与两组图像相关的信息。第一个layer_test_1是通过将一个1GB的文件添加到基本CentOS映像而创建的。第二组镜像layer_test_2是直接从layer_test_1创建的,但是1GB文件的模式是通过chmodu+x命令改变的。layer_test_2lateste11b5e58e2fc7secondsago2.35GBlayer_test_1latest6eca792a4ebe2minutesago1.27GB如您所见,新图像比之前的图像大1GB以上。尽管layer_test_1实际上只代表layer_test_2的前两层,但第二组图像中还隐藏了另一个1GB的文件。在镜像构建过程中,所有与文件相关的删除、移除或更改都会导致此结果。关于专用镜像与灵活镜像的轶事:当我们大量采用RubyonRails应用程序时,同事们慢慢开始接受容器的新颖性。我们的第一项工作是为所有团队创建一个官方的Ruby基础镜像。为简单起见,我们使用rebenv将四个最新的Ruby版本安装到镜像中,允许我们的开发人员使用单个版本将所有应用程序迁移到容器镜像中。这实际上导致了一个非常大但灵活(至少我们认为)的镜像集,涵盖了我们跨团队工作的所有基础知识。但事实证明,这都是浪费时间。维护特定图像的单个修改版本可以相对容易地自动化,因为为特定图像选择特定版本实际上可以帮助在引入破坏性更改之前意识到原始应用程序不再适合后续需求,从而避免结果,浩劫接踵而至。此外,过大的图像是一种资源浪费:当我们拆分不同的Ruby版本时,我们最终得到多组图像共享同一个基础。如果同时保存在服务器上,与包含多个版本的巨幅图像相比,并不会占用太多额外空间,但传输速度要快得多。这并不是说构建灵活性镜像没有意义。仅在我们的案例中,创建专用映像最终节省了存储空间和维护时间,同时还确保团队可以在享受收益的同时对公共基础映像进行必要的更改。从头开始:将您需要的内容添加到空白图像操作系统——其他大小甚至可以与标准的Docker基础镜像相媲美。之前写过Buildah,这里再提一下,因为它相当灵活,利用大型机中的工具从头开始制作镜像,安装打包软件,修改镜像内容。更重要的是,这些工具将始终存在于图像之外,因此它们不会增加图像本身的大小。Buildah取代了dockerbuild命令。通过它,你可以将容器镜像的文件系统挂载到宿主机上,并使用宿主机中的工具与之交互。我们试试用上面的Nginx例子看看Biuldah的效果(这里暂时忽略缓存):#!/usr/bin/envbashset-oerrexit#创建容器container=$(buildahfromscratch)#挂载容器文件系统mountpoint=$(buildahmount$container)#安装基本文件系统和最小包集,以及nginxdnfinstall--installroot$mountpoint--releasever28glibc-minimal-langpacknginx--setoptinstall_weak_deps=false-y#将容器保存为镜像buildahcommit--formatdocker$containernginx#清理buildahunmount$container#将镜像推送到Dockerdaemon进行存储buildahpushnginx:latestdocker-daemon:nginx:latest大家可能已经注意到,这里我们不再使用Dockerfile来构建镜像,而是使用一个简单的Bash脚本。我们从一组草图(或空白)图像构建。这个Bash脚本会将容器的根文件系统挂载到主机上的一个挂载点,然后使用主机命令安装每个包。这样,包管理器甚至不需要超出容器本身的范围。如果没有额外的部分——比如基础镜像中的额外内容,比如dnf——镜像本身的大小只有304MB,比之前使用Dockerfile构建的Nginx镜像小了100多MB。[chris@krang]$dockerimages|grepnginxdocker.io/nginxbuildah2505d35974574minutesago304MB注意:docker.io部分包含在镜像名称中,因为它被推送到Dockerdaemon的命名空间,但它仍然是本地使用上面构建脚本构建的镜像。考虑到基本映像本身只有300MB左右,节省100MB显然是相当可观的。使用软件管理器安装Nginx也会带来很多依赖。如果您使用主机提供的工具进行源码编译,您将能够进一步节省存储空间,因为您可以选择确切的依赖项而不是引入任何不必要的额外文件。使用Buildah构建镜像可以有效摆脱完整的操作系统和构建工具,从而进一步减小你的镜像体积。对于一些特定类型的镜像,我们也可以使用相同的方法创建只包含应用程序本身的镜像。仅使用静态链接的二进制文件创建镜像遵循相同的理念,我们可以进一步从镜像中清理管理和构建工具。如果我们拥有必要的专业知识并且不需要站在容器内进行故障排除,我们是否可以取消Bash?我们还需要GNU内核吗?我们还需要底层的Linux文件系统吗?您可以使用任何编译语言通过从静态链接库创建二进制文件来做到这一点——程序运行所需的所有库和函数都被复制并存储在二进制文件中。这是一种在Golang社区中颇受欢迎的做事方式,因此我们在这里使用Go应用程序对其进行演示。以下是一个使用小型GoHello-World应用程序并将其编译为FROMgolang:1.8映像的Dockerfile:FROMgolang:1.8ENVGOOS=linuxENVappdir=/go/src/gohelloworldCOPY.//go/src/goHelloWorldWORKDIR/go/src/goHelloWorldRUNgogetRUNgobuild-o/goHelloWorld-aCMD["/goHelloWorld"]生成的镜像包含二进制文件、源代码和基础镜像层,总体积716MB。然而,最终我们的应用程序真正需要的只是编译后的二进制文件,其他一切都是多余的。如果我们在统计时使用CGO_ENABLED=0来禁用cgo,我们可以创建一组不打包C库的二进制文件:GOOS=linuxCGO_ENABLED=0gobuild-agoHelloWorld.go生成的二进制文件可以添加到空的,或者"buildfromscratch"在镜像中:FROMscratchCOPYgoHelloWorld/CMD["/goHelloWorld"]接下来我们比较一下两组镜像的体积差异:[chris@krang]$dockerimagesREPOSITORYTAGIMAGEIDCREATEDSIZEgoHelloscratcha5881650d6e913secondssago1.55MBgoHellobuilder980290a100db716MB14差异巨大你可以看到。golang:1.8构建的镜像包含goHelloWorld库(标记为'builder'),比纯二进制镜像大460倍。纯二进制图像的大小仅为1.55MB。这意味着如果我们使用构建器构建的镜像,其中将有大约713MB的数据根本不存在。如果适用,考虑压缩方法另一种可以通过将所有命令链接到层中来节省空间的方法是图像压缩(压缩)。在进行图像压缩时,您实际上是在导出图像,移除所有中间层,并将图像的当前状态保存为单个层。这将有效地控制图像的实际大小。过去,我们需要利用一些创造性的解决方案来恢复压缩层——例如导出容器内容并将其作为单层图像重新导入,或者使用docker-squash等工具。但从1.13版本开始,Docker引入了一个方便的标志——squash,它可以在构建过程中做同样的事情:FROMfedora:28LABELmaintainerChrisCollinsRUNdnfinstall-ynginxRUNdnfcleanallRUNrm-rf/var/cache/yum[chris@krang]$dockerbuild-tsquash-fDockerfile-squash--squash.[chris@krang]$dockerimages--format"{{.Repository}}:{{.Size}}"|head-n1squash:271MB使用dockersquash来处理这个多层Dockerfile,我们最终得到了一个271MB的图像,其功能与之前的链接指令图像相同。然而,这也带来了新的潜在问题。太极端:过度压缩,过于“瘦”,过度专注于图像之间的图层共享。它的base可能是xMB,但是只需要拉/存一次,其他镜像就可以使用了。用于层共享的每个图像的实际大小是基础层加上某些变化的方差。这样,我们就可以以非常低的额外空间投资交换基于同一图像的数千个修改后的图像。而这正是图像压缩或专业化方法带来的弊端。当将图像压缩成单层形式时,我们完全失去了与其他图像共享图层的机会。每组镜像最终将与其单层的体积一致。因此,如果您只需要少量图像并在其中运行大量容器,那么过度压缩就可以了;但如果您要处理许多不同的图像,从长远来看,它最终会耗尽您的存储空间。让我们重新审视Nginx压缩示例,看看“瘦身”过程在这种情况下不是问题。我们最终安装了Fedora和Nginx,清除了缓存,并进行了有效压缩。然而,Nginx本身并不是很有用,你通常需要以自定义的方式进行各种有针对性的操作——比如配置文件、其他包甚至一些应用程序代码。这些操作中的每一个都会向Dockerfile添加更多指令。如果你以传统方式构建镜像,你将有一个单独的基础镜像层在镜像中托管Fedora,一个安装了Nginx的层(有或没有缓存),然后每个定制都有自己的层。其他镜像包括Fedora、Nginx等将能够共享这些层。在这种情况下,所需的图像将是:[App1Layer(5MB)][App2Layer(6MB)][NginxLayer(21MB)]--------------------^[FedoraLayer(249MB)]但是如果你压缩镜像,Fedorabaselayer也会被压缩。基于Fedora的压缩镜像需要发布相关的Fedora内容,也就是说每套镜像会增加249MB![Fedora+Nginx+App1(275MB)][Fedora+Nginx+App2(276MB)]如果构建大量的Dedicated和超小镜像,那么这绝对会造成很大的麻烦。因为就像生活中的其他一切一样,适度是镜子音量控制的关键。并且考虑到镜像层的工作原理,随着容器镜像的压缩性和特异性逐渐增加,将无法与其他相关镜像共享基础镜像层,压缩带来的瘦身效果会减弱甚至消失.具有一定程度自定义的图像可以共享基础层。如前所述,这个基础层可以是xMB,但只需要做一次拉取/存储,所有图像都可以使用它。所有图像的有效尺寸是基础层加上每个特定变化的方差。这样,我们就可以以非常低的额外空间投资交换基于同一图像的数千个修改后的图像。[specificapp][specificapp2][customizations]------------^[baselayer]但是如果你的图片压缩得太厉害或者有太多的修改或自定义调整,那么我们将不得不面对大量的镜像。由于这些图像没有相同的共享基础层集,因此它们各自占用磁盘上的存储空间。[specificapp1][specificapp2][specificapp3]总结我们有多种处理方法可以有效降低容器镜像所需的存储空间和传输带宽,但最有效的方法无疑是降低镜像本身的大小。无论您选择简单地清理其中的缓存(避免将其保留在中间层),将所有层压缩为一个层,或者只是将静态二进制文件添加到一个空图像中,都值得花一些时间研究其中可能存在的内容图片。不必要的内容并将其缩小到合理的大小。原标题:Buildingtinycontainerimages,作者:ChrisCollins