|-------------------------|----------------------------------------|----------------------------------|
| 章节 | 关键文件 | 解答 |
| 1. 为什么有 builder-next | daemon/internal/builder-next/ 整个目录 | 与 v1 builder 的根本差异 |
| 2. 整体架构 | builder.go / controller.go / worker.go | BuildKit 适配层怎么组装 |
| 3. 入口 Builder 类型 | builder.go | Daemon 持有的 buildkit.Builder 是什么 |
| 4. Build 请求翻译 | builder.go::Build | Docker 选项 → SolveRequest |
| 5. Controller 装配 | controller.go | 两种后端:snapshotter vs graphdriver |
| 6. Worker 与 Executor | worker/worker.go + executor*.go | 谁来跑 RUN、谁来出网络 |
| 7. Exporter 与产物 | exporter/、mobyexporter/ | BuildKit 产物怎么落回 moby image store |
| 8. 上传上下文:reqbodyhandler | reqbodyhandler.go | HTTP body → BuildKit 的 URL |
| 9. 取消、Prune、DiskUsage | builder.go::Cancel/Prune/DiskUsage | 生命周期 API |
| 10. 与 v1 builder 的对照 | --- | 异同速查 |
| 11. 调用链总览 | --- | 一图速查 |
1. 为什么有 builder-next
经典 v1 builder(daemon/builder/dockerfile/)是 "在 dockerd 进程内的 Dockerfile 顺序解释器",每条指令一个临时容器 → commit 一层,缓存严格按顺序匹配。这套实现有天然上限:
- 不能并行执行多阶段;
- 不能跨构建共享执行结果(cache 只按 image history 串匹配);
- 不支持
RUN --mount、缓存挂载、密钥/SSH 注入; --chmod、--link、--parents、SBOM/Provenance 等都做不了。
BuildKit 用 LLB(DAG)作为 IR,按"内容寻址 + 并行求解"重做整个执行模型;Docker 社区从 18.09 起把 BuildKit 嵌进 dockerd,这就是 daemon/internal/builder-next/("next" 即相对于 v1 的下一代 builder)。它的包名直接叫 buildkit:
Go
// daemon/internal/builder-next/builder.go
package buildkit
而 daemon/builder/backend/backend.go::Backend.Build 根据 Options.Version 决定走 v1 还是 v2:
Go
useBuildKit := options.Version == build.BuilderBuildKit
if useBuildKit {
buildResult, err = b.buildkit.Build(ctx, config) // 本文档主角
} else {
buildResult, err = b.builder.Build(ctx, config) // v1
}
关键认识:Moby 本身并不重写 BuildKit 的执行引擎,而是:
- 把 BuildKit 的
control.Controller、worker.Worker、executor.Executor等组件装配进 dockerd 进程;
- 用 adapters 把 BuildKit 期望的接口(content store / snapshotter / cache source / image source)桥接到 dockerd 自己的镜像/层存储;
- 用 mobyexporter 把 BuildKit 算出的 OCI descriptor 写回 dockerd 的 image store;
- 用 executor + libnetwork 让 BuildKit 的 RUN 走 dockerd 自己的网络栈(bridge/NAT)而不是 BuildKit 自带的 CNI/host 网络。
也就是说,builder-next 是一套"适配胶水层"。
2. 整体架构
Go
HTTP POST /build (DOCKER_BUILDKIT=1)
│
▼
daemon/builder/backend/backend.go::Backend.Build
│ useBuildKit == true
▼
daemon/internal/builder-next/builder.go::Builder.Build
│ ① 翻译选项 → controlapi.SolveRequest
│ ② 注入 dockerfile.v0 前端
│ ③ errgroup: Solve / Status / aux 流
▼
github.com/moby/buildkit/control.Controller.Solve ← BuildKit 上游
│
├─ Frontend: dockerfile.v0 (forwarder.NewGatewayForwarder)
│ └─ github.com/moby/buildkit/frontend/dockerfile/builder.Build
│
├─ Worker (worker.Controller 选一个)
│ └─ moby worker (NewContainerdWorker / NewWorker)
│ ├─ SourceManager (containerimage / git / http / local)
│ ├─ CacheManager (bbolt cache.db)
│ ├─ Snapshotter (adapters/snapshot 或 containerd snapshotter)
│ └─ Executor (runc / containerd)
│
└─ Exporter: "moby" 包装层
└─ exporter/wrapper → mobyexporter
└─ 写回 dockerd image store + 触发 Named/Exported 回调
目录与职责一览:
|-----------------------------------|-----------------------------------------------------------------------------------|
| 路径 | 角色 |
| builder.go | 顶层 Builder:API 暴露的 Build/Cancel/Prune/DiskUsage |
| controller.go | newController:装配 BuildKit control.Controller(含 snapshotter/graphdriver 两套) |
| executor*.go | BuildKit executor.Executor 实现:Linux=runc / Windows=containerd / 其他=stub |
| executor_opts.go | 跨平台 executor 入参容器 |
| reqbodyhandler.go | "HTTP body 当作 URL" 的 fake transport,给 BuildKit 拉 build context |
| worker/worker.go | GraphDriver 模式的 Worker(装配 source/cache/executor/exporter) |
| worker/containerdworker.go | containerd snapshotter 模式的 Worker |
| worker/gc.go | 默认 GC 策略(缓存分层、KeepDuration 等) |
| exporter/exporter.go | 常量定义(Moby、BuildRefLabel) |
| exporter/wrapper.go | 给 BuildKit 的 image exporter 套一层 moby 钩子(unpack、dangling prefix、Named/Exported 回调) |
| exporter/mobyexporter/export.go | GraphDriver 模式下的镜像导出器(通过 Differ 落 moby layer) |
| exporter/overrides/ | repo:tag 规范化、SanitizeRepoAndTags |
| adapters/snapshot/ | 把 moby graphdriver + layer store 包成 BuildKit snapshot.Snapshotter |
| adapters/containerimage/ | BuildKit 的 source.ContainerImage 适配 → 走 dockerd 自己的 pull 链路 |
| adapters/localinlinecache/ | registry 内联缓存的本地导入适配 |
| imagerefchecker/checker.go | 给 BuildKit cache manager 提供外部引用探测(防止把 moby 仍在用的层 GC 掉) |
3. 入口 Builder 类型
Go
// builder.go
type Builder struct {
controller *control.Controller // BuildKit 上游控制器(真正干活的人)
dnsconfig config.DNSConfig
reqBodyHandler *reqBodyHandler // 把 build context HTTP body 暴露成 URL
diskUsage singleflight.Group[...] // DiskUsage 去重
mu sync.Mutex
jobs map[string]*buildJob // 用于 stream/upload-request 的 build 任务表
useSnapshotter bool // 走 containerd-snapshotter 还是 graphdriver
}
func New(ctx context.Context, opt Opt) (*Builder, error) {
reqHandler := newReqBodyHandler(tracing.DefaultTransport)
c, err := newController(ctx, reqHandler, opt) // 一次性装配 Controller
return &Builder{controller: c, ..., jobs: map[string]*buildJob{}, useSnapshotter: opt.UseSnapshotter}, nil
}
几个关键设计:
- 单例 :
buildkit.New在daemon/command/daemon.go:610``initBuildkit中被调用,整个 dockerd 生命周期内只有一个*Builder,所有/build请求共用一个 Controller。
- GRPC 注册 :
Builder.RegisterGRPC(s)把 Controller 挂到 dockerd 内嵌的 gRPC server 上,让客户端可以直接以 BuildKit 协议访问(这是buildctl命令通道)。
- Opt 是"Daemon 能力注入清单" :
SessionManager / NetworkController / RegistryHosts / IdentityMapping / DistributionServices / ImageTagger / CDICache / BuilderConfig等,BuildKit 通过这层接口反过来用 dockerd 的资源,而不是自己另起炉灶。
Opt 关键字段速查
|-------------------------------------------------------------|----------------------------------------------------------------------------------------------------------------------------|
| 字段 | 用途 |
| SessionManager | BuildKit 的 session.Manager,承载客户端密钥/SSH/上下文上传等长连接通道 |
| Root | BuildKit 自身工作目录(默认 <docker-root>/buildkit/),存 cache.db、containerdmeta.db、history*.db、content/、executor/、net/ |
| EngineID | dockerd 实例 ID,写入 worker 元数据 |
| Dist | images.DistributionServices:layer store / ref store / image store / download manager / v2 metadata 等 |
| ImageTagger | 谁能给镜像打 tag(d.ImageService()) |
| NetworkController | libnetwork.Controller:BuildKit 内 bridge 网络的真实实现 |
| RegistryHosts | 镜像仓库 endpoint 解析(auth、mirror) |
| BuilderConfig | daemon.json 里 builder 段配置:GC、entitlements、history |
| UseSnapshotter | 决定走哪条装配路径:true → containerd snapshotter;false → graphdriver(overlay2 等) |
| Snapshotter / ContainerdAddress / ContainerdNamespace | containerd-snapshotter 模式所需的连接参数 |
| Callbacks | BuildkitCallbacks{Exported, Named}:把 BuildKit 的产物事件回传给 dockerd |
| CDICache | CDI(Container Device Interface)设备注入 |
4. Builder.Build:Docker 选项 → BuildKit SolveRequest
这是 BuildKit 适配层最核心的 200 行。整体流程:
Go
Build(ctx, opt)
├─ 1) 处理 upload-request / RemoteContext=upload-request(流式 build context)
├─ 2) 把 Options 翻译成 frontendAttrs(BuildKit 前端键值)
├─ 3) 解析 exporterName / exporterAttrs
├─ 4) 处理 inline cache(BUILDKIT_INLINE_CACHE)
├─ 5) 构造 controlapi.SolveRequest
└─ 6) errgroup 三路并发:Solve / Status / aux 流
4.1 build context 的两种入参
Go
rc := opt.Source
if buildID := opt.Options.BuildID; buildID != "" {
if strings.HasPrefix(buildID, "upload-request:") {
// 客户端先上传 body,再发真正的 build 请求
upload = true
buildID = strings.TrimPrefix(buildID, "upload-request:")
}
j := b.jobs[buildID] // 复用同一个 buildJob
if upload {
j.SetUpload(ctx, rc) // 把 body 暂存
return nil, nil
}
if opt.Options.RemoteContext == "upload-request" {
rc, err = j.WaitUpload(ctx) // 等待上传
opt.Options.RemoteContext = ""
}
}
BuildKit 不直接吃 HTTP body,所以走第二种"普通 POST build"路径时,要靠 reqBodyHandler:
reqBodyHandler 是一个 http.RoundTripper:拦截 host == "build-context-<id>" 的 GET,把暂存的 body 当成 200 响应回出去;其他请求直接转发到底层 transport(tracing.DefaultTransport)。最终 BuildKit 用普通的 HTTP 拉取就能拿到客户端上传的 tar 上下文------这是个非常聪明的设计,避免了把 dockerd 的 IO 耦合进 BuildKit 内部。
4.2 翻译 frontendAttrs
BuildKit 的 dockerfile.v0 前端通过 map 接收所有可配置项,这里把 Docker 选项一一映射:
|-------------------------------------------|-----------------------------------------|-----------------------------|
| Docker 选项 | frontendAttrs 键 | 说明 |
| Options.Target | target | 多阶段构建的截止 stage |
| Options.Dockerfile(非空非 ".") | filename | 指定 Dockerfile 路径 |
| Options.RemoteContext(非 client-session) | context | git/url 形式的远端 context |
| 否则(HTTP body context) | context | build-context-<id> URL |
| Options.CacheFrom | cache-from(逗号分隔) | 引用缓存来源 |
| Options.BuildArgs[k]=v(v 非 nil) | build-arg:k | build 参数 |
| Options.Labels[k]=v | label:k | 注入 label 到末段 |
| Options.NoCache | no-cache(空值) | 禁用缓存 |
| Options.PullParent | image-resolve-mode=pull(否则 default) | 强制拉父镜像 |
| Options.Platform | platform | 跨平台构建 |
| Options.NetworkMode ∈ {host, none} | force-network-mode | 其他值报错 |
| Options.ExtraHosts | add-hosts(CSV:host=ip) | 经 toBuildkitExtraHosts 转换 |
| Options.ShmSize | shm-size | RUN 容器的 /dev/shm |
| Options.Ulimits | ulimit(CSV) | RUN 容器的 ulimit |
注意几个细节:
host-gateway关键字 :toBuildkitExtraHosts把extra-hosts: host:ip中的ip == "host-gateway"替换成 daemon 配置里的HostGatewayIPs,支持多 IP。
- NetworkMode 限制 :BuildKit 后端只接受
host/none/空;像bridge、container: 这种 moby 网络模式直接拒绝------这也是为什么 BuildKit 模式下docker build --network=bridge会报错。
force-network-mode=host隐含 entitlements :进入 SolveRequest 时附加Entitlements: ["network.host"],daemon 侧getEntitlements默认允许。
4.3 选 exporter
Go
exporterName := ""
exporterAttrs := map[string]string{}
if len(opt.Options.Outputs) == 0 {
exporterName = exporter.Moby // 默认:写回 moby image store
} else if opt.Options.Outputs[0].Type != "cacheonly" {
exporterName = opt.Options.Outputs[0].Type // 用户显式指定(tar/local/oci-digest/...)
exporterAttrs = opt.Options.Outputs[0].Attrs
}
// 把 --tag 翻译成 BuildKit 期望的 name 属性
if (exporterName == client.ExporterImage || exporterName == exporter.Moby) && len(opt.Options.Tags) > 0 {
nameAttr, _ := overrides.SanitizeRepoAndTags(opt.Options.Tags)
exporterAttrs["name"] = strings.Join(nameAttr, ",")
}
exporter.Moby == "moby" 是 moby 自定义的 exporter 名字;在 worker/containerdworker.go::Exporter 里有一个特例分支:当请求 moby 时,先取出 BuildKit 内置的 ExporterImage,再套上 exporter.NewWrapper 装上 moby 回调。
cacheonly 是个哨兵:用户传 --output type=cacheonly 表示"只算 cache,不要导出镜像"。
4.4 inline cache
Go
cache := &controlapi.CacheOptions{}
if inlineCache := opt.Options.BuildArgs["BUILDKIT_INLINE_CACHE"]; inlineCache != nil {
if b, err := strconv.ParseBool(*inlineCache); err == nil && b {
cache.Exports = append(cache.Exports, &controlapi.CacheOptionsEntry{Type: "inline"})
}
}
经典 BUILDKIT_INLINE_CACHE=1 把缓存元数据内联进镜像 annotation,是 BuildKit 兼容老 Docker 用户的入口。
4.5 构造 SolveRequest 并三路并发
Go
id := identity.NewID()
req := &controlapi.SolveRequest{
Ref: id,
Exporters: []*controlapi.Exporter{{Type: exporterName, Attrs: exporterAttrs}},
Frontend: "dockerfile.v0",
FrontendAttrs: frontendAttrs,
Session: opt.Options.SessionID,
Cache: cache,
}
aux := streamformatter.AuxFormatter{Writer: opt.ProgressWriter.Output}
eg, ctx := errgroup.WithContext(ctx)
// ① Solve:阻塞等待构建完成
eg.Go(func() error {
resp, err := b.controller.Solve(ctx, req)
if exporterName != exporter.Moby && exporterName != client.ExporterImage {
return nil
}
imgID := resp.ExporterResponse["containerimage.digest"]
out.ImageID = imgID
return aux.Emit("moby.image.id", build.Result{ID: imgID})
})
// ② Status:把 buildkit 的 StatusResponse 流通过 chan 喂给 ③
ch := make(chan *controlapi.StatusResponse)
eg.Go(func() error {
defer close(ch)
stream := &statusProxy{streamProxy: streamProxy{ctx: context.TODO()}, ch: ch}
return b.controller.Status(&controlapi.StatusRequest{Ref: id}, stream)
})
// ③ aux:把每个 StatusResponse 编码成 protobuf,以 "moby.buildkit.trace" aux 消息回传给客户端
eg.Go(func() error {
for sr := range ch {
dt, _ := proto.Marshal(sr)
aux.Emit("moby.buildkit.trace", dt)
}
return nil
})
eg.Wait()
几个关键点:
- 三个 goroutine 共享 ctx,errgroup 串联错误------任何一个失败整体取消。
- Status 的 ctx 故意用
context.TODO():注释解释,"请求取消时只应取消构建本身,不应取消状态推送"。
- 客户端拿到两条 aux :
moby.image.id(最终镜像 digest)和持续的moby.buildkit.trace(StatusResponse 流,包含 vertex/log/timing)------BuildKit 的 progress UI 就是吃这个流。
statusProxy/pruneProxy实现 gRPC server stream 接口,把 Send/SendMsg 转发到 chan,相当于把 BuildKit 的服务器端流式响应"骗"成 Go channel。
4.6 buildJob:流式 build context
buildJob 用一对 chan 在"上传请求"和"消费请求"之间做同步:
Go
type buildJob struct {
cancel func()
waitCh chan func(io.ReadCloser) error
}
SetUpload(rc) 把读端塞进 waitCh;WaitUpload() 注册一个回调到 waitCh,回调被消费时拿到 rc。wrapRC 在 Read/Close 时通过 waitCh 通知对方"读完了",让上传侧感知到对方已经消费完 body。
这套机制是为了支持 BuildKit 把 build context 通过 client-session 流式传输------老 Docker 客户端的"先 POST body,再发 build 请求"两段式语义在这里被桥接。
5. controller.go:BuildKit Controller 装配
newController 根据 opt.UseSnapshotter 分叉:
Go
func newController(ctx, rt, opt) (*control.Controller, error) {
if opt.UseSnapshotter {
return newSnapshotterController(ctx, rt, opt) // containerd image store 后端
}
return newGraphDriverController(ctx, rt, opt) // 经典 graphdriver 后端(overlay2 等)
}
5.1 newSnapshotterController(containerd image store 模式)
走 BuildKit 自带的 containerd worker:
Go
workerOpts := containerd.WorkerOptions{
Root, Address, SnapshotterName, Namespace,
Rootless, DNS, NetworkOpt{Mode: "host"(Linux) / "auto"(Windows)},
ApparmorProfile, CDIManager,
}
wo, _ := containerd.NewWorkerOpt(workerOpts, ctd.WithTimeout(60*time.Second))
wo.GCPolicy = getGCPolicy(...)
wo.RegistryHosts = opt.RegistryHosts
wo.Labels = getLabels(opt, wo.Labels)
wo.Executor = newExecutor(...) // 自己造的 runc executor
w, _ := mobyworker.NewContainerdWorker(ctx, wo, opt.Callbacks, rt)
关键事实:
- 网络用 host 模式 (Linux),因为 BuildKit 内部的 CNI/host 网络由 moby 自家 libnetwork 接管(见下文
bridgeProvider)。
- executor 是 moby 自家 newExecutor,而不是 BuildKit 默认------目的是让 RUN 用 moby 配置的 cgroup parent、apparmor、idMap。
- Capabilities 差异 :containerd-snapshotter 模式天然支持
pb.CapMergeOp/pb.CapDiffOp(layer merge/diff 加速),而 graphdriver 模式被显式禁用:
Go
// newGraphDriverController 里
pb.Caps.Init(apicaps.Cap{
ID: pb.CapMergeOp, Enabled: false,
DisabledReasonMsg: "only enabled with containerd image store backend",
})
pb.Caps.Init(apicaps.Cap{
ID: pb.CapDiffOp, Enabled: false,
DisabledReasonMsg: "only enabled with containerd image store backend",
})
5.2 newGraphDriverController(经典 overlay2 模式)
这是 moby 自己写的一套大装配,干了好几件事:
- 从 LayerStore 拿 graphdriver.Driver:
Go
driver = dist.LayerStore.(interface{ Driver() graphdriver.Driver }).Driver()
- 建本地 content store + containerd metadata DB(不连真实 containerd,纯进程内 boltdb + 文件):
Go
innerStore, _ := local.NewStore(filepath.Join(root, "content"))
db, _ := bolt.Open(filepath.Join(root, "containerdmeta.db"), 0o644, nil)
mdb := ctdmetadata.NewDB(db, innerStore, map[string]snapshots.Snapshotter{})
store := containerdsnapshot.NewContentStore(mdb.ContentStore(), "buildkit")
- 用
adapters/snapshot把 moby 的 graphdriver 包成 BuildKit snapshotter:
Go
snapshotter, lm, _ := snapshot.NewSnapshotter(snapshot.Opt{
GraphDriver: driver,
LayerStore: dist.LayerStore,
Root: root,
IdentityMapping: opt.IdentityMapping,
}, ctdmetadata.NewLeaseManager(mdb), "buildkit")
这是整套适配层的"灵魂":BuildKit 期望的快照 API(Stat/Usage/Prepare/Mounts/Commit/Remove/...)全部翻译成 moby graphdriver 的调用 + 一层 boltdb 存元数据(parent/committed/chainid/size)。
- 建 BuildKit cache.Manager + content store ,并接入
imagerefchecker:
Go
refChecker := imagerefchecker.New(imagerefchecker.Opt{
ImageStore: dist.ImageStore,
LayerGetter: snapshotter.(imagerefchecker.LayerGetter),
})
cm, _ := cache.NewManager(cache.ManagerOpt{
Snapshotter: snapshotter,
MetadataStore: md,
PruneRefChecker: refChecker, // ← 防止 GC 把 moby image store 在用的层删掉
LeaseManager: lm,
ContentStore: store,
GarbageCollect: mdb.GarbageCollect,
Root: root,
})
- 建 image source(pull 走 moby 的 download manager):
Go
src, _ := containerimage.NewSource(containerimage.SourceOpt{
CacheAccessor: cm,
ContentStore: store,
DownloadManager: dist.DownloadManager, // ← moby 的并发下载器
MetadataStore: dist.V2MetadataService,
ImageStore: dist.ImageStore,
ReferenceStore: dist.ReferenceStore,
RegistryHosts: opt.RegistryHosts,
LayerStore: dist.LayerStore,
...
})
这是关键差异:BuildKit 默认会用 containerd 的 remotes 拉镜像;而 moby 让 BuildKit 的 FROM 走 moby 自己的 pull 链路 (共享下载缓存、共享 blob store、共享 throttling),从而保证 docker pull 和 docker build 的 FROM 共享层。
- 建 mobyexporter :把 BuildKit 算出的 descriptor 通过
Differ.EnsureLayer落成 moby layer:
Go
differ := snapshotter.(mobyexporter.Differ)
exp, _ := mobyexporter.New(mobyexporter.Opt{
ImageStore: dist.ImageStore,
ContentStore: store,
Differ: differ,
ImageTagger: opt.ImageTagger,
LeaseManager: lm,
ImageExportedCallback: opt.Callbacks.Exported,
})
- 清理上次构建的 stale lease:
Go
leases, _ := lm.List(ctx, `labels."buildkit/lease.temporary"`)
for _, l := range leases { lm.Delete(ctx, l) }
避免 dockerd 重启后上次的临时层永远挂着。
- 统一装配 frontends:
Go
frontends := map[string]frontend.Frontend{
"dockerfile.v0": forwarder.NewGatewayForwarder(wc.Infos(), dockerfile.Build),
"gateway.v0": gwf,
}
dockerfile.v0 是 BuildKit 上游的 gateway frontend;gateway.v0 允许把构建逻辑放进容器里(自定义前端)。
- 交回
control.NewController(control.Opt{...}),把 session/worker/frontend/cache/remoteCache/history/lease/trace 都连进去。
注意两种模式的 cache importer/exporter 不同:
|-------------|-------------------------------------|---------------------------------|
| 模式 | CacheImporter | CacheExporter |
| Snapshotter | gha / local / registry | gha / inline / local / registry |
| GraphDriver | registry (localinlinecache) / local | inline |
也就是说 graphdriver 模式下没法直接用 registry 远端缓存------只能用 inline 模式。这是历史遗留限制。
5.3 GC 策略
getGCPolicy(conf, root) 把 daemon.json 里 builder.gc 配置翻译成 BuildKit 的 []client.PruneInfo。如果没配,用 worker.DefaultGCPolicy(root, ...) 默认四档:
Go
// gc.go
return []client.PruneInfo{
// 档 1:>512MB 的 source.local/exec.cachemount/source.git.checkout,48h 未用即清
{Filter: []string{"type==source.local,type==exec.cachemount,type==source.git.checkout"},
KeepDuration: 48 * time.Hour, MaxUsedSpace: tempCacheReservedSpace},
// 档 2:所有 60 天未用的层
{KeepDuration: 60 * 24 * time.Hour, ReservedSpace/MaxUsedSpace/MinFreeSpace},
// 档 3:把未共享缓存压到 cap 以内
{ReservedSpace/MaxUsedSpace/MinFreeSpace},
// 档 4:兜底------上述都不够就动 internal 数据
{All: true, ReservedSpace/MaxUsedSpace/MinFreeSpace},
}
tempCachePercent = math.E * math.Pi * math.Phi ≈ 13.8------一个数学常数彩蛋,用于推导临时缓存保留比例。
getEntitlements 默认放开 network.host 和 device;security.insecure 必须显式 opt-in。
6. Worker 与 Executor:谁真正执行 RUN
6.1 Worker 的两种实现
|------------------------------|------------------------------|-----------------------------------------------------------|
| Worker 实现 | 文件 | 适用模式 |
| worker.NewWorker | worker/worker.go | GraphDriver(经典 dockerd) |
| worker.NewContainerdWorker | worker/containerdworker.go | containerd snapshotter(features containerd-snapshotter) |
两者都返回 worker.Worker 接口给 BuildKit,内部都嵌入 BuildKit 的 base.Worker。mobyworker.NewContainerdWorker 实际是:
Go
bw, _ := base.NewWorker(ctx, wo) // BuildKit base worker
bw.SourceManager.Register(httpSource) // 额外注册 HTTP source(带 moby 的 transport)
return &ContainerdWorker{Worker: bw, callbacks: callbacks}
并 override 了 Exporter(name):当 name == "moby" 时,把 BuildKit 内置 ExporterImage 包一层 wrapper,注入 moby 钩子。
6.2 Worker 注册的 Source Manager
NewWorker 注册四类 BuildKit source:
Go
sm.Register(opt.ImageSource) // adapters/containerimage:FROM <image>
gs := git.NewSource(...) // buildkit 自带:FROM git+...
sm.Register(gs)
hs := http.NewSource(...) // buildkit 自带:ADD/COPY http://...
sm.Register(hs)
ss := local.NewSource(...) // buildkit 自带:客户端 session 上传的 context
sm.Register(ss)
第一类是 moby 自己实现的 imageadapter.Source,它绕开 BuildKit 默认的 containerd remotes,转用 moby 的 download manager:
Go
// adapters/containerimage/pull.go
type Source struct {
SourceOpt // DownloadManager / V2MetadataService / ImageStore / LayerStore ...
g flightcontrol.Group[*resolveRemoteResult] // 去重并发解析
}
效果:BuildKit 拉镜像和 docker pull 走同一套缓存与限速;FROM 多个阶段共享层;不会重复拉。
6.3 Executor:真正跑 RUN 进程
executor.go 中的 bridgeProvider 把 moby 的 libnetwork.Controller 包装成 BuildKit network.Provider:
Go
type bridgeProvider struct {
*libnetwork.Controller
Root string
}
func (p *bridgeProvider) New(ctx, _ string) (network.Namespace, error) {
n, _ := p.NetworkByName(networkName) // "bridge" (Linux) / "nat" (Windows)
iface := &lnInterface{ready: ..., provider: p}
go iface.init(p.Controller, n) // 异步建 endpoint + sandbox + join
return iface, nil
}
lnInterface.init 在另一个 goroutine 里完成 CreateEndpoint + NewSandbox(OptionUseExternalKey) + Join------OptionUseExternalKey 很关键,它允许把网络命名空间 attach 到 BuildKit runc 创建的容器上(而不是反过来)。
各平台分流:
|---------|-------------------------------------------------------------------------------------|---------------------------------|------------|
| 平台 | newExecutor / newExecutorGD 实现 | 网络 | 容器运行时 |
| Linux | executor_linux.go:runcexecutor.New | bridge(libnetwork)/ host / none | runc |
| Windows | executor_windows.go:containerdexecutor.New | nat(libnetwork)/ none | containerd |
| 其他 | executor_others.go + executor_nolinux.go:stubExecutor(直接返回 "not implemented") | --- | --- |
注意 executor_nolinux.go::newExecutorGD 也返回 stub,这就是为什么 graphdriver + BuildKit 在非 Linux/Windows 上跑不了。
Linux 上有个细节:
Go
idmap := &opts.identityMapping
if opts.identityMapping.Empty() {
idmap = nil // 空 idmap 会破坏 BuildKit,必须传 nil
}
rm, _ := resources.NewMonitor()
runcCmds := []string{"runc"}
if v := os.Getenv("DOCKER_BUILDKIT_RUNC_COMMAND"); v != "" {
runcCmds = []string{v} // 给 rootless / 自定义 runc 留口子
}
return runcexecutor.New(runcexecutor.Opt{
Root: filepath.Join(opts.root, "executor"),
CommandCandidates: runcCmds,
DefaultCgroupParent: opts.cgroupParent,
Rootless: opts.rootless,
NoPivot: os.Getenv("DOCKER_RAMDISK") != "", // RAM disk 下不能 pivot_root
IdentityMapping: idmap,
DNS: opts.dnsConfig,
ApparmorProfile: opts.apparmorProfile,
ResourceMonitor: rm,
CDIManager: opts.cdiManager,
}, networkProviders)
lnInterface.Set(*specs.Spec) 是 OCI spec 生成钩子,在容器真正启动前由 runc executor 调用,把 libnetwork 的 sandbox 通过 libnetwork-setkey reexec 注入容器 netns(Linux)或把 HNS endpoint id 列表填进 spec.Windows.Network(Windows)。
7. Exporter:BuildKit 产物写回 moby
7.1 三层包装
client (SolveRequest.Exporters[0].Type)
│
│ "moby" (默认)
▼
worker.ContainerdWorker.Exporter("moby", sm)
│ 取 BuildKit 自带 image exporter,套 wrapper
▼
exporter.NewWrapper(buildkitImageExporter, contentStore, callbacks) [wrapper.go]
│ - 在 Resolve 时强制 unpack=true、dangling prefix="moby-dangling"
│ - 在 Export 后写 refLabel、触发 Exported/Named 回调
▼
buildkit containerimage.Exporter (上游)
│ 生成 OCI descriptor、写 content store
▼
(graphdriver 模式下另有一条 mobyexporter 路径,通过 Differ.EnsureLayer 落 moby layer)
7.2 wrapper 干的几件事
Go
// wrapper.go::Resolve
exporterAttrs[OptKeyName] = sanitize(...)
exporterAttrs[OptKeyUnpack] = "true" // 默认 unpack 到 moby image store
exporterAttrs[OptKeyDanglingPrefix] = "moby-dangling" // 匿名镜像前缀
exporterAttrs[OptKeyDanglingEmptyOnly] = "true"
unpack=true:让 BuildKit 不只是写 manifest/config,还要把 layer 解压进 moby layer store,这样docker run就能直接用。
dangling prefix=moby-dangling:未命名镜像打这个 prefix,方便后续 GC 和docker images -f dangling=true。
Go
// wrapper.go::imageExporterInstanceWrapper.Export
refLabelKey := BuildRefLabel + buildInfo.Ref // "moby/build.ref.<build-ref>"
content.Update(ctx, content.Info{
Digest: desc.Digest,
Labels: map[string]string{refLabelKey: json(BuildRefLabelValue{CreatedAt: now})},
}, "labels."+refLabelKey)
if callbacks.Exported != nil { callbacks.Exported(ctx, imageID, desc) }
if callbacks.Named != nil { ... callbacks.Named(ctx, namedTagged, desc) ... }
moby/build.ref.<ref>content label :记录"这个 image content 是哪次构建产生的、什么时候产生的",用于docker builder prune --filter until=...等管理操作。
Exported回调 →daemon.Daemon.ImageExportedByBuildkit,让 dockerd 内部记录"BuildKit 给我产了一个新镜像"。
Named回调 →daemon.Daemon.ImageNamedByBuildkit:BuildKit 模式下 tag 操作直接由 image service 处理(参见mobyexporter.Opt里 "Callbacks.Named is not used here because the tag operation is handled directly by the image service")。
7.3 mobyexporter(graphdriver 专用)
exporter/mobyexporter/export.go::imageExporter.Export:在 graphdriver 后端下,BuildKit 没有 containerd image store 可写,所以 moby 自己实现一份导出:
- 解析
inp.Refs(多平台时只允许单 ref);
- 调用
Differ.EnsureLayer(ctx, key)把 BuildKit snapshot 固化成 moby layer;
- 构造 moby
image.Image写入image.Store;
- 通过
ImageTagger.TagImage打 tag。
Differ 接口由 adapters/snapshot 实现------本质上是把 BuildKit 的 snapshot diff 翻译成 graphdriver 的 ApplyDiff/Commit。
8. imagerefchecker:跨子系统引用探测
BuildKit 的 cache manager 有自己的 GC,但 moby image store 里仍然在引用的层不能被 BuildKit GC 掉 。imagerefchecker 就是这道防线的实现:
Go
// imagerefchecker/checker.go
func New(opt Opt) cache.ExternalRefCheckerFunc {
return func() (cache.ExternalRefChecker, error) {
return &checker{
opt: opt,
layers: lchain{}, // 用 DiffID 链构造前缀树
cache: map[string]bool{},
}, nil
}
}
它的工作机制:
- BuildKit 准备 GC 某个 ref 前会调
ExternalRefChecker.IsYetVisible(blobKey, ...);
- checker 把 moby image store 中所有镜像的 layer chain 装进一棵
lchain前缀树;
- 查询:BuildKit 给的 layer chain 是否是某棵 moby layer chain 的前缀?是 → 还在用,不能 GC;否 → 可以 GC。
这样把 moby 的 "image store 是 source of truth" 语义延伸到 BuildKit cache 层。
9. Cancel / Prune / DiskUsage:管理 API
9.1 Cancel
Go
func (b *Builder) Cancel(ctx, id string) error {
b.mu.Lock()
if j, ok := b.jobs[id]; ok && j.cancel != nil {
j.cancel() // 取消 build context
}
b.mu.Unlock()
return nil
}
注意:Cancel 只取消通过 b.jobs 注册过的请求(带 BuildID 的)。这意味着大部分直接走 docker build CLI 的请求,POST /build/cancel是通过 HTTP 连接断开来传播取消的,而不是靠 BuildID------和 v1 builder 不同。
9.2 Prune
Go
func (b *Builder) Prune(ctx, opts) (size int64, cacheIDs []string, err error) {
// ① 校验 filter 字段:id/parent/type/description/inuse/shared/private + until/unused-for/label
// ② toBuildkitPruneInfo 翻译:until/unused-for 转 KeepDuration,filter 转 BuildKit csv
// ③ errgroup 两路:
// - controller.Prune(PruneRequest{...}) → UsageRecord chan
// - 消费 chan 累计 size、cacheIDs
}
支持的 filter(cacheFields):
|--------------------------------------------------------------------|--------------------------------------|
| key | 含义 |
| id | 按 cache ID 过滤(转成 id~=<value> 模糊匹配) |
| parent / type / description / inuse / shared / private | 精确匹配(==) |
| until / unused-for | 时间过滤(互斥;都解析成"距今 duration") |
| label / label! | 标签过滤(TODO 未实现) |
| mutable / immutable | BuildKit 内部字段,不暴露 |
until 和 unused-for 是同义词,配置任意一个都行;同时传会报 errConflictFilter。
9.3 DiskUsage
Go
func (b *Builder) DiskUsage(ctx, options) (*buildbackend.DiskUsage, error) {
// 用 singleflight 防并发重复查询
return b.diskUsage.Do(ctx, options, func(ctx) (*buildbackend.DiskUsage, error) {
duResp, _ := b.controller.DiskUsage(ctx, &controlapi.DiskUsageRequest{})
var usage buildbackend.DiskUsage
for _, r := range duResp.Record {
usage.TotalCount++
usage.TotalSize += r.Size
if r.InUse { usage.ActiveCount++ }
if !r.InUse && !r.Shared { usage.Reclaimable += r.Size }
if options.Verbose {
usage.Items = append(usage.Items, build.CacheRecord{...})
}
}
return &usage, nil
})
}
singleflight.Group 让并发的 docker buildx du 请求共享同一次底层 RPC------避免对 BuildKit cache 的扫描被并发风暴打爆。
10. 与经典 v1 builder 的对照
|------------------------------------------|--------------------------------------------------------------------------------|------------------------------------------------------------------------------------------|
| 维度 | v1 (daemon/builder/dockerfile/) | BuildKit (daemon/internal/builder-next/) |
| 执行模型 | 顺序解释 Dockerfile,每条指令一个临时容器 | LLB DAG,按内容寻址、并行求解 |
| 缓存语义 | 严格线性,一次 miss 立即"熔断" | 按内容指纹,跨 stage / 跨 build 重用 |
| 网络 | 复用 docker run 的 NetworkMode | libnetwork bridge / host / none(受 entitlements 约束) |
| 进程模型 | dockerd 内嵌解释器 | dockerd 内嵌 BuildKit Controller(仍然同进程) |
| RUN 实现 | ContainerCreate + ContainerStart + ContainerWait 走 dockerd container API | runc 直跑(Linux)/ containerd executor(Windows) |
| 多阶段 | 顺序执行,前序 stage 提交成 image 才能被后面 COPY --from | 并行求解,前序 stage 不必先变成 image |
| 文件来源 | tar / git / url,统一在 remotecontext.Detect | tar(HTTP body)/ git / http / local session / oci-layout |
| Build context 来源 | HTTP body | HTTP body(通过 reqbodyhandler 转 URL)/ session 流 |
| Tag 时机 | 在 Backend.Build 末尾 tagImages | BuildKit exporter 直接打 tag(通过 ImageTagger 回调) |
| 取消粒度 | 每条指令执行之间 | BuildKit 在 DAG vertex 边界取消,更及时 |
| 输出形态 | 只能是 moby image | image / tar / local / oci-digest / cacheonly / registry push / dockerfile(schema 转 LLB)等 |
| 远端缓存 | 仅靠 --cache-from=<image> 串匹配 | gha / inline / local / registry,多种远端缓存后端 |
| 进度展示 | Step N/M : ... 文本 | 结构化 vertex/log/progress 流(progress UI) |
| 进度反馈通道 | 直接写到 stdout/stderr | aux 消息(moby.buildkit.trace) + moby.image.id |
| RUN --mount、--chmod、--link | ❌ | ✅ |
| 多平台 --platform=linux/amd64,linux/arm64 | ❌ | ✅ |
| SBOM / Provenance | ❌ | ✅ |
| 历史/审计 history.db | ❌ | ✅(history_c8d.db / history.db) |
11. 调用链总览(一图速查)
Go
HTTP POST /build (DOCKER_BUILDKIT=1, or ?version=2)
│
▼ buildbackend.Backend (daemon/builder/backend/backend.go::Backend.Build)
│
▼ daemon/internal/builder-next/builder.go::Builder.Build
│
├── [if BuildID/upload-request] buildJob.SetUpload / WaitUpload
│
├── 翻译 frontendAttrs(target/filename/context/build-arg/...)
├── 解析 exporterName(默认 "moby")+ tags
├── inline cache(BUILDKIT_INLINE_CACHE)
│
├── 构造 controlapi.SolveRequest {
│ Ref, Exporters, Frontend="dockerfile.v0",
│ FrontendAttrs, Session, Cache, Entitlements
│ }
│
└── errgroup 三路:
├── controller.Solve(ctx, req)
│ │
│ ▼ github.com/moby/buildkit/control
│ ├── 选 frontend: dockerfile.v0 → forwarder → gateway → dockerfile.Build
│ │ └─ LLB 求解(解析 AST → 构建 DAG → 内容寻址)
│ ├── 选 worker: mobyworker(graphdriver 或 containerd-snapshotter)
│ │ ├─ CacheManager (bbolt cache.db)
│ │ ├─ Snapshotter (adapters/snapshot 包 graphdriver)
│ │ ├─ SourceManager (containerimage / git / http / local)
│ │ │ └─ adapters/containerimage.Source
│ │ │ └─ moby download manager 拉取(与 docker pull 共享缓存)
│ │ └─ Executor (executor_linux.go::runcexecutor / windows::containerdexecutor)
│ │ ├─ networkProviders:
│ │ │ ├─ UNSET → bridgeProvider (libnetwork.Controller)
│ │ │ │ └─ lnInterface.Set: libnetwork-setkey reexec
│ │ │ ├─ HOST → hostProvider
│ │ │ └─ NONE → noneProvider
│ │ └─ runc run / containerd task
│ │
│ └── 选 exporter:
│ ├── "moby" → wrapper(imageExporter) → mobyexporter / containerd image store
│ │ ├─ unpack → moby layer store
│ │ ├─ content label "moby/build.ref.<ref>"
│ │ ├─ callback Exported → daemon.ImageExportedByBuildkit
│ │ └─ callback Named → daemon.ImageNamedByBuildkit
│ └── 其他:tar / local / oci-digest / registry ...
│
├── controller.Status(stream=statusProxy)
│ └─ 持续推送 StatusResponse → chan
│
└── aux.Emit("moby.buildkit.trace", proto(StatusResponse))
└─ 客户端 progress UI
最终 Build 返回 *builder.Result{ImageID: <containerimage.digest>}
12. 学习路径建议
按以下顺序读,效率最高:
builder.go::Builder.Build------看一遍这 200 行,BuildKit 适配层的所有"翻译规则"都在这。
controller.go::newGraphDriverController------经典 dockerd 模式下整套装配,最复杂但也最能体现"moby 把 BuildKit 嵌进自己"的设计。
executor_linux.go::newExecutor+executor.go::bridgeProvider------理解 RUN 怎么跑、网络怎么连。
worker/worker.go::NewWorker------Source/Cache/Exporter/Executor 的总装车间。
exporter/wrapper.go+mobyexporter/export.go------产物回写 moby image store 的两条路径。
adapters/snapshot/snapshot.go+adapters/containerimage/pull.go------更底层的"接口翻译",这是适配层的两个最大接口边界。
- 最后回到
daemon/command/daemon.go::initBuildkit------看 Opt 是怎么从Daemon上拿东西填进去的,整套装配上下文就闭合了。
读这套代码时建议手里备着 BuildKit 上游(github.com/moby/buildkit)的接口定义对照:control.Opt、worker.Worker、executor.Executor、source.Source、cache.Manager、exporter.Exporter。Moby 在这些接口之外做的事情很少,绝大部分逻辑都是"实现接口 + 注入依赖"。
13. 关键设计要点小结
- 进程内嵌而非旁路 :BuildKit 不是独立 daemon,而是作为 gRPC server/controller 嵌入 dockerd;客户端通过 dockerd 暴露的 socket 走 BuildKit 协议(
buildctl dial-stdio)也行。
- 两条后端路径 :containerd-snapshotter(新)与 graphdriver(旧),由
opt.UseSnapshotter决定,决定了 capabilities、exporter、cache 后端范围都不同。
- 网络交给 libnetwork :BuildKit 自带的 CNI/host 网络被 moby 替换,RUN 容器和
docker run共享同一个 libnetwork bridge/nat;通过OptionUseExternalKey+libnetwork-setkeyreexec 把网络命名空间注入 runc 创建的容器。
- 镜像拉取共享 :
adapters/containerimage.Source把 BuildKit 的 FROM 转给 moby 的 download manager,与docker pull同一层缓存,避免重复下载。
- GC 安全 :
imagerefchecker防止 BuildKit cache GC 误删 moby image store 仍在用的层;moby-danglingprefix 让匿名镜像有统一前缀便于清理。
- 取消的双轨制 :HTTP 客户端取消通过连接断开;带 BuildID 的取消走
b.jobs[id].cancel。
- AuxFormatter 双消息 :
moby.image.id(最终镜像 ID)+moby.buildkit.trace(StatusResponse 流)共同构成 BuildKit 模式的客户端可见输出。
- HTTP body → URL 巧妙桥接 :
reqBodyHandler是一个"假 HTTP 服务器",让 BuildKit 用普通 GET 就能拿到客户端上传的 build context,避免在 BuildKit 内部塞入 dockerd 的 IO 路径。
- 跨平台 stub:非 Linux/Windows 平台直接 stub executor,明确不支持------这也是为什么 macOS/FreeBSD 上 BuildKit 不可用,必须靠 Docker Desktop 的 Linux VM。
- Opt 即"Daemon 能力清单":BuildKit 通过 Opt 反向使用 dockerd 的镜像/网络/Registry/Cgroup/IdentityMapping/CDI 等所有能力,而非自己重建一套------这是嵌入式集成的关键设计。