docker build缓存失效分析

2,394 阅读6分钟

1、背景

前两天对一个前端项目进行自动化部署改造。改造后开发人员推送代码到Gitlab服务器就会触发阿里云容器镜像服务(ACR)自动构建镜像,构建完成后自动调用容器服务(ACK)的触发器,重新部署应用服务。

但是改造完成后有个问题,即使代码没有任何改变,ACR的镜像构建也无法使用缓存。效果如下:
每次构建耗时相差不大,几乎都是300s左右
image.png

构建日志也能看出从指令2/6开始没有任何使用缓存的迹象,命令都是重新执行的
image.png

如果代码不变完全使用缓存,构建时间和构建日志应该长这样:
构建耗时从300s左右降到30s左右
image.png

使用缓存的指令都有CACHED标志
image.png

2、docker build 缓存

以下是官方文档对docker build缓存说明,官方文档传送门,译文传送门

在镜像的构建过程中,Docker 会遍历 Dockerfile 文件中的指令,然后按顺序执行。在执行每条指令之前,Docker 都会在缓存中查找是否已经存在可重用的镜像,如果有就使用现存的镜像,不再重复创建。如果你不想在构建过程中使用缓存,你可以在 docker build 命令中使用 --no-cache=true 选项。 但是,如果你想在构建的过程中使用缓存,你得明白什么时候会,什么时候不会找到匹配的镜像,遵循的基本规则如下:

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

一旦缓存失效,所有后续的 Dockerfile 指令都将产生新的镜像,缓存不会被使用。

上面的说明没看懂可以先略过,下面我会强行简单解释一波:在 Dockerfile 的基础镜像(FROM 指令)不变的情况下,对于 ADDCOPY 指令而言只要指令不变且源文件不变就会走缓存, ADDCOPY 以外的指令只要指令不变就会走缓存;一旦某条指令缓存失效,后续指令的缓存也会失效。

这里可能不太好理解,可以先了解一些前置知识(使用Dockerfile构建镜像)再看

3、缓存失效的原因

3.1、猜测

对于大部分 Dockerfile 配置而言,如果代码不变所有指令都会使用缓存。那这个项目的Dockerfile配置到底有啥问题呢,先看具体配置:

FROM registry.cn-shenzhen.aliyuncs.com/xxx/node:lts-alpine AS builder

WORKDIR /app

COPY package.json .npmrc pnpm-lock.yaml ./

RUN npx pnpm install

COPY . .

RUN npm run build:prod

FROM registry.cn-shenzhen.aliyuncs.com/xxx/nginx:latest

COPY --from=builder /app/nginx /usr/local/openresty/nginx/conf/site-enabled

COPY --from=builder /app/build /app/frontend/www/console

唯一可能导致缓存失效的指令是:COPY . .。这条指令对应的源文件是源代码项目的根目录,里面的文件都是从Gitlab服务器 clone下来的,肯定不会出现差异,也许有一个例外:.git目录。

3.2、验证

为了验证.git目录是否会导致缓存失效,我在.dockerignore文件里把这个目录排除掉又构建了两遍,然后缓存生效,所有指令都使用缓存。

.git

现在可以确定导致缓存失效的元凶就是.git目录。但是为啥这个目录会出现变化呢?

从阿里云构建镜像的日志可以看出,每次构建的第一步都是clone代码
image.png

难道每次clone下来的代码.git目录都会不同?为了验证这个问题,我在本地先后两次clone这个项目的代码,然后对比他们的.git目录。从下图可以看到确实存在差异,而且有4个文件不同,其中.git/logs/HEAD文件出现差异的是clone时间。
image.png

4、解决方案

至此,缓存失效问题的原因已经明确:由于ACR构建镜像的第一步是git clone,而每次clone下来的代码中.git目录文件都会有所不同,因此导致Dockerfile中的COPY . .指令缓存失效。

这个问题的解决方案有两个:

  • 编写Dockerfile时,ADDCOPY 指令的源路径尽量指明那些会被构建工具使用到的路径,以此避免.git目录导致缓存失效
  • 使用.dockerignore排除.git目录

使用.dockerignore排除无用的路径,既可以减少传递给Docker引擎的文件数量,又可以避免影响缓存效果,这些都能提升构建速度。对于前端项目.dockerignore可以如下配置,也可以参考一些常用的.gitignore模板配置.dockerignore

.idea
.vscode
.git
node_modules
README.md

5、ACR构建的诡异表现

上面讨论的问题和方案都是基于代码没有任何修改的前提,这种情况下所有指令都会使用缓存。

如果修改了源码文件,缓存情况如何?按docker官方文档说明推测,COPY . .这条指令之后的缓存失效,之前的应该正常使用缓存。

实际上从阿里云的构建日志可以看出,ACR构建的表现与docker官方文档说明不一致,COPY . .之前的指令缓存也失效了
image.png

本地复现的结果与docker官方说明文档一致,从下图的构建日志可以看出,COPY . .之前的指令正常使用缓存,之后的缓存失效
image.png

也就是说,上文提出的解决方案,对阿里云容器镜像服务而言能使用缓存的情况是代码没有任何改变,但是对于自建的镜像构建服务而言,即使代码改变也有一定缓存效果,比如可以节省1min左右的npm install的时间。

这个问题没找到原因,不知道是否有大佬知道原因或有啥好建议。

6、排查ACR构建问题

起初排查ACR构建问题没找到啥好办法,只能反复猜测问题、尝试改代码、推送代码、触发ACR构建、到阿里云控制台页面查看日志、验证猜想,整个流程繁琐而且费时费力。后阶段是在本地安装docker环境,模拟ACR构建尝试复现和排查问题,脚本如下:

rm -rf workspace \
&& git clone git@gitlab.xxx.com:xxx.git workspace\
&& cd workspace \
&& git checkout test \
&& sudo docker build -t imgxx:v1 .

# 查看镜像
# sudo docker image ls
# 删除镜像
# sudo docker image prune -a

由于不是专业选手,不知道是否有更好的排查调试方法。