bigywater.tech
Docker · 15

BuildKit 并行构建:多阶段里互不依赖的阶段怎么并发

多阶段构建除了让最终镜像变小,还有一个常被忽视的好处:BuildKit 会把互不依赖的阶段同时构建。这篇用“睡 4 秒”的假工作量精确地验证:什么时候并发、什么时候串行、没被引用的阶段到底跑不跑。

4.34 秒 vs ≥ 8 秒
两个独立的 4 秒阶段并发构建的总用时,与串行的下限
0.33 秒
最终镜像不引用某阶段时,构建不执行它的总用时
4.45 秒 vs 16 秒
4 个独立的 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 汇合。

分步演示

多阶段构建并发
本步的对象

    本文用到的工具与边界

    • 本机 macOS 27 arm64 / Docker Desktop(Engine 29.8.2,容器跑在 Docker Desktop 自带的 Linux 虚拟机里,内核与宿主机的 macOS 不同);镜像为 linux/arm64;需要看虚拟机内部时用特权容器 + nsenter;计时用 time / date +%s%N,数字每次运行会有小幅抖动。每个阶段的“工作”是 RUN 里睡 4 秒,每次构建前给 RUN 加了带时间戳的内容避免命中缓存;总用时用 Python time.time 测;各情形只运行一次;实验后已删除所有镜像
    • 没有验证:CPU 密集构建下的并行度上限(BuildKit 的 max-parallelism 配置)、含网络下载阶段的并发、legacy builder 的行为、docker compose build 并发多个服务、内存受限时的并发表现
    京ICP备2026067502号-1京公网安备11010502063487号