多阶段构建除了让最终镜像变小,还有一个常被忽视的好处:BuildKit 会把互不依赖的阶段同时构建。这篇用“睡 4 秒”的假工作量精确地验证:什么时候并发、什么时候串行、没被引用的阶段到底跑不跑。
构建退出码 0;总用时 4.34 秒(串行执行至少要 8 秒) 各步骤:#1 [internal] load build definition from Dockerfile | #2 [internal] load metadata for docker.io/library/alpine:3.20 | #3 [internal] load .dockerignore | #5 [b 2/2] RUN echo 1791619675.199984 && sleep 4 && echo B > /b.txt | #6 [a 2/2] RUN echo 1791619675.199984 && sleep 4 && echo A > /a.txt | #5 [b 2/2] RUN echo 1791619675.199984 && sleep 4 && echo B > /b.txt | #7 [stage-2 2/3] COPY --from=a /a.txt /a.txt | #8 [stage-2 3/3] COPY --from=b /b.txt /b.txt
两个阶段各睡 4 秒、最终阶段 COPY --from 两边,构建只用了 4.34 秒,输出里两个 RUN 同时出现。注意“工作量”是 sleep,不吃 CPU;真实的 CPU 密集构建受机器核数限制,不会这么理想(本文没有测)。
构建退出码 0;总用时 8.53 秒
FROM a AS b 让 b 依赖 a,总用时 8.53 秒,和两段相加一致。
构建退出码 0;总用时 0.33 秒;构建输出里 RUN 真正打印出 "这一步被执行了" 的行数 = 0;出现的步骤:#1 [internal] load build definition from Dockerfile | #2 [internal] load metadata for docker.io/library/alpine:3.20 | #3 [internal] load .dockerignore | #5 [used 2/2] RUN echo 1791619688.0704021 && echo "used 阶段执行" > /u.txt | #6 [stage-2 2/2] COPY --from=used /u.txt /u.txt 同一个 Dockerfile 用 --target never 单独构建那个阶段:总用时 3.33 秒;步骤:#1 [internal] load build definition from Dockerfile | #2 [internal] load metadata for docker.io/library/alpine:3.20 | #3 [internal] load .dockerignore | #5 [never 2/2] RUN echo 1791619688.404342 && echo "这一步被执行了" && sleep 3
最终阶段没有引用 never,整个构建 0.33 秒,构建输出里根本没有那个阶段的步骤;用 --target never 才会执行它(3.33 秒)。这也是 --target 常用来在同一个 Dockerfile 里构建测试镜像、调试镜像的原因。
构建退出码 0;总用时 8.51 秒
把两条 RUN 写在同一个阶段里,8.51 秒;拆成两个独立阶段是 4.34 秒。想要并发,工作必须分到互不依赖的阶段。
构建退出码 0;总用时 4.45 秒(完全并行约 4 秒,完全串行 16 秒)
4 个独立阶段各睡 4 秒,总共 4.45 秒,完全串行要 16 秒。把可以并行的工作(各自下载依赖、编译不同组件)放在不同阶段,最后用 COPY --from 汇合。