镜像优化
背景说明
我们的应用服务使用镜像的方式打包,镜像太大会降低传输速度、占用额外资源。
因此笔者在项目中实际落地尝试了镜像优化的各种方案,目的是
- 加快构建速度
- 降低镜像大小
- 去除无用资源
笔者在实际交付过程中,使用本优化方案,项目镜像大小平均都减少了50%,构建速度也加快了不少。
原理说明
知道docker的一些原理,我们就知道应该怎么优化了,知其然也知其所以然。
构建阶段
介绍
构建阶段,docker会把你指定的资源上传到docker服务器进行打包构建,如图。
Client客户端,用于和 docker服务端交互。
Docker Host是服务端,启动了服务,处理诸如 docker build 等类似的命令。
Registry是第三方镜像仓库,存储了大量的别人的镜像,可以拉取过来使用。
因此,我们docker build时,docker会把构建依赖的文件发送给 docker服务端。
假设我们的dockerfile为
FROM openjdk:17
COPY index.html app/index.html
构建命令为
docker build -f .dockerfile -t app:1.0 .
上下文为
我们发现当前文件夹下,我们仅依赖dockerfile和index.html,有个dockerfile中没有用到的 “无用的文件”占用了300MB,如果这些都被打包发送给docker服务端,网络传输就要耗费很长时间,便会降低整体构建速度。
但是目前最新版本的docker一般都会采用 buildkit 的方式构建,如果使用了这种方式,即使我们指定了很多无用的内容,docker也只会把需要的内容发送给服务端。因此这部分我们就不过多赘述了。记得使用最新的构建方式即可。(官方文档提到,BuildKit 是 Docker Desktop 和 Docker Engine v23.0 及更高版本用户的默认构建器。如果你是低版本的,参考官方文档配置吧,笔者就不过多赘述了。)
结论
开启BuildKit即可
构建原理
介绍
docker是分层构建的,如图。
也就是,一条命令会构建一层,底层是不可变、只可读的。【如图,即使仅仅修改了index.html的文件名,也会重新创建一层来存储这个内容】
扩展阅读:
为什么要采用分层呢?
答:因为这样会提高复用率、提高构建速度、天生具有版本(可回滚)、节省存储空间、快速分发。
解释一下。
假设你们有100个项目,都基于centos作为基本镜像。
如果分层的话,基本镜像只需要在服务器存在一份即可,如果不分层直接修改原本镜像,那么要重复100份centos。
并且假设其中有20个Java项目,依赖在centos上依赖jdk,那么所有的java项目都可以复用这2层,
只是各自服务的jar包不一致,因此之需要从jar包开始构建即可,最下面的2层不需要构建,提高了构建速度。
结论
- 每一层都
少增加/删除内容,可以一行搞定的就一行搞定,减少每一层的多余文件。 - 基本镜像小的话,整体当然也会小。
优化实战
优化编译+构建时间
-
使用新的构建器提升构建速度【上文提到的buildkit】
-
利用缓存,经常变动的文件放在dockerfile的下层。
根据上文提到的,基本上父层镜像不变,本层镜像不变,则能多用上缓存,因此我们要把 不经常变动的放在dockerfile的上面,经常要变动的放在下面。例如:
| 修改前 | 修改后 |
|---|---|
| 构建时间:1m40s | 构建时间:43s【减少了57%】(因为代码变动更多,依赖变动少) |
此处需要注意,构建时复用缓存存在一些条件:
1. COPY/ADD命令:比较 文件内容 是否和上次一样。【这种一般没有问题】
1. 如:COPY a.txt a.txt
2. 其他命令:仅比较 当前层 命令字符串是否和上次一样。【这种可能导致异常】
1. 如:RUN wget http://www.baidu.com/a.txt【若命令和上次一样,则不会重新拉取内容,不会判断http://www.baidu.com/a.txt的内容是否有更改】
-
缓存构建过程中下载第三方依赖包
-
maven构建时会拉取的jar包(spring的包),python的numpy依赖包等,很多基本的包是不会变动的。如:spring包等
【注意,目前现有的云上构建 或 成熟的公司内部使用的,一般都会有此功能,开启即可,无需手动缓存】
【自己想做一个编译机器的话,才需要此操作】
以maven为例,把构建过程中拉取的包,缓存到主机上。下一次就会加速 RUN --mount=type=cache,target=/root/.m2 mvn clean package -
使用yum install、apt-get命令时,提前切换为国内的镜像源【具体国内源请自行搜索,笔者不再赘述】
-
优化镜像大小
- 减少基本镜像的大小
选择合适的基本镜像,以Java为例子,一般来说,只是运行的话,选择jre足够,最多选个jdk方便排查问题,如果有更多的依赖,再考虑选择 操作系统 + jdk(如ubuntu + jdk)。
- 使用多阶段构建,减少中间层的大小
| 不使用两阶段构建 | 使用两阶段构建 |
|---|---|
| 700MB | 340MB |
- 合并命令,清除无用缓存
| 不好的 | 好的 |
|---|---|
| 使用2条RUN命令,docker构建时会产生2层,父层的缓存文件并没有被实际删除,还保留在旧层里 | 直接在一条命令里做完了删除操作,不会保存到最终的镜像内容中 |
每一种操作系统的缓存文件,可以自行搜索如何删除,此处不过多赘述。
工具
- docker history
Docker history可以查看每层镜像的大小,针对性分析。【如图】
docker history eclipse-temurin:21
深入分析每一层文件的新增、修改、删除的情况。【如图】
docker run --rm -it \
-v /var/run/docker.sock:/var/run/docker.sock \
wagoodman/dive:latest eclipse-temurin:21
左边是层、右边是新增、修改、删除的内容。