docker镜像体积优化攻略参考—— 筑梦之路

简单介绍

镜像的本质是镜像层和运行配置文件组成的压缩包 ,构建镜像是通过运行 Dockerfile 中的 RUNCOPYADD 等指令生成镜像层和配置文件的过程。

和镜像体积大小有关的关键点:

  • RUNCOPYADD 指令会在已有镜像层的基础上创建一个新的镜像层,执行指令产生的所有文件系统变更会在指令结束后作为一个镜像层整体提交。

  • • 镜像层具有 copy-on-write 的特性,如果去更新其他镜像层中已存在的文件 ,会先将其复制到新的镜像层中再修改,造成双倍的文件空间占用

  • • 如果去删除其他镜像层的一个文件 ,只会在当前镜像层生成一个该文件的删除标记,并不会减少整个镜像的实际体积

验证上面的结论过程如下:

bash 复制代码
cat Dockerfile

FROM alpine:latest
COPY resource.tar /
RUN touch /resource.tar
RUN rm -f /resource.tar
ENTRYPOINT ["/bin/ash"]

# 构建镜像,并查看层

$ docker build -t test-image -f Dockerfile .
$ docker history test-image:latest
IMAGE          CREATED              CREATED BY                                      SIZE      COMMENT
95f1695b2904   About a minute ago   /bin/sh -c #(nop)  ENTRYPOINT ["/bin/ash"]      0B
1780448c656f   About a minute ago   /bin/sh -c rm -f /resource.tar                  0B
a85d29bf7738   About a minute ago   /bin/sh -c touch /resource.tar                  135MB
6dac335fa653   4 minutes ago        /bin/sh -c #(nop) COPY file:66065d6e23e0bc52...   135MB
e66264b98777   7 weeks ago          /bin/sh -c #(nop)  CMD ["/bin/sh"]              0B
<missing>      7 weeks ago          /bin/sh -c #(nop) ADD file:8e81116368669ed3d...   5.53MB

docker history 的输出结果中可以看到:

  • RUN touch /resource.tar 指令只是修改了文件的元信息,但依然将整个文件拷贝到了新的镜像层中。

  • RUN rm -f /resource.tar 指令虽然删除了文件,并且该文件在运行容器时不可见,但依然在前两个镜像层中以及最终的镜像中存在。

常用分析工具

1. docker history

docker 自带的 docker history 命令,该命令可以展示所有镜像层的创建时间、指令以及体积等较为基础的信息,但对于复杂的镜像则有些乏力。

2. dive

第三方的 dive 工具,该工具可以分析镜像层组成,并列出每个镜像层所包含的文件列表,可以很方便地定位到影响镜像体积的构建指令以及具体文件

如何优化

1. 分阶段构建与从零构建

分阶段构建 (multi-stage builds)和从零构建(build from scratch)是优化镜像体积的基本手段和必备技巧。该技巧将镜像构建过程区分为构建和运行环境,在构建环境安装编译器等依赖并编译所需的二进制包,然后将其复制到仅包含必要运行依赖的运行环境中。

对 golang 这类能够编译静态二进制文件的语言来说分阶段构建的效果尤为明显,我们可以将编译产生的二进制文件放到 scratch 镜像中运行(scratch 是一个特殊的空镜像)

bash 复制代码
FROM golang
COPY hell0.go .
ENV CGO_ENABLED=0
RUN go build hello.go

FROM scratch
COPY --from=0 /go/hello .
CMD ["./hello"]

如果直接使用 golang 镜像作为运行环境,其镜像体积通常接近 1 个 G,其中大部分文件都不是在运行容器时所必要的。将编译结果拷贝到运行环境后,体积只有几十 kb~mb 不等,如果需要在运行容器中保留基本的系统工具,可以考虑使用 alpine 镜像作为运行环境。

关于分阶段构建和从零构建的更多细节可参考 Docker 官方文档中的 Use multi-stage builds 和 Create a simple parent image using scratch。

2. 避免产生无用的文档或缓存

docker 镜像不应该包含文档、缓存等对运行容器没有作用的内容。

  1. 避免在本地保留安装缓存。 大部分包管理器会在安装时缓存下载的资源以备之后使用,以 pip 为例,会将下载的响应和构建的中间文件保存在 ~/.cache/pip 目录,应使用 --no-cache-dir 选项禁用默认的缓存行为。

  2. 避免安装文档。 部分包管理器提供了选项可以不安装附带的文档,如 dnf 可使用 --nodocs 选项。

  3. 避免缓存包索引 。部分包管理器在执行安装之前,会尝试查询所有已启用仓库的包列表、版本等元信息缓存在本地作为索引。个别仓库的索引缓存可达到 150 M 以上。我们应该仅在安装包时查询索引,并在安装完成后清理,不应该在单独的指令中执行 yum makecache 这类缓存索引的命令

  1. 及时清理不需要的文件

运行容器时不需要的文件,一定要在创建的同一层清理,否则依然会保留在最终的镜像中。

通过包管理安装包,通常会产生大量的缓存文件,一定要在同一 RUN 指令的结尾处立刻清理。在安装依赖数量较多时,可以节省大量的缓存空间。

bash 复制代码
# dnf
RUN dnf install -y --nodocs <PACKAGES> \
  && dnf clean all \
  && rm -rf /var/cache/dnf

# apt
RUN apt-get update \
  && apt-get install -y <PACKAGES> \
  && rm -rf /var/lib/apt/lists/*
# 官方的 ubuntu/debian 镜像 apt-get 会在安装后自动执行 clean 命令

3. 合并多个镜像层

应该避免在不同镜像层中更新文件而造成额外的体积占用。当构建的层数很多且执行指令较复杂时,很难避免在不同的镜像层中更新文件,可通过以下手段精简这部分额外体积

1) 在最终生成镜像时将所有镜像层合并成一层,在 docker build 命令中使用 ---squash 即可实现(需要开启 docker daemon 的实验性功能)

docker build -t squash-image --squash -f Dockerfile .

最终生成的镜像只有一个镜像层,包含最后实际存在的文件系统,在合并所有镜像层的过程中,相当于禁用了 copy-on-write 特性。这种做法的坏处 在于,镜像在保存和分发时是可以复用镜像层的,推送镜像时会跳过镜像仓库已存在的镜像层,拉取镜像时会跳过本地已拉取过的镜像层,而合并成一层后则失去了这种优势

  1. 分阶段构建,将部分中间镜像层压缩成一层作为基础镜像。 在开发团队内部,我们往往会在官方镜像的基础上添加或更新部分依赖,然后作为团队内部统一使用的基础镜像,这种复用方式可以大大减少实际占用的镜像体积。更进一步,我们可以将这类基础镜像压缩成一层
bash 复制代码
FROM golang:1.16 as base

FROM scratch
COPY --from=base / /
ENTRYPOINT ["/bin/bash"]

压缩成一层后,golang:1.16 的镜像体积从 919MB 变成 913MB,官方镜像已经做了很多优化所以节省空间十分有限,但对于开发团队内部制作的基础镜像,这种优化往往会带来意外惊喜

4. 复制文件的同时修改元信息

先将文件添加到镜像内,然后再修改文件的执行权限和所属用户,这类 COPY-RUN 指令在 Dockerfile 中十分常见:

bash 复制代码
COPY output/hello /usr/bin/hello
RUN chmod +x /usr/bin/hello && chown normal:normal /usr/bin/hello

但修改文件元信息也会将文件复制到新的镜像层 ,以上指令会产生两份相同的文件。在文件体积较大时,会显著增加整个镜像的体积。事实上,我们可以在复制文件的同时完成对文件元信息的修改,COPYADD 指令都提供了修改元信息的 --chmod--chown 选项:

bash 复制代码
COPY --chmod=755 --chown=normal:normal output/hello /usr/bin/hello

--chmod 特性目前还未添加到官方文档,使用前需要开启 docker 的 buildkit 特性(在 docker build 命令前添加 DOCKER_BUILDKIT=1 即可),目前只支持 --chmod=755--chmod=0755 这种设置方法,不支持 --chmod=+x

注:经测试,当使用 ADD 指令且源文件为下载链接时 --chmod 选项不起作用,不清楚这是 docker 的 bug 还是 feature。解决方案是直接使用 RUN 指令 wget + chmod 来替代 ADD

本文搜集来自镜像体积从1000M到10M,几招就做到,仅供参考。

相关推荐
耶啵奶膘1 小时前
uniapp-是否删除
linux·前端·uni-app
_.Switch2 小时前
高级Python自动化运维:容器安全与网络策略的深度解析
运维·网络·python·安全·自动化·devops
2401_850410832 小时前
文件系统和日志管理
linux·运维·服务器
JokerSZ.2 小时前
【基于LSM的ELF文件安全模块设计】参考
运维·网络·安全
XMYX-03 小时前
使用 SSH 蜜罐提升安全性和记录攻击活动
linux·ssh
芯盾时代3 小时前
数字身份发展趋势前瞻:身份韧性与安全
运维·安全·网络安全·密码学·信息与通信
心灵彼岸-诗和远方4 小时前
DevOps业务价值流:架构设计最佳实践
运维·产品经理·devops
一只哒布刘4 小时前
NFS服务器
运维·服务器
南猿北者4 小时前
docker容器
docker·容器
苹果醋34 小时前
Java8->Java19的初步探索
java·运维·spring boot·mysql·nginx