区区镜像优化,不是手到擒来?

184 阅读5分钟

镜像优化

背景说明

我们的应用服务使用镜像的方式打包,镜像太大会降低传输速度、占用额外资源。

因此笔者在项目中实际落地尝试了镜像优化的各种方案,目的是

  1. 加快构建速度
  2. 降低镜像大小
  3. 去除无用资源

笔者在实际交付过程中,使用本优化方案,项目镜像大小平均都减少了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 .

上下文为

我们发现当前文件夹下,我们仅依赖dockerfileindex.html,有个dockerfile中没有用到的 “无用的文件”占用了300MB,如果这些都被打包发送给docker服务端,网络传输就要耗费很长时间,便会降低整体构建速度。

但是目前最新版本的docker一般都会采用 buildkit 的方式构建,如果使用了这种方式,即使我们指定了很多无用的内容,docker也只会把需要的内容发送给服务端。因此这部分我们就不过多赘述了。记得使用最新的构建方式即可。(官方文档提到,BuildKit 是 Docker Desktop 和 Docker Engine v23.0 及更高版本用户的默认构建器。如果你是低版本的,参考官方文档配置吧,笔者就不过多赘述了。)

结论

开启BuildKit即可

构建原理

介绍

docker是分层构建的,如图。

image-20241216145404754.png

也就是,一条命令会构建一层,底层是不可变、只可读的。【如图,即使仅仅修改了index.html的文件名,也会重新创建一层来存储这个内容】

扩展阅读:
为什么要采用分层呢?
答:因为这样会提高复用率、提高构建速度、天生具有版本(可回滚)、节省存储空间、快速分发。
解释一下。
假设你们有100个项目,都基于centos作为基本镜像。
如果分层的话,基本镜像只需要在服务器存在一份即可,如果不分层直接修改原本镜像,那么要重复100份centos。
并且假设其中有20个Java项目,依赖在centos上依赖jdk,那么所有的java项目都可以复用这2层,
只是各自服务的jar包不一致,因此之需要从jar包开始构建即可,最下面的2层不需要构建,提高了构建速度。
结论
  1. 每一层都少增加/删除内容,可以一行搞定的就一行搞定,减少每一层的多余文件。
  2. 基本镜像小的话,整体当然也会小。

优化实战

优化编译+构建时间

  1. 使用新的构建器提升构建速度【上文提到的buildkit】

  2. 利用缓存,经常变动的文件放在dockerfile的下层。

    根据上文提到的,基本上父层镜像不变,本层镜像不变,则能多用上缓存,因此我们要把 不经常变动的放在dockerfile的上面,经常要变动的放在下面。例如:

修改前修改后
image-20241216164018168.pngimage-20241216164036515.png
构建时间: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的内容是否有更改】
  1. 缓存构建过程中下载第三方依赖包

    1. maven构建时会拉取的jar包(spring的包),python的numpy依赖包等,很多基本的包是不会变动的。如:spring包等

      【注意,目前现有的云上构建 或 成熟的公司内部使用的,一般都会有此功能,开启即可,无需手动缓存】

      【自己想做一个编译机器的话,才需要此操作】

      以maven为例,把构建过程中拉取的包,缓存到主机上。下一次就会加速
      RUN --mount=type=cache,target=/root/.m2 mvn clean package
      
    2. 使用yum install、apt-get命令时,提前切换为国内的镜像源【具体国内源请自行搜索,笔者不再赘述】

优化镜像大小

  1. 减少基本镜像的大小

选择合适的基本镜像,以Java为例子,一般来说,只是运行的话,选择jre足够,最多选个jdk方便排查问题,如果有更多的依赖,再考虑选择 操作系统 + jdk(如ubuntu + jdk)。

image.png

  1. 使用多阶段构建,减少中间层的大小
不使用两阶段构建使用两阶段构建
image-20241216161509517.pngimage-20241216161543546.png
700MB340MB
  1. 合并命令,清除无用缓存
不好的好的
image-20241216161632877.pngimage-20241216162235925.png
使用2条RUN命令,docker构建时会产生2层,父层的缓存文件并没有被实际删除,还保留在旧层里直接在一条命令里做完了删除操作,不会保存到最终的镜像内容中

每一种操作系统的缓存文件,可以自行搜索如何删除,此处不过多赘述。

工具

  1. docker history

Docker history可以查看每层镜像的大小,针对性分析。【如图】

docker history eclipse-temurin:21

image-20241216162846634.png

  1. dive命令

深入分析每一层文件的新增、修改、删除的情况。【如图】

docker run --rm -it \
    -v /var/run/docker.sock:/var/run/docker.sock \
    wagoodman/dive:latest eclipse-temurin:21

左边是层、右边是新增、修改、删除的内容。

image-20241216163419459.png