7

优化 docker 镜像的几种方法

 3 years ago
source link: https://www.techug.com/post/several-methods-of-optimizing-docker-image.html
Go to the source link to view the article. You can view the picture content, updated content and better typesetting reading experience. If the link is broken, please click the button below to view the snapshot at that time.
neoserver,ios ssh client

优化 docker 镜像的几种方法

2

Devops 和 k8s 的火热,越来越多的企业将 docker 运用到自动化运维中,不管是为了保证开发、测试、生产环境的环境一致性,还是和 CI/CD 工具的集成度,比如 jenkins 对 docker 或 k8s 的自动构建部署等,亦或利用 docker 进行自动化测试等

那么,在现在这种随随便便一天动辄几十次的快速构建迭代中,镜像作为一个贯穿整个自动化过程中的一个关键,怎么保证自动化构建部署的效率?就是镜像尽可能的小

要保证镜像尽可能小,可以从五个方面

  • 基础镜像小
  • 层级尽量少
  • 去除不必要
  • 复用镜像层
  • 分阶段构建

基础镜像小

基础镜像小,主要是保证镜像层底层或者说 From 的镜像本身小

每个企业或个人使用容器,都是应对不同的业务场景,没有完全一致的业务场景,所以你最好不要直接用别人的第三方镜像,除非你了解该镜像的所有层级内容,而且从安全角度考虑,也尽量使用官方镜像,它没有太多第三方的,你不需要的东西,你可以在此基础上增加你的业务部分内容

选择 Alpine 镜像代替 Ubuntu、CentOS、Debian 等镜像

虽然 Alpine 没有其他系统完备的库、依赖,但是基本的应用它都是支持的,都可以通过 apk 去安装(apk 包管理) ,而且官方现在也推荐用 Alpine,很多开源项目都有基于 Alpine 的官方镜像

但如果你的项目涉及到编译,比如 python 等涉及编译的项目,要注意,Alpine 用的是 musl,因为它原本是用作嵌入式系统的,所以并没有 glibc 那么完整的 C 标准库

另外如果你要在 Alpine 中跑一些脚本的话,那你要注意一些 shell 和 linux 下的还是有所区别的,Alpine 是基于 busybox 的,同样也是设计于嵌入式的,所以很多 shell 命令做了裁剪,并不具备 Ubuntu、CentOS、Debian 等系统中那么完整的功能

除了使用小镜像之外,可以使用空镜像 scratch,自己手动添加 rootfs 来构建镜像,这个不建议新手,因为你可能不知道你需要些啥,折腾半天反而比官方镜像还要大

层级尽量少

前面文章有介绍 docker 的联合文件系统”Docker挂了,数据如何找回“,Dockerfile 构建镜像流程大致如下:

  • docker 从基础镜像运行一个容器
  • 执行一条指令对容器进行修改
  • 执行类似 docker commit 的操作提交这次修改
  • docker 再基于刚提交的镜像运行一个新容器
  • 执行 Dockerfile 中的下一条指令,依次循环,直到命令执行完成

所以每执行一条 Dockerfile 中的指令,就会提交一次修改,这次修改会保存成一个只读层挂载到联合文件系统,看过上面的文章应该知道,上面层的文件,如果和下面层有冲突或不同,会覆盖隐藏底层的文件,所以每增加一层,镜像大小就会增加,虽然在 docker1.10 后只有 RUN、COPY、ADD 指令会创建层,其他指令会创建临时的中间镜像,不会直接增加构建的镜像大小

所以在编写 Dockerfile 时,我们可以根据实际情况去合并一些指令,比如我们在编译安装 nginx 时,解压、编译、安装以及删除源文件的指令可以放在一起,以减少最终的镜像层

去除不必要

前面提到的用空镜像,或者裁剪过的小镜像来做基础镜像,其实就是一种去除不必要的依赖、库的一种形式

除了以上的这种形式,还有必要去除的,就是 Dockerfile 构建过程中或手动构建后 commit 的过程中所产生的临时文件

比如源码包、编译过程中产生的日志文件、添加的包管理仓库、包管理缓存,以及构建过程中安装的一些当时又用过后没用的软件或工具

如果可以,甚至建议不在容器中进行编译,如果二进制 binary 文件可以执行的话,在本地编译后,将 binary 文件 copy 到容器内

除了上面的,还有一些不常更新的文件,比如 web 静态资源文件 css、js 以及图片、视频等资源,建议存储 OSS 或共享存储系统 nfs、mfs 等,这些文件不应该打包到镜像里面,而应该通过 OSS 调用或通过共享存储挂载

对于不需要 build 进镜像的资源,可以使用.dockerignore 文件进行指定要忽略的文件或目录

当然,如果你想基于别人的镜像来做优化的话,可以通过 docker history 命令来查看镜像的层级关系,做相应的优化,更好的工具推荐 dive

当然也可以用自动化的镜像瘦身工具 docker-slim,它支持静态分析和动态分析,静态分析主要是通过分析镜像历史信息,获取生成镜像的 dockerfile 文件及相关的配置信息,而动态分析主要是通过 ptrace、pevent、fanotify 解析出镜像中必要的文件和文件依赖,将对应文件组织成新镜像来减小镜像体积

另外还可以通过 docker-squash 来压缩镜像层级,但是要考虑实际情况,并不是压缩一定是好的

复用镜像层

接上面为什么压缩不一定是好,压缩的原理是将镜像导出,然后删除所有中间层,将镜像的当前状态保存为单一层,达到压缩层级的效果

当你使用单一镜像或者少量镜像的时候可能没有太大问题,但是这样完全破坏了镜像的层级缓存功能

还是之前的文章中提的关于 docker 的存储的,docker 镜像的每个层级会存一个 hash 计算后的目录,那么 Dockerfile 构建过程中怎么利用缓存?

在镜像的构建过程中,Docker 根据 Dockerfile 指定的顺序执行每个指令。在执行每条指令之前,Docker 都会在缓存中查找是否已经存在可重用的镜像,如果有就使用现存的镜像,不再重复创建

而如果压缩为单一的层之后,缓存就失效了,不会命中缓存的层级,所以每次构建或者 pull 的时候,都是整个镜像构建或 pull

缓存命中除了和分层有关系,还和指令执行编排顺序有关系,首先看下缓存匹配遵循的基本规则:

  • 从一个基础镜像开始(FROM 指令指定),下一条指令将和该基础镜像的所有子镜像进行匹配,检查这些子镜像被创建时使用的指令是否和被检查的指令完全一样。如果不是,则缓存失效
  • 在大多数情况下,只需要简单地对比 Dockerfile 中的指令和子镜像。然而,有些指令需要更多的检查和解释
  • 对于 ADD 和 COPY 指令,镜像中对应文件的内容也会被检查,每个文件都会计算出一个校验值。这些文件的修改时间和最后访问时间不会被纳入校验的范围。在缓存的查找过程中,会将这些校验和和已存在镜像中的文件校验值进行对比。如果文件有任何改变,比如内容和元数据,则缓存失效
  • 除了 ADD 和 COPY 指令,缓存匹配过程不会查看临时容器中的文件来决定缓存是否匹配。例如,当执行完 RUN apt-get -y update 指令后,容器中一些文件被更新,但 Docker 不会检查这些文件。这种情况下,只有指令字符串本身被用来匹配缓存
  • 一旦缓存失效,所有后续的 Dockerfile 指令都将产生新的镜像,缓存不会被使用

所以为什么和指令执行顺序和编排有关系,或者说我们在合并命令减少层级的时候不能一味的追求合并,而是需要合理的合并一些指令

举个例子,比如我们用同一个基础镜像,分别编译 nginx 和 php,那么 nginx 也需要 pcre 库依赖,php 也需要,那我们是不是可以提取共同的依赖用一条 RUN 指令去执行,而不是每次构建都执行

再或者最简单的,添加镜像仓库,安装基本的编译工具,比如 gcc、autoconf、make、zlib 等这些不常改动,但是常用的指令放在前面去执行,这样后面构建用到的所有镜像都不会再重新安装

这样合理的利用层级缓存,不管是在 jenkins 中自动构建镜像,还是 push 到远程仓库、亦或是在部署 pull 的时候,都能够利用缓存,从而节省传输带宽和时间

分阶段构建

最后一个更重要的是分阶段构建,或者多阶段构建,其实它也是一种减少分层或者去除不必要的一种方式,单独列出来,是觉得这个方式应该是推荐的一种方式,在 docker17.05 中开始支持

具体的多阶段构建,就是通过将构建过程分析,分成多个阶段来执行,后面的或者最终的构建可以使用前面构建的结果,而不需要所有的构建都包含到最终的镜像中

拿 nginx 构建来举个例子

上图是一个 nginx 的 Dockerfile,构建之后查看大小

然后通过多阶段构建改造 Dockerfile

再构建后查看镜像大小

只比基础镜像多了 1MB,之前的所有构建阶段 as build 在打包镜像的时候全部被抛弃,只最后 FROM 生成最终镜像

基于多阶段构建,google 推出了 distroless,更加轻量级,它只包含应用程序机器运行时依赖项,不包含程序管理器、shell 以及标准 liunx 发行版中可以找到的任何其他程序

这应该就是正是我们所需要的,而我们又不会剪裁,distroless 帮我们做了这个工作

还是基于刚才的 nginx,我们不以 rhel7 为基本镜像了,我们以 distroless 为基本镜像构建

构建后查看大小

通过以上几种方式,可以有效精简 docker 镜像,从而提高自动化运维过程中的 CI/CD 效率,缩短交付时间

本文作者: InfoQ


About Joyk


Aggregate valuable and interesting links.
Joyk means Joy of geeK