作者|徐伟
起源|尔达 Erda 公众号

简介

容器镜像相似于虚拟机镜像,封装了程序的运行环境,保障了运行环境的一致性,使得咱们能够一次创立任意场景部署运行。镜像构建的形式有两种,一种是通过 docker build 执行 Dockerfile 里的指令来构建镜像,另一种是通过 docker commit 将存在的容器打包成镜像,通常咱们都是应用第一种形式来构建容器镜像。

在构建 docker 容器时,咱们个别心愿尽量减小镜像,以便放慢镜像的散发;然而不失当的镜像构建形式,很容易导致镜像过大,造成带宽和磁盘资源节约,尤其是遇到 daemonset 这种须要在每台机器上拉取镜像的服务,会造成大量资源节约;而且镜像过大还会影响服务的启动速度,尤其是解决紧急线上镜像变更时,间接影响变更的速度。如果不是刻意管制镜像大小、留神镜像瘦身,个别的业务零碎中可能 90% 以上的大镜像都存在镜像空间节约的景象(不信能够尝试检测看看)。因而咱们十分有必要理解镜像瘦身办法,减小容器镜像。

如何判断镜像是否须要瘦身

通常,咱们可能都是在容器镜像过大,显著影响到镜像上传/拉取速度时,才会思考到剖析镜像,尝试镜像瘦身。此时采纳的多是 docker image history 等 docker 自带的镜像剖析命令,以查看镜像构建历史、镜像大小在各层的散布等。而后依据教训判断是否存在空间节约,然而这种判断形式终点较高、没有量化,不不便自动化判断。以后,社区中也有很多镜像剖析工具,其中比拟风行的 dive 剖析工具,就能够量化给出_容器镜像有效率_、_镜像空间节约率_等指标,如下图:

采纳 dive 对一个 mysql 镜像进行效率剖析,发现镜像有效率只有 41%,镜像空间节约率高达 59%,显然须要瘦身。

如何进行镜像瘦身

当判断一个镜像须要瘦身后,咱们就须要晓得如何进行镜像瘦身,上面将联合具体案例解说一些典型的镜像瘦身办法。

多阶段构建

所谓多阶段构建,实际上是容许在一个 Dockerfile 中呈现多个 FROM 指令。最初生成的镜像,以最初一条 FROM 构建阶段为准,之前的 FROM 构建阶段会被摈弃。通过多阶段构建,后一个阶段的构建过程能够间接利用前一阶段的构建缓存,无效升高镜像大小。一个典型的场景是将编译环境和运行环境拆散,以一个 go 我的项目镜像构建过程为例:

# Go语言编译环境根底镜像FROM golang:1.16-alpine# 拷贝源码到镜像COPY server.go /build/# 指定工作目录WORKDIR /build# 编译镜像时,运行 go build 编译生成 server 程序RUN CGO_ENABLED=0 GOOS=linux GOARCH=amd64 GOARM=6 go build -ldflags '-w -s' -o server# 指定容器运行时入口程序ENTRYPOINT ["/build/server"]

这种传统的构建形式有以下毛病:

  • 根底镜像为反对编译环境,蕴含大量go语言的工具/库,而运行时并不需要
  • COPY 源码,减少了镜像分层,同时有源码透露危险

采纳多阶段构建形式,能够将上述传统的构建形式批改如下:

## 1 编译构建阶段#  Go语言编译环境根底镜像FROM golang:1.16-alpine AS build# 拷贝源码到镜像COPY server.go /build/# 指定工作目录WORKDIR /build# 编译镜像时,运行 go build 编译生成 server 程序RUN CGO_ENABLED=0 GOOS=linux GOARCH=amd64 GOARM=6 go build -ldflags '-w -s' -o server## 2 运行构建阶段#  采纳更小的运行时根底镜像FROM scratch# 从编译阶段仅拷贝所需的编译后果到以后镜像中COPY --from=build /build/server /build/server# 指定容器运行时入口程序ENTRYPOINT ["/build/server"]

能够看到,应用多阶段构建,能够获取如下益处:

  • 最终镜像只关怀运行时,采纳了更小的根底镜像。
  • 间接拷贝上一个编译阶段的编译后果,缩小了镜像分层,还防止了源码透露。

缩小镜像分层

镜像的层就像 Git 的提交(commit)一样,用于保留镜像的以后版本与上一版本之间的差别,然而镜像层会占用空间,领有的层越多,最终的镜像就越大。在构建镜像时,RUN, ADD, COPY 指令对应的层会减少镜像大小,其余命令并不会减少最终的镜像大小。上面以理论工作中的一个案例解说如何缩小镜像分层,以减小镜像大小。

背景

测试项目 mysql 镜像时,遇到了容器创立比较慢的状况,咱们发现次要是因为容器镜像较大,拉取镜像工夫较长,所以就打算看看 mysql 镜像为什么这么大,是否能够减小容器镜像。

镜像大小剖析

通过 docker image history 查看镜像构建历史及各层大小。

镜像大小:2.9GB

其相应 Dockerfile 如下:

#### MySQL 5.7##FROM centos:7...RUN yum -y install crontabsRUN groupadd -g ${MY_GID} -r ${MY_GROUP} && \    adduser ${MY_USER} -u ${MY_UID} -M -s /sbin/nologin -g ${MY_GROUP}# RUN wget https://dev.mysql.com/get/Downloads/MySQL-5.7/mysql-5.7.29-1.el7.x86_64.rpm-bundle.tarCOPY mysql-5.7.29-1.el7.x86_64.rpm-bundle.tar  /RUN tar -vxf /mysql-5.7.29-1.el7.x86_64.rpm-bundle.tarRUN rm /mysql-5.7.29-1.el7.x86_64.rpm-bundle.tarRUN yum clean allRUN yum -y install libaioRUN yum -y install numactlRUN yum -y install net-toolsRUN yum -y install perl# RUN rpm -e --nodeps mariadb-libs-1:5.5.52-1.el7.x86_64RUN rpm -ivh mysql-community-common-5.7.29-1.el7.x86_64.rpmRUN rpm -ivh mysql-community-libs-5.7.29-1.el7.x86_64.rpmRUN rpm -ivh mysql-community-client-5.7.29-1.el7.x86_64.rpmRUN rpm -ivh mysql-community-server-5.7.29-1.el7.x86_64.rpmRUN rm -rf mysql-community-*RUN yum clean all#### Entrypoint##ENTRYPOINT ["/bin/bash","/docker-entrypoint.sh"]

能够发现:Dockerfile 中存在过多扩散的 RUN/COPY 指令,而且还是大文件相干操作,导致了过多的镜像分层,使得镜像过大,能够尝试合并相干指令,以减小镜像分层。

合并 RUN 指令

该 Dockerfile 中 RUN 指令较多,能够将 RUN 指令合并到同一层:

RUN yum -y install crontabs && \    mv /tmp/dumb-init_1.2.5_x86_64 /usr/bin/dumb-init && \    chmod +x /usr/bin/dumb-init && \    groupadd -g ${MY_GID} -r ${MY_GROUP} && \    adduser ${MY_USER} -u ${MY_UID} -M -s /sbin/nologin -g ${MY_GROUP} && \    tar -vxf /tmp/mysql-5.7.29-1.el7.x86_64.rpm-bundle.tar && \    yum clean all && \    yum -y install libaio numactl net-tools perl && \    rpm -ivh mysql-community-common-5.7.29-1.el7.x86_64.rpm && \    rpm -ivh mysql-community-libs-5.7.29-1.el7.x86_64.rpm && \    rpm -ivh mysql-community-client-5.7.29-1.el7.x86_64.rpm && \    rpm -ivh mysql-community-server-5.7.29-1.el7.x86_64.rpm && \    rm -rf mysql-community-* && \    rm -rf /tmp/mysql-5.7.29-1.el7.x86_64.rpm-bundle.tar

编译后镜像大小显著降落:

镜像大小:1.92GB

COPY 指令转换合并到 RUN 指令

从上图中能够看到,一个较大的镜像层是 COPY 指令导致的,拷贝的文件较大,所以咱们思考将 COPY 指令转换合并到 RUN 指令;具体做法是将文件上传到 oss,在 RUN 指令中下载。当然也能够发现之前还有一个 RUN 指令漏掉没有合并,须要持续合并到已有 RUN 指令中。

RUN curl -o /tmp/mysql-5.7.29-1.el7.x86_64.rpm-bundle.tar http://xxx.oss.aliyuncs.com/addon-pkgs/mysql-5.7.29-1.el7.x86_64.rpm-bundle.tar && \    tar -vxf /tmp/mysql-5.7.29-1.el7.x86_64.rpm-bundle.tar && \    ...

编译后镜像大小显著降落:

镜像大小: 1.27GB

留神:此处次要是因为 COPY 指令操作的相干文件较大,对应层占用空间较多,才会将 COPY 指令转换合并到RUN 指令;如果其对应层占用空间较小,则只需别离合并 COPY 指令、RUN 指令,会更加清晰,而没必要将两者转换合并到一层。

缩小容器中不必要的包

还是以上述 mysql 镜像为例,咱们发现下载的包 mysql-5.7.29-1.el7.x86_64.rpm-bundle.tar 蕴含如下 rpm 包:


而装置所需的 rmp 包只有:


删除不必要的包,用最新的最小 rpm 压缩包替换 mysql-5.7.29-1.el7.x86_64.rpm-bundle.tar 后从新编译镜像:

镜像大小: 1.19GB

镜像剖析工具

后面咱们通过 docker 自带的 docker image history 命令剖析镜像,本节次要解说镜像剖析工具 dive 的应用,其次要特色如下:

  • 按层显示 Docker 镜像内容
  • 指出每一层的变动
  • 评估 “镜像的效率”,节约的空间
  • 疾速的构建/剖析周期
  • 和 CI 集成,不便自动化检测镜像效率是否合格

镜像效率剖析

之前是通过 docker image history 剖析镜像体积散布,并进行镜像瘦身,此处将采纳 dive 剖析镜像有效率。

应用办法:dive <image_name>

优化前

原始镜像有效率: 41%,大部分镜像体积都是节约的。

如下:

优化后

优化后镜像有效率:97%

留神:优化后,镜像分层显著缩小,镜像有效率显著进步;然而此时的镜像效率晋升次要是依附缩小节约空间获取的,如果要持续优化镜像体积,须要联合镜像体积瓶颈点评估下一步优化方向。一个通常的持续优化点是:减小根底镜像体积和不必要的包。

如下所示:

番外篇:如何通过镜像复原 Dockerfile

后面次要通过镜像剖析工具剖析镜像体积散布,发现节约空间,优化镜像大小。镜像剖析工具的另一个典型利用场景是:当只有容器镜像时如何通过镜像复原 Dockerfile?

镜像构建历史查看

个别,咱们能够通过 docker image history 查看镜像构建历史、镜像层及对应的构建指令,从而还原出对应Dockerfile。

留神:docker image history 查看对应的构建命令可能显示不全,须要带上 --no-trunc 选项。

这种办法有如下缺点:

  • 一些指令信息提取不残缺、不易读,如 COPY/ADD 指令,对应的操作文件用 id 示意,如下图所示。

  • 对于一些镜像层,不是通过 Dockerfile 指令构建进去的,而是间接通过批改容器内容,而后 docker commit 生成,不不便查看该层变更的文件。

借助 dive 剖析工具还原

借助 dive 剖析工具还原 Dockerfile,次要是因为 dive 能够指出每一层的变动,如下:

  • 能够依据 COPY 层变动内容(右侧),直观判断拷贝的文件。
  • 因为能够查看每一层的变动,所以对于 docker commit 也更容易剖析相干操作对应的变动范畴。


思考

镜像变胖的起因

镜像变胖的起因很多,如:

  • 无用文件,比方编译过程中的依赖文件对编译或运行无关的指令被引入到镜像
  • 零碎镜像冗余文件多
  • 各种日志文件,缓存文件
  • 反复编译两头文件
  • 反复拷贝资源文件
  • 运行无依赖文件

然而个别状况是,用户可能对大量的镜像空间节约不那么敏感;然而在操作大文件时,一些不当的指令(RUN/COPY/ADD)应用形式却很容易造成大量的空间节约,此时尤其要留神镜像剖析与镜像瘦身。

镜像瘦身难吗

对于根底镜像的减小、零碎包的减小,将镜像体积从 200M 减小到 190M 等可能绝对难些,此时须要对程序镜像十分相熟,并联合专门的剖析工具具体分析。然而个别场景下,镜像的节约很可能仅仅是因为镜像构建命令的应用姿态不佳。此时联合本文的镜像瘦身办法,和 Dockerfile 最佳实际,个别都能实现镜像瘦身。

如何评估瘦身成果(镜像效率)

如果能够评估镜像的空间应用效率,一方面能够比拟直观的判断哪些镜像空降节约重大,须要瘦身;另一方面也能够对瘦身的成果进行评估。上文介绍的,镜像剖析工具 dive 即可满足要求。

CI 集成

如果须要对大量镜像的体积应用效率进行把关,就必须将效率检测作为自动化流程的一环,而 dive 就比拟容易集成到 CI 中,只需执行如下指令:

CI=true dive <image-name>

优化前 mysql 镜像执行后果:由上文可知,优化前理论效率值为 41%,因为默认效率阈值为 90%,所以执行失败。


优化后镜像执行后果:效率值为 97%,因为默认效率阈值为 90%,所以执行通过。


同时我的项目也能够依据其对镜像大小的敏感度,将镜像大小最为一个检测条件,如只有镜像大小超过 1G 时,才进行镜像效率检测,这就能够防止大量小镜像的检测,放慢 CI 流程。

如何自动化的检测 Docerfile 并给出优化倡议呢

联合上文,ADD/COPY/RUN 指令对应层会减少最终镜像大小,而个别镜像的构建过程蕴含:文件筹备、文件操作等。文件筹备阶段在 ADD/COPY/RUN 指令中都有可能呈现;文件操作阶段次要由 RUN 指令实现,如果指令过于扩散,文件操作阶段会依据 写时复制 准则,拷贝一份到以后镜像层,造成空间节约,尤其是在波及大文件操作时。更重大的状况是,如果对文件的操作扩散在不同的 RUN 指令中,不就造成了屡次文件拷贝节约了。试想一下,如果拷贝和操作在同一层进行,不就能够防止这些文件跨层拷贝了吗。

所以有以下一些通用的优化检测办法和倡议:

  • 检测 RUN 指令是否过于扩散,倡议合并。
  • 检测 COPY/ADD 指令是否有拷贝大文件,且在 RUN 指令中有对文件进行操作,则倡议将 COPY/ADD 指令转换合并到 RUN 指令中。当然此种检测办法,仅仅只有 Dockerfile 还是不够的,还须要有上下文,能力检测相干文件的大小。

当然还有很多其余的检测方向和优化倡议,有待进一步欠缺,欢送增加小助手微信(Erda202106)进入交换群探讨!

参考

  • dive
  • Best practices for writing Dockerfiles

欢送参加开源

Erda 作为开源的一站式云原生 PaaS 平台,具备 DevOps、微服务观测治理、多云治理以及快数据治理等平台级能力。点击下方链接即可参加开源,和泛滥开发者一起探讨、交换,共建开源社区。欢送大家关注、奉献代码和 Star!

  • Erda Github 地址:https://github.com/erda-project/erda
  • Erda Cloud 官网:https://www.erda.cloud/