减小镜像体积

<section class="section" id="docker__section_wyg_d43_25b"><h2 class="doc-tairway">背景信息</h2> <p class="p">Docker镜像由多个镜像层(Layers)组成(最多127层),在构建 Docker 容器时,应该尽量想办法构建体积更小的镜像。</p> <pre class="pre codeblock"><code>Small images are faster to pull over the network and faster to load into memory when starting containers or services. ——摘自Docker官方文档《Dockerfile的最佳实践》</code></pre> <p class="p"> 镜像大存在如下问题:</p> <ol class="ol" id="docker__ol_yhv_t54_dsb"> <li class="li">上传困难,很容易失败。</li> <li class="li">加速困难,下载缓慢,无法有效利用缓存,中断后无断点续传。</li> <li class="li">下载困难,分成多层Docker镜像,可以并行下载和只下载增量,小镜像部署更快。</li> </ol> <p class="p"> 精简Docker镜像大小有如下好处:</p> <ol class="ol" id="docker__ol_bly_w54_dsb"> <li class="li">减少了构建时间。</li> <li class="li">减少了磁盘使用量。</li> <li class="li">减少了下载时间。</li> <li class="li">因为包含文件少,攻击面减小,提高了安全性。</li> <li class="li">提高了部署速度。</li> </ol> <p class="p"> 可以通过<code class="ph codeph">docker history 298ec5e17509</code>命令查看镜像层数和每层的大小。 </p> <p class="p"> 要保证镜像尽可能小,可以从以下五个方面入手:</p> <ol class="ol" id="docker__ol_acv_x54_dsb"> <li class="li">基础镜像小</li> <li class="li">层级尽量少</li> <li class="li">清除不必要的文件</li> <li class="li">分阶段构建</li> <li class="li">小技巧</li> </ol> </section> <section class="section" id="docker__section_afy_l43_25b"><h2 class="doc-tairway">基础镜像小</h2> <p class="p">使用alpine或busybox镜像来减小镜像体积,以保证部署和扩容速度。</p> <p class="p">alpine是一个高度精简且包含了基本工具的轻量级Linux发行版,本身的Docker镜像只有4~5M大小,并且提供了非常好用的apk软件包管理工具,用来安装基础系统中不包含的程序。</p> <p class="p">各种开发语言和框架都有基于alpine制作的基础镜像,在开发自己的应用镜像时,选择这些镜像作为基础镜像,可以大大减小镜像的体积。 </p> <p class="p">各种语言对应的基础镜像如下:</p> <ul class="ul" id="docker__ul_cwk_543_25b"> <li class="li">Java(Spring Boot): - openjdk:8-jdk-alpine,openjdk:8-jre-alpine等</li> <li class="li">Java(Tomcat) - tomcat:8.5-alpine等</li> <li class="li"> Nodejs - node:9-alpine, node:8-alpine等</li> <li class="li">Python - python:3-alpine, python:2-alpine等</li> <li class="li">Go或可执行文件 - 直接基于alpine镜像,把编译后的可执行文件打入镜像。 因为alpine不同于普通的Ubuntu/Centos等发行版,需要静态编译和链接应用代码, 例如Go 需要关闭cgo: CGO_ENABLED=0 go build ...</li> </ul> </section> <section class="section" id="docker__section_wqq_m43_25b"><h2 class="doc-tairway">层级尽量少</h2> <p class="p">在单层镜像不大(不超过1G)的情况下,层级尽量少,docker build 实现 Dockerfile 到 Docker 镜像的构建。而对于单条 Dockerfile 中的命令(如命令RUN apt-get update ),则是通过针对 Docker 容器的 commit 操作,实现将其构建为单层镜像,镜像的层就像 Git 的提交(commit)一样,Docker 的层用于保存镜像的上一版本和当前版本之间的差异。可以使用<code class="ph codeph">docker commit</code>对正在运行的镜像或Dockerfile创建镜像,就像 Git 一样,当你向镜像仓库请求镜像时,只是下载你尚未下载的层。</p> <div class="p"> <img class="image" id="docker__image_rrx_1qv_dsb" width="800" src="https://obs-cn-shanghai.fincloud.pinganyun.com/pacloud/20221910111411-1d7ab5fd9d94.png"> </div> <p class="p">在Docker1.10后只有RUN、COPY、ADD指令会创建层(自动commit),其他指令会创建临时的中间镜像,不会直接增加构建的镜像大小。</p> <p class="p">常见Dockerfile如下:</p> <pre class="pre codeblock"><code># syntax=docker/dockerfile:1 FROM ubuntu:18.04 COPY . /app RUN make /app CMD python /app/app.py</code></pre> <p class="p">Dockerfile构建镜像流程大致如下:</p> <ol class="ol" id="docker__ol_mhd_z43_25b"> <li class="li">FROM:从基础镜像ubuntu:18.04创建一个镜像层。</li> <li class="li">COPY: 把你当前目录下的文件复制到容器中。</li> <li class="li">RUN: 运行 make命令构建你的程序。</li> <li class="li">CMD: 指定哪个命令在容器中运行(CMD不会创建新的分层)。</li> </ol> <p class="p">每执行一条Dockerfile中的RUN、COPY、ADD指令,就会提交一次修改,这次修改会创建一个新层,最终会保存成一个只读层挂载到联合文件系统。如果上面层的文件和下面层有冲突或不同,会覆盖隐藏底层的文件,所以每增加一层,镜像大小就会增加,可以通过docker history查看层数和每层大小。所以在编写Dockerfile时,可以根据实际情况去合并一些指令,以减少最终的镜像层。</p> <div class="p"> <img class="image" id="docker__image_t1s_bqv_dsb" width="800" src="https://obs-cn-shanghai.fincloud.pinganyun.com/pacloud/20221910111411-14728f9c915f.png"> </div> </section> <section class="section" id="docker__section_p33_n43_25b"><h2 class="doc-tairway">清除不必要的文件</h2> <pre class="pre codeblock"><code>Inadvertently including files that are not necessary for building an image results in a larger build context and larger image size. This can increase the time to build the image, time to pull and push it, and the container runtime size. ——来自Docker官方文档《 Dockerfile的最佳实践》</code></pre> <p class="p">构建镜像时包含不必要的文件会导致更大的构建上下文和更大的镜像。由此会增加构建镜像的时间、拉取和推送镜像的时间以及容器运行时的大小,比如源码包、编译过程中产生的日志文件、添加的包管理仓库、包管理缓存,以及构建过程中安装的一些当时使用过后没用的软件或工具,都是可以清除的。</p> <ul class="ul" id="docker__ul_ivn_np3_25b"> <li class="li">清除安装后的软件包缓存:<ul class="ul" id="docker__ul_c54_sp3_25b"> <li class="li">Alpine镜像:<code class="ph codeph">apk --update add php7 && rm -rf /var/cache/apk/*</code></li> <li class="li">Ubuntu或Debian系统镜像:<code class="ph codeph">apt install curl && rm -rf /var/lib/apt/lists/* </code></li> <li class="li">CentOS或者RHEL系统镜像:<code class="ph codeph">yum install curl && yum clean all </code></li> </ul><div class="p"> <img class="image" id="docker__image_bxv_dqv_dsb" src="https://obs-cn-shanghai.fincloud.pinganyun.com/pacloud/20221910111411-10a8fc1d9326.png"> </div></li> <li class="li">删除不必要依赖:<p class="p">删除不必要的依赖,不要安装调试工具。如果实在需要调试工具,可以在容器运行之后再安装。某些包管理工具(如 apt)除了安装用户指定的包之外,还会安装推荐的包,将会增加镜像的体积。</p><p class="p">apt 可以通过添加参数 --no-install-recommends 确保不会安装不需要的依赖项。如果确实需要某些依赖项,请在后面手动添加。</p><div class="p"> <img class="image" id="docker__image_uby_fqv_dsb" src="https://obs-cn-shanghai.fincloud.pinganyun.com/pacloud/20221910111411-19323ccc9f3a.png"> </div></li> <li class="li">运行环境不和编译环境混用:<p class="p">Docker内程序的编译环境和运行环境可以不一样,比如JAVA程序的编译使用jdk镜像,运行时使用jre镜像不包含 SDK,这么做可以大大减少镜像体积。发布运行时使用的镜像,建议不要在容器中进行编译,如果二进制binary文件可以执行的话,在本地编译后,将binary文件copy到容器内,尽量保证发布的运行时镜像尽可能小。</p></li> <li class="li">使用dockerignore:<p class="p">就像在git使用中.gitignore 一样,对于不需要build进镜像的资源,可以使用gitignore一样的语法,用.dockerignore文件指定要忽略的文件或目录。</p></li> <li class="li">只拷贝需要的文件:<p class="p">拷贝文件到镜像中时,尽量只拷贝需要的文件,切忌使用 COPY . 指令拷贝整个目录。</p><div class="p"> <img class="image" id="docker__image_y5q_gqv_dsb" src="https://obs-cn-shanghai.fincloud.pinganyun.com/pacloud/20221910111411-13de733b9327.png"> </div></li> <li class="li">VOLUME:<p class="p">VOLUME(数据卷)是Docker主机文件系统直接挂载到宿主机上的一个文件或目录。经常变动和需要持续读写的配置和文件,可以使用数据卷,直接存储到宿主机上,一方面可以提升读写速度,另一方面能保证数据持久化,容器被删除时数据不会丢失。合理使用数据卷等外部存储,不必将所有数据都打包到Docker镜像中,docker镜像只包含运行时。</p></li> </ul> </section> <section class="section" id="docker__section_fmz_n43_25b"><h2 class="doc-tairway">分阶段构建</h2> <p class="p">通常,应熟练使用多阶段构建来减小镜像大小,往往这也是最有效减小镜像的方法。分阶段构建在Docker17.05中开始支持,不过,需要注意,如果处理不当, 可能会造成构建的镜像无法运行。</p> <p class="p">多阶段构建的核心概念很简单:不要包括 C 或者 Go或者Java的编译器和整个构建辅助工具,仅仅想要可执行文件。</p> <p class="p">例如:C程序通过以下的Dockerfile文件构建镜像:</p> <pre class="pre codeblock"><code>FROM gcc COPY hello.c . RUN gcc -o hello hello.c CMD ["./hello"]</code></pre> <p class="p">可以发现一个简单的Hello World程序,最终构建出的镜像大小超过了1G,主要是由于使用了gcc基础镜像。</p> <p class="p">如果使用Ubuntu镜像,安装C编译器,然后编译程序,最终构建出镜像大小只有300MB,和第一次相比,减小了不少,但这对于一个实际只有 12KB 的hello二进制文件来说,仍然大的难以接受。</p> <pre class="pre codeblock"><code>#ls -l hello -rwxr-xr-x 1 root root 12556 6 15 11:35 hello </code></pre> <p class="p">使用分阶段构建,具体的Dockerfile文件如下:</p> <pre class="pre codeblock"><code>FROM gcc AS mybuildstage COPY hello.c . RUN gcc -o hello hello.c FROM ubuntu COPY --from=mybuildstage hello . CMD ["./hello"]</code></pre> <p class="p">使用gcc作为基础镜像编译hello.c程序,这一阶段为编译阶段mybuildstage。然后,开始定义新的阶段即执行阶段, 这个阶段使用ubuntu镜像,将上个阶段的构建产物hello可执行文件复制到指定目录中,最终构建出的镜像只有64MB,大小减少了大约95%:</p> <pre class="pre codeblock"><code># docker images c-hello REPOSITORY TAG IMAGE ID CREATED SIZE c-hello gcc.ubuntu d9492a009e98 23 minutes ago 73.9MB c-hello gcc db0206def0e6 27 minutes ago 1.19GB</code></pre> <p class="p">通过多阶段构建,极大地优化了最终镜像的大小。关于多阶段构建还有一些需要注意的点:</p> <ol class="ol" id="docker__ol_wjz_5w4_dsb"> <li class="li">在声明构建阶段时,可以不显示使用As关键字。后续阶段可以使用数字(以 0 开始)从前面的阶段复制文件。在复杂的构建中,显示定义名称便于后续的维护。<pre class="pre codeblock"><code>COPY --from=mybuildstage hello . COPY --from=0 hello .</code></pre></li> <li class="li">使用经典镜像:关于运行阶段的基础镜像选择,建议使用一些经典基础镜像, 如 Centos,Debian,Fedora,Ubuntu,Alpine镜像等,官方经典基础镜像可以节省大量的维护时间,因为官方镜像的所有安装步骤都使用了最佳实践。如果有多个项目,可以共享这些镜像层,因为他们都可以使用相同的基础镜像。</li> </ol> </section> <section class="section" id="docker__section_ccl_443_25b"><h2 class="doc-tairway">小技巧</h2> <ul class="ul" id="docker__ul_thq_yq3_25b"> <li class="li"> <p class="p">docker-slim:无需更改 Docker 容器映像中的任何内容,通过静态分析和动态分析,将其缩小 30 倍,更小更安全。</p> <p class="p">地址:https://github.com/docker-slim/docker-slim</p> </li> <li class="li"> <p class="p">dive:在学习Docker以及编写Dockerfile时,可以通过工具dive帮助分析镜像的结构,方便后续优化。</p> <p class="p">地址:https://github.com/wagoodman/dive </p> </li> </ul> <div class="p"> <img class="image" id="docker__image_slt_jqv_dsb" width="800" src="https://obs-cn-shanghai.fincloud.pinganyun.com/pacloud/20221910111411-1e5ed6b19367.png"> </div> </section> <section class="section" id="docker__section_tgd_cr3_25b"><h2 class="doc-tairway">参考文献</h2> <div class="p">Dockerfile的最佳实践:<ul class="ul" id="docker__ul_p2v_fpt_dvb"> <li class="li"> <p class="p">https://docs.docker.com/develop/dev-best-practices/</p> </li> <li class="li"> <p class="p">https://docs.docker.com/develop/develop-images/dockerfile_best-practices/</p> </li> </ul></div> </section>
以上内容是否解决了您的问题?
请补全提交信息!
联系我们

电话咨询

400-151-8800

邮件咨询

fincloud@ocft.com

在线客服

工单支持

解决云产品相关技术问题