介绍:云原生时代软件的构建和部署离不开容器技术。提到容器,几乎每个人都会下意识地想到Docker。Docker中有两个非常重要的概念,一个是Image(镜像),一个是Container(容器)。前者是一个静态视图,封装了应用程序的目录结构和运行环境;后者是动态视图(进程),显示程序的运行状态(cpu、内存、存储)等信息。下面几篇文章主要分享如何编写技巧,让Dockerfile构建过程更快,构建镜像更小。大家好,我是陈泽峰,在云霄负责Flow流水线编排和任务调度引擎相关工作。在云霄的产品体系下,我们服务过各种研发规模和技术深度的企业用户,得到了大量的用户反馈。对于使用Flow在云端构建的用户来说,构建速度是大家关心的一个关键因素。在深入分析用户案例的过程中,我们发现了很多共性问题。我们只需要修改和优化自己的项目或项目配置即可。可以大大提高构建的性能,从而进一步加快CICD的效率。今天我们就以构建容器镜像为切入点,总结一些在实际工程中非常实用的优化技巧。云原生时代软件的构建和部署离不开容器技术。提到容器,几乎每个人都会下意识地想到Docker。Docker中有两个非常重要的概念,一个是Image(镜像),一个是Container(容器)。前者是一个静态视图,封装了应用程序的目录结构和运行环境;后者是动态视图(进程),显示程序的运行状态(cpu、内存、存储)等信息。下面几篇文章主要分享如何编写技巧,让Dockerfile构建过程更快,构建镜像更小。镜像定义首先我们看一下Docker镜像,由多个只读层堆叠而成,每一层都是对上一层的增量修改。当基于图像创建新容器时,会在基础层之上添加一个新的可写层。这一层通常被称为“容器层”。下图是创建容器时基于docker.io/centos基础镜像构建的应用镜像的视图。从图中我们可以看到镜像构建和容器启动的过程。首先是拉取基础镜像docker.io/centos;启动一个基于docker.io/centos的容器,运行命令yumupdate然后执行dockercommit提交一个新的只读层v1(可以理解为生成一个新的临时镜像A,但是用户不会直接引用对它);基于临时镜像A启动一个新的容器,运行http服务器等软件的安装配置,提交一个新的只读层v2,这里最终生成开发者引用的镜像版本B;基于镜像版本B运行的容器会增加一层读写层(对容器的文件创建、修改、删除等操作都在该层生效);image的sourceimage主要是Docker通过读取并运行Dockerfile的指令生成的。以官网的Dockerfile为例:FROMubuntu:18.04COPY。/appRUNmake/appCMDpython/app/app.py其核心逻辑是定义引用的baseimage基础镜像,执行COPY命令将文件从context复制到容器中,运行RUN执行自定义构建脚本,最后定义容器启动的CMD或ENTRYPOINT。构建更高效的图像也应该围绕上述涉及的概念进行优化。Dockerfile优化技术采用国内基础镜像Flow作为云上产品。每一次构建都会为用户提供一个全新的构建环境,避免环境污染带来的过高运维成本。因此,Flow每次构建时都会重新下载Dockerfile中指定的基础镜像。如果Dockerfile中指定的baseimage来自DockerHub,可能会因为网络延迟导致下载缓慢,例如:FromNginxFromjava:8FROMopenjdk:8-jdk-alpine典型现象如下:Youcandumpyourownbase镜像文件去国内镜像仓库,修改自己的Dockerfile。操作步骤如下:将海外镜像拉取到本地。dockerpullopenjdk:8-jdk-alpine;将基础镜像推送到阿里云镜像仓库(cr.console.aliyun.com)的国内区域(如北京、上海等)。dockertagopenjdk:8-jdk-alpineregistry.cn-beijing.aliyuncs.com/yournamespace/openjdk:8-jdk-alpinedockerpushregistry.cn-beijing.aliyuncs.com/yournamespace/openjdk:8-jdk-alpi;在你的dockerfile中修改FROM,从你自己的镜像仓库下载镜像。来自registry.cn-beijing.aliyuncs.com/yournamespace/openjdk:8-jdk-alpine;基础镜像尽量小,大镜像够用,不仅占用磁盘空间大,而且在应用部署时占用的网络消耗也大,导致服务启动时间变长。使用较小的基础镜像,例如alpine作为基础镜像。这里我们看一个打包好的mysql-client二进制镜像,根据alpine和ubuntu的镜像大小对比。FROMalpine:3.14RUNapk添加--no-cachemysql-clientENTRYPOINT["mysql"]FROMubuntu:20.04RUNapt-getupdate\&&apt-getinstall-y--no-install-recommendsmysql-client\&&rm-rf/var/lib/apt/lists/*ENTRYPOINT["mysql"]由此可见,使用尽可能小的基础镜像有利于大大减小镜像的体积。减少与上下文相关的目录文件。docker是c/s架构设计。用户在执行dockerbuild时,并不是直接在客户端进行build,而是将build指定的目录作为context传递给server,然后执行上述imagebuild的过程。如果在构建镜像的上下文中关联了大量不需要的文件,可以使用.dockerignore忽略这些文件(类似于.gitignore,定义的文件不会被跟踪和传输)。这是官方网站上的示例。从buildlog可以看到context的大小只有几十个字节:mkdirmyproject&&cdmyprojectecho"hello">helloecho-e"FROMbusybox\nCOPY//\nRUNcat/hello">Dockerfiledockerbuild-thelloapp:v1--progress=plain.#7[internal]loadbuildcontext#7sha256:6b998f8faef17a6686d03380d6b9a60a4b5abca988ea7ea8341adfae112ebaec#7传输上下文:26Bdone#0.0projectwhenweputaprogramhasnothingtodowithmydone#7done发现一个大文件(或者不相关的小文件,比如应用构建的依赖包等),重建helloapp:v3时,发现有70MB的内容需要传输到服务器,图像大小达到71MB。#5[internal]loadbuildcontext#5sha256:746b8f3c5fdd5aa11b2e2dad6636627b0ed8d710fe07470735ae58682825811f#5transferringcontext:70.20MB1.0sdone#5DONE1.1s如果层数和控制镜像构建的脚本大小减少,层数和控制层ash的大小在简单的执行过程中,往往镜像层过多,镜像层包含无用文件的坑。下面分别看看这三个dockerfile的写法和构建的镜像大小。第一个是centos_git_nginx:normal镜像,它在centos基础镜像的基础上增加了两层,分别安装了git和nginx的两个二进制文件。你可以看到图像的大小约为402MB。FROMcentosRUNyuminstall-ygitRUNyuminstall-ynginx然后我们优化dockerfile,改成如下只加一层的方式。可以看到镜像的大小缩小到了384MB,证明了层数的减少可以减小镜像的大小。FROMcentosRUNyuminstall-ygit&&yuminstall-ynginx由于yuminstall过程会产生一些缓存数据,这些缓存数据在应用运行过程中是不需要的,所以我们在安装软件后立即删除,观察镜像又缩小了到357MB。FROMcentosRUNyuminstall-ygit&&\yuminstall-ynginx&&\yumcleanall&&rm-rf/var/cache/yum/*TIPS:我们知道镜像构建过程生成的每一层都是只读层并且可以不再修改,下面的写法并没有起到缩小图片大小的作用,只是增加了一个无用的图片层。FROMcentosRUNyuminstall-ygit&&\yuminstall-ynginxRUNyumcleanall&&rm-rf/var/cache/yum/*需要注意的是,追求太少的层级不一定是好的做法,这会使在拉取镜像时降低构建或层被缓存的概率。常量层放在前面,变量层放在后面。当我们同时多次执行dockerbuild时,可以发现再次构建镜像后,docker会使用缓存中的镜??像数据直接复用。.实际上,Docker会单步执行Dockerfile中的指令,并按照指定的顺序执行每条指令。检查每条指令时,Docker会在其缓存中查找可重复使用的现有图像。Docker从缓存中已有的父图像开始,并将下一条指令与从该基础图像派生的所有子图像进行比较,以查看是否有任何子图像是使用完全相同的指令构建的。否则,缓存将失效。比如我们可以把简单的、经常依赖的基础软件,如git、make等不常变化但常用的指令放在执行前,这样构建镜像的过程层就可以直接使用之前生成的缓存,而不是直接使用之前生成的缓存。反复下载软件会浪费带宽和时间。这里我们比较一下这两种写法。首先初始化相关目录和文件:mkdirmyproject&&cdmyprojectecho"hello">hellodockerfile的第一种写法是先COPY文件,然后执行RUN安装软件操作。FROMubuntu:18.04COPY/hello/RUNapt-getupdate--fix-missing&&apt-getinstall-y\aufs-tools\automake\build-essential\curl\dpkg-sig\libcap-dev\libsqlite3-dev\mercurial\reprepro\ruby??1.9.1\&&rm-rf/var/lib/apt/lists/*通过时间构建镜像dockerbuild-tcache_test-fDockerfile。如果构建成功并多次执行,可以发现后续构建直接命中了缓存镜像。timedockerbuild-tcache_test-fDockerfile.[+]Building59.8s(8/8)FINISHED=>[internal]从Dockerfile加载构建定义0.0s=>=>transferringdockerfile:35B0.0s=>[internal]load.dockerignore0.0s=>=>传输上下文:2B0.0s=>[内部]为docker.io/library/ubuntu:18.04加载元数据0.0s=>[内部]加载构建上下文0.0s=>=>传输上下文:26B0.0s=>[1/3]FROMdocker.io/library/ubuntu:18.040.0s=>缓存[2/3]COPY/hello/0.0s=>[3/3]RUNapt-getupdate&&apt-getinstall-yaufs-toolsautomakebuild-essentialcurldpkg-sig&&rm-rf/var/lib/apt/lists/*58.3s=>导出到图像1.3s=>=>导出图层1.3s=>=>写入图像sha256:5922b062e65455c75a74c94273ab6cb855f3730c6e458ef911b8ba2ddd1ede180.0s=>=>命名为docker.io/library/cache_test0.0sdockerbuild-tcache_test-fDockerfile。0.33s用户0.31s系统1%cpu1:00.37totaltimedockerbuild-tcache_test-fDockerfile.dockerbuild-tcache_test-fDockerfile。0.12suser0.08ssystem34%cpu0.558total修改hello文件内容,echo"world">>hello,执行timedockerbuild-tcache_test-fDockerfile。耗时回到1分钟左右。dockerfile的第二种写法如下。我们把基本不变的基础软件安装在上面,把可能变化的hello文件放在下面。从ubuntu:18.04RUNapt-getupdate&&apt-getinstall-y\aufs-tools\automake\build-essential\curl\dpkg-sig\&&rm-rf/var/lib/apt/lists/*COPY/hello/通过时间dockerbuild-tcache_test-fDockerfile.的镜像构建,第一次构建耗时1分钟左右(构建成功后多次执行命中缓存生成镜像)。修改hello文件内容,date>>hello,执行timedockerbuild-tcache_test-fDockerfile。再次。此时镜像构建耗时不到1秒,即成功复用了第二层构建的缓存层。使用多阶段分离构建和运行这里以golang为例,首先将示例代码库https://github.com/golang/exa...克隆到本地,添加dockerfile构建应用镜像。来自戈朗:1.17.6ADD。/go/src/github.com/golang/exampleWORKDIR/go/src/github.com/golang/exampleRUNgobuild-o/go/src/github.com/golang/example/hello/go/src/github.com/golang/example/hello/hello.goENTRYPOINT["/go/src/github.com/golang/example/hello"]可以看到图片大小为943MB,程序正常输出Hello,Goexamples!接下来,让我们使用多阶段构建和尽可能小的运行时来优化上述过程。从golang:1.17.6作为BUILDERADD。/go/src/github.com/golang/exampleRUNgobuild-o/go/src/github.com/golang/example/hello/go/src/github.com/golang/example/hello/hello.go从golang:1.17.6-alpineWORKDIR/go/src/github.com/golang/exampleCOPY--from=BUILDER/go/src/github.com/golang/example/hello/go/src/github.com/golang/example/helloENTRYPOINT["/go/src/github.com/golang/example/hello"]可以看到当前图片大小只有317MB。通过多阶段构建,将应用构建和运行时依赖分离,只有运行时依赖的软件才会最终放入应用镜像中。原文链接本文为阿里云原创内容,未经许可不得转载。
