moby-BuildKit(builder-next)

|-------------------------|----------------------------------------|----------------------------------|
| 章节 | 关键文件 | 解答 |
| 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 的执行引擎,而是:

  1. 把 BuildKit 的 control.Controllerworker.Workerexecutor.Executor 等组件装配进 dockerd 进程;
  1. adapters 把 BuildKit 期望的接口(content store / snapshotter / cache source / image source)桥接到 dockerd 自己的镜像/层存储;
  1. mobyexporter 把 BuildKit 算出的 OCI descriptor 写回 dockerd 的 image store;
  1. 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 | 常量定义(MobyBuildRefLabel) |
| 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.Newdaemon/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.dbcontainerdmeta.dbhistory*.dbcontent/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.jsonbuilder 段配置: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关键字toBuildkitExtraHostsextra-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():注释解释,"请求取消时只应取消构建本身,不应取消状态推送"。
  • 客户端拿到两条 auxmoby.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) 把读端塞进 waitChWaitUpload() 注册一个回调到 waitCh,回调被消费时拿到 rcwrapRC 在 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 自己写的一套大装配,干了好几件事:

  1. 从 LayerStore 拿 graphdriver.Driver
Go 复制代码
driver = dist.LayerStore.(interface{ Driver() graphdriver.Driver }).Driver()
  1. 建本地 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")
  1. 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)。

  1. 建 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,
})
  1. 建 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 pulldocker build 的 FROM 共享层。

  1. 建 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,
})
  1. 清理上次构建的 stale lease
Go 复制代码
leases, _ := lm.List(ctx, `labels."buildkit/lease.temporary"`)
for _, l := range leases { lm.Delete(ctx, l) }

避免 dockerd 重启后上次的临时层永远挂着。

  1. 统一装配 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 允许把构建逻辑放进容器里(自定义前端)。

  1. 交回 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.jsonbuilder.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.hostdevicesecurity.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.Workermobyworker.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.goruncexecutor.New | bridge(libnetwork)/ host / none | runc |
| Windows | executor_windows.gocontainerdexecutor.New | nat(libnetwork)/ none | containerd |
| 其他 | executor_others.go + executor_nolinux.gostubExecutor(直接返回 "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
    }
}

它的工作机制:

  1. BuildKit 准备 GC 某个 ref 前会调 ExternalRefChecker.IsYetVisible(blobKey, ...)
  1. checker 把 moby image store 中所有镜像的 layer chain 装进一棵 lchain 前缀树;
  1. 查询: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 内部字段,不暴露 |

untilunused-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. 学习路径建议

按以下顺序读,效率最高:

  1. builder.go::Builder.Build------看一遍这 200 行,BuildKit 适配层的所有"翻译规则"都在这。
  1. controller.go::newGraphDriverController------经典 dockerd 模式下整套装配,最复杂但也最能体现"moby 把 BuildKit 嵌进自己"的设计。
  1. executor_linux.go::newExecutor+ executor.go::bridgeProvider------理解 RUN 怎么跑、网络怎么连。
  1. worker/worker.go::NewWorker------Source/Cache/Exporter/Executor 的总装车间。
  1. exporter/wrapper.go+ mobyexporter/export.go------产物回写 moby image store 的两条路径。
  1. adapters/snapshot/snapshot.go+ adapters/containerimage/pull.go------更底层的"接口翻译",这是适配层的两个最大接口边界。
  1. 最后回到 daemon/command/daemon.go::initBuildkit------看 Opt 是怎么从 Daemon 上拿东西填进去的,整套装配上下文就闭合了。

读这套代码时建议手里备着 BuildKit 上游(github.com/moby/buildkit)的接口定义对照:control.Optworker.Workerexecutor.Executorsource.Sourcecache.Managerexporter.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-setkey reexec 把网络命名空间注入 runc 创建的容器。
  • 镜像拉取共享adapters/containerimage.Source 把 BuildKit 的 FROM 转给 moby 的 download manager,与 docker pull 同一层缓存,避免重复下载。
  • GC 安全imagerefchecker 防止 BuildKit cache GC 误删 moby image store 仍在用的层;moby-dangling prefix 让匿名镜像有统一前缀便于清理。
  • 取消的双轨制 :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 等所有能力,而非自己重建一套------这是嵌入式集成的关键设计。
相关推荐
风曦Kisaki1 小时前
Kubernetes(K8s)笔记Day04:控制器(ReplicaSet 与Deployment),滚动更新及回滚,滚动更新策略,Pod 的 DNS 策略
linux·运维·笔记·docker·容器·kubernetes
惠惠软件1 小时前
快速生成磁盘目录文件-双击运行a.bat ,可以看到磁盘目录下出现了 文件目录.txt-供大家学习研究参考
windows·学习·文件列表
星恒随风1 小时前
C++ STL 详解:set 与 multiset 的使用、区间查询和算法应用
开发语言·c++·笔记·学习·算法
张忠琳1 小时前
【NVIDIA】k8s-device-plugin v0.19.3 辅助命令模块深度分析之七
云原生·容器·架构·kubernetes·nvidia
m4Rk_1 小时前
【论文阅读】Agent 记忆机制(20):RecMem——只在信息反复出现时进行长期记忆巩固
论文阅读·人工智能·学习·开源·github
爱码少年1 小时前
此docker compose非彼docker-compose
docker
菩提树下的打坐1 小时前
测试工程与 DevOps / SRE 的边界:三方协作的真实工作流
学习
幸福在路上wellbeing1 小时前
AI 智能体开发 · Day 2 详细学习手册
人工智能·学习
网络安全零基础教程1 小时前
零基础转行网安,前两个月具体该学什么工具
网络·学习·安全·web安全·网络安全