bigywater.tech
Docker · 85

构建缓存失效:一行 RUN 改动后哪些层要重建

为什么改了一行代码,依赖又重新装了一遍?构建缓存到底看什么?这篇用 sleep 做可控的耗时,逐项量出:改 mtime、改内容、调换顺序、加注释、改命令各自让缓存怎样。

3.4 秒 对 0.3 秒
改一个源码文件后重建:先 COPY . 后安装依赖,与先依赖后源码的用时
touch 不失效
只改文件修改时间,构建 0.2 秒全部命中缓存
注释不失效
在 Dockerfile 里加一行注释,仍然全部 CACHED

缓存键

实测 · 本机执行输出
  第 1 次构建(冷):用时 5.8 秒;RUN echo salt-1791641893.564141 → 执行 | COPY app.txt /app.txt → 执行 | RUN sleep 2 && echo A > /a → 执行 | COPY dep.txt /dep.txt → 执行 | RUN sleep 2 && echo B > /b → 执行
  第 2 次:什么都没改:用时 0.2 秒;RUN sleep 2 && echo A > /a → CACHED | COPY dep.txt /dep.txt → CACHED | COPY app.txt /app.txt → CACHED | RUN echo salt-1791641893.564141 → CACHED | RUN sleep 2 && echo B > /b → CACHED
  第 3 次:只改 app.txt 的修改时间(touch):用时 0.2 秒;RUN sleep 2 && echo A > /a → CACHED | COPY dep.txt /dep.txt → CACHED | COPY app.txt /app.txt → CACHED | RUN echo salt-1791641893.564141 → CACHED | RUN sleep 2 && echo B > /b → CACHED
  第 4 次:改 app.txt 的内容:用时 4.7 秒;RUN echo salt-1791641893.564141 → CACHED | COPY app.txt /app.txt → 执行 | RUN sleep 2 && echo A > /a → 执行 | COPY dep.txt /dep.txt → 执行 | RUN sleep 2 && echo B > /b → 执行
  第 5 次:只改 dep.txt 的内容:用时 2.5 秒;RUN echo salt-1791641893.564141 → CACHED | COPY app.txt /app.txt → CACHED | RUN sleep 2 && echo A > /a → CACHED | COPY dep.txt /dep.txt → 执行 | RUN sleep 2 && echo B > /b → 执行
  第 6 次:app.txt 写入同样的内容(v2):用时 0.4 秒;COPY dep.txt /dep.txt → CACHED | RUN sleep 2 && echo A > /a → CACHED | RUN echo salt-1791641893.564141 → CACHED | COPY app.txt /app.txt → CACHED | RUN sleep 2 && echo B > /b → CACHED

COPY 指令的缓存键是所拷文件的内容(不是 mtime),RUN 指令的缓存键是命令字符串加上前面所有步骤的结果。一步失效,后面的步骤全部失效。内容改了再改回去,又会命中之前的缓存(第 6 次构建)。

顺序

实测 · 本机执行输出
  先 COPY . 再装依赖(RUN):改了源码 main.py 后:用时 3.4 秒;RUN echo salt2-1791641893.564141 → CACHED | COPY . /src → 执行 | RUN sleep 3 && echo deps > /deps → 执行
  先 COPY 依赖清单、装依赖,再 COPY 源码:改了源码 main.py 后:用时 0.3 秒;COPY deps.txt /src/deps.txt → CACHED | RUN echo salt3-1791641893.564141 → CACHED | RUN sleep 3 && echo deps > /deps → CACHED | COPY . /src → 执行

把变化少的放前面:先只拷依赖清单、安装依赖,再拷源码。这样改源码只会让最后的 COPY 重新执行。

Dockerfile 本身的修改

实测 · 本机执行输出
  加了一行注释:用时 0.3 秒;RUN echo salt4-1791641893.564141 → CACHED | RUN sleep 2 && echo X > /x → CACHED
  RUN 命令末尾多一个空格:用时 0.2 秒;RUN echo salt4-1791641893.564141 → CACHED | RUN sleep 2 && echo X > /x → CACHED
  RUN 命令里改了一个字符:用时 2.4 秒;RUN echo salt4-1791641893.564141 → CACHED | RUN sleep 2 && echo Y > /x → 执行
  在前面加一个 ENV:用时 2.4 秒;RUN echo salt4-1791641893.564141 → CACHED | RUN sleep 2 && echo X > /x → 执行

注释和 RUN 命令末尾的空格没有让缓存失效;改命令里的字符、在前面加 ENV 则会。这里只试了这四种改法,其它改法(如换行、续行符)我没有测。

强制重跑

实测 · 本机执行输出
  不加参数(全部命中缓存):用时 0.2 秒;RUN echo salt5-1791641893.564141 & → CACHED | RUN echo salt6-1791641893.564141 & → CACHED | COPY --from=one /1 /1 → CACHED | COPY --from=two /2 /2 → CACHED
  --no-cache-filter one(只让 one 阶段重新执行):用时 2.4 秒;RUN echo salt5-1791641893.564141 & → 执行 | COPY --from=one /1 /1 → CACHED | RUN echo salt6-1791641893.564141 & → CACHED | COPY --from=two /2 /2 → CACHED
  --no-cache(全部重新执行):用时 2.6 秒;RUN echo salt5-1791641893.564141 & → 执行 | RUN echo salt6-1791641893.564141 & → 执行 | COPY --from=one /1 /1 → 执行 | COPY --from=two /2 /2 → 执行

--no-cache 全部重跑;--no-cache-filter 阶段名 只重跑一个阶段,多阶段构建里很有用。

分步演示

构建缓存失效
本步的对象

    本文用到的工具与边界

    • 本机 macOS 27 arm64 / Docker Desktop(Engine 29.8.2,容器跑在 Docker Desktop 自带的 Linux 虚拟机里,内核与宿主机的 macOS 不同);镜像为 linux/arm64;需要看虚拟机内部时用特权容器 + nsenter;计时用 time / date +%s%N,数字每次运行会有小幅抖动。每个 RUN 里都有 sleep 2 或 3 秒用来让「执行」和「命中缓存」在耗时上一目了然,开头的 RUN echo 盐用来避开之前构建留下的缓存;是否 CACHED 取自 --progress=plain 的输出;每个场景之间 Dockerfile 与文件的变化见正文;实验后已删除镜像
    • 没有验证:RUN --mount=type=cache(见 c9)、外部缓存导入导出 --cache-from / --cache-to、ADD 远程 URL 的缓存键、ARG 对缓存的影响(见 c7)、.dockerignore 对缓存的影响(见 c2)、多平台构建的缓存
    京ICP备2026067502号-1京公网安备11010502063487号