镜像服务门面模式

全局视角:为什么要看这两个文件

Docker 镜像管理正在经历历史性的后端迁移:

  • 老后端 :daemon/images/ ------ 基于自研的 graph driver (overlay2/vfs/windowsfilter),自管 layerStore / imageStore / refStore。
  • 新后端 :daemon/containerd/ ------ 直接复用 containerd 的 image store + content store + snapshotter,新部署的默认路径。

两个后端实现了完全相同的接口 (daemon.ImageService),所以 daemon 主流程、API 路由、CLI 命令完全感知不到后端切换 。这就是经典的门面模式(Facade)+ 策略模式(Strategy)

学习这两个文件的价值:

  1. 理解"接口隔离" :daemon 主流程不依赖具体后端,只依赖 ImageService 接口,这是 docker 能完成这种大规模重构的关键。
  1. 理解"门面"的意义 :images.ImageServicecontainerd.ImageService 各自都是一组复杂子系统的门面------对外暴露简化的 API,对内组合 imageStore/layerStore/snapshotter/contentStore 等。
  1. 理解"迁移代价" :两个实现各 1000+ 行,如何保证行为等价是工程上的核心挑战。

先读 daemon/image_service.go:27-77 的接口定义(只有 50 行,列出了所有镜像操作),建立"门面契约"的全景认知,然后再分别看两个实现。

1. 门面契约:daemon.ImageService 接口

源码位置:daemon/image_service.go:27-77

接口被注释明确标为临时接口:

scss 复制代码
// ImageService is a temporary interface to assist in the migration to the
// containerd image-store. This interface should not be considered stable,
// and may change over time.
type ImageService interface {
    // ===== Images =====
    PullImage(...)
    PushImage(...)
    CreateImage(...)
    ImageDelete(...)
    ExportImage(...)
    LoadImage(...)
    Images(...)
    LogImageEvent(...)
    CountImages(...)
    ImagePrune(...)
    ImportImage(...)
    TagImage(...)
    GetImage(...)
    ImageHistory(...)
    CommitImage(...)
    SquashImage(...)
    ImageInspect(...)
    ImageDiskUsage(...)

    // ===== Layers =====
    GetImageAndReleasableLayer(...)
    CreateLayer(...)
    CreateLayerFromImage(...)
    GetLayerByID(...)
    LayerStoreStatus()
    GetLayerMountID(...)
    ReleaseLayer(...)
    GetContainerLayerSize(...)
    Changes(...)

    // ===== Windows =====
    GetLayerFolders(...)

    // ===== Build =====
    MakeImageCache(...)
    CommitBuildStep(...)

    // ===== Other =====
    DistributionServices()
    Children(...)
    Cleanup()
    StorageDriver()
    UpdateConfig(...)
}

1.1 接口的五个语义分组

分组 方法数 关注点
Images 17 镜像本身的 CRUD(pull/push/list/delete/tag/inspect/history/...)
Layers 9 容器的可写层 + 镜像层管理(create/release/changes)
Windows 1 Windows 平台特有(GetLayerFolders)
Build 2 镜像构建相关(image cache、commit step)
Other 5 元信息查询(driver/children/cleanup/config)

1.2 "Temporary Interface" 的工程含义

注释说**"should not be considered stable"**------这反映了 docker 团队的取舍:

  • 老后端的某些操作(如 SquashImageGetLayerMountID)在 containerd 后端没有自然对应,可能在未来版本被移除或重设计。
  • 接口在迁移期会演化,调用方不应该把它当稳定 API 依赖。
  • 但 daemon 主流程依然依赖这个接口,这本身就是 "用抽象换取演进空间" 的实践。

2. 老后端门面:images.ImageService

源码位置:daemon/images/service.go:74-88

go 复制代码
type ImageService struct {
    containers                containerStore              // 容器存储(用于冲突检测)
    distributionMetadataStore metadata.Store              // push/pull 元数据
    downloadManager           *xfer.LayerDownloadManager  // 并发下载管理
    eventsService             *daemonevents.Events        // 事件总线
    imageStore                image.Store                 // 镜像元数据存储
    layerStore                layer.Store                 // 层存储(graph driver 入口)
    pruneRunning              atomic.Bool                 // 防并发 prune
    referenceStore            refstore.Store              // 镜像引用(tag/digest)
    registryService           distribution.RegistryResolver
    uploadManager             *xfer.LayerUploadManager    // 并发上传管理
    leases                    leases.Manager              // containerd lease(共享用)
    content                   content.Store               // content store(共享用)
    contentNamespace          string
}

2.1 构造:NewImageService

源码位置:daemon/images/service.go:52-71

yaml 复制代码
func NewImageService(ctx context.Context, config ImageServiceConfig) *ImageService {
    log.G(ctx).Debugf("Max Concurrent Downloads: %d", config.MaxConcurrentDownloads)
    ...

    return &ImageService{
        containers:                config.ContainerStore,
        distributionMetadataStore: config.DistributionMetadataStore,
        downloadManager:           xfer.NewLayerDownloadManager(config.LayerStore, config.MaxConcurrentDownloads, xfer.WithMaxDownloadAttempts(config.MaxDownloadAttempts)),
        eventsService:             config.EventsService,
        imageStore:                &imageStoreWithLease{
            Store:  config.ImageStore,
            leases: config.Leases,
            ns:     config.ContentNamespace,
        },
        layerStore:       config.LayerStore,
        referenceStore:   config.ReferenceStore,
        registryService:  config.RegistryService,
        uploadManager:    xfer.NewLayerUploadManager(config.MaxConcurrentUploads),
        leases:           config.Leases,
        content:          config.ContentStore,
        contentNamespace: config.ContentNamespace,
    }
}

2.2 imageStoreWithLease:装饰器模式

注意 imageStore: &imageStoreWithLease{...} 这一行------image.Store装饰 了一层 lease 管理。这就是经典的装饰器模式:

  • 原始 image.Store 只负责镜像元数据读写。
  • imageStoreWithLease 在外面套了一层:所有写入操作都自动包一个 containerd lease,保证元数据生命周期和 content 生命周期一致。

门面 + 装饰器:门面把多个 store 组合成统一接口;装饰器在不改 Store 接口的前提下增加 lease 行为。两个模式叠加,实现了"调用方无感知"的 lease 管理。

2.3 老后端的依赖关系

scss 复制代码
                  ┌──────────────────────────────────────┐
                  │      images.ImageService (门面)      │
                  └──────────────────────────────────────┘
                              │
        ┌──────────┬──────────┼──────────┬──────────┬──────────┐
        ▼          ▼          ▼          ▼          ▼          ▼
   imageStore  layerStore  refStore  downloadMgr uploadMgr  eventsService
   (元数据)    (graphdrv)  (tag)    (xfer)     (xfer)    (events)
        │          │
        │          ▼
        │     overlay2/aufs/vfs/windowsfilter
        │     (graphdriver 接口)
        │
        └─→ content store + leases (containerd 提供,但仅用于跨进程共享)

特点 :docker 自己实现的 layer/image 存储;containerd 的 content/leases 只是辅助。

2.4 老后端的"门面感"体现在哪里

CountImages(service.go:112):

go 复制代码
func (i *ImageService) CountImages(ctx context.Context) int {
    return i.imageStore.Len()
}

一行代码。这是因为 imageStore 已经做了所有工作,门面只是个转发。

再看 CreateLayer(service.go:126):

go 复制代码
func (i *ImageService) CreateLayer(container *container.Container, initFunc layer.MountInit) (container.RWLayer, error) {
    var img *image.Image
    if container.ImageID != "" {
        containerImg, err := i.imageStore.Get(container.ImageID)
        if err != nil {
            return nil, err
        }
        img = containerImg
    }

    rwLayerOpts := &layer.CreateRWLayerOpts{
        MountLabel: container.MountLabel,
        InitFunc:   initFunc,
        StorageOpt: container.HostConfig.StorageOpt,
    }

    return i.CreateLayerFromImage(img, container.ID, rwLayerOpts)
}

这里门面"组装"了三个 store 的协作:

  1. imageStore 拿镜像(根据 container.ImageID)。
  1. 从镜像的 RootFS 算出 chainID。
  1. 用 chainID 调 layerStore.CreateRWLayer 创建可写层。

调用方只看到 CreateLayer(container, initFunc) 一个简单签名,内部却串起了三个子系统。这就是门面的核心价值


3. 新后端门面:containerd.ImageService

源码位置:daemon/containerd/service.go:32-50

go 复制代码
type ImageService struct {
    client              *containerd.Client                       // containerd gRPC client
    images              c8dimages.Store                          // containerd 镜像存储
    content             content.Store                            // content store(blob 存储)
    containers          container.Store                          // 容器存储
    snapshotterServices map[string]snapshots.Snapshotter         // 多 snapshotter 支持
    snapshotter         string                                   // 默认 snapshotter(如 overlayfs)
    registryHosts       docker.RegistryHosts                     // registry 解析
    registryService     distribution.RegistryResolver
    eventsService       *daemonevents.Events
    pruneRunning        atomic.Bool
    refCountMounter     snapshotter.Mounter                      // 引用计数 mount
    idMapping           user.IdentityMapping                     // 用户/组 ID 映射
    policyVerifier      func() (*policyverifier.Verifier, error) // 镜像策略校验
    identity            imageIdentityState                       // 镜像身份缓存
    defaultPlatformOverride platforms.MatchComparer               // 测试用,可覆盖默认平台
}

3.1 构造:NewService

源码位置:daemon/containerd/service.go:66-94

arduino 复制代码
func NewService(config ImageServiceConfig) *ImageService {
    service := &ImageService{
        client:  config.Client,
        images:  config.Client.ImageService(),       // ★ 从 client 直接拿
        content: config.Client.ContentStore(),       // ★ 同上
        snapshotterServices: map[string]snapshots.Snapshotter{
            config.Snapshotter: config.Client.SnapshotService(config.Snapshotter),  // ★ 同上
        },
        containers:      config.Containers,
        snapshotter:     config.Snapshotter,
        registryHosts:   config.RegistryHosts,
        registryService: config.Registry,
        eventsService:   config.EventsService,
        refCountMounter: config.RefCountMounter,
        idMapping:       config.IDMapping,
        policyVerifier:  config.PolicyVerifierProvider,
        identity: imageIdentityState{
            cache: make(map[string]imageIdentityCacheEntry),
            cacheStore: func() identitycache.Backend {
                if config.IdentityCacheBackend != nil {
                    return config.IdentityCacheBackend
                }
                return identitycache.NewNopBackend()
            }(),
        },
    }
    service.startImageIdentityCacheRefresh()   // ★ 启动后台 goroutine 刷新身份缓存
    return service
}

和老后端的关键差异:

  • 不再持有 imageStore / layerStore / referenceStore 三个独立 store。
  • 统一通过 containerd.Client 获取所有底层服务(ImageService/ContentStore/SnapshotService)。
  • 多了 identity(镜像身份缓存)------因为 containerd 的镜像 ID 体系(digest)和 docker 历史 ID(随机 sha256)不同,需要缓存做映射。
  • 多了 policyVerifier------支持镜像拉取策略(OPA/契约校验)。

3.2 snapshotterService:多 snapshotter 懒加载

源码位置:daemon/containerd/service.go:96-104

go 复制代码
func (i *ImageService) snapshotterService(snapshotter string) snapshots.Snapshotter {
    s, ok := i.snapshotterServices[snapshotter]
    if !ok {
        s = i.client.SnapshotService(snapshotter)
        i.snapshotterServices[snapshotter] = s   // 缓存到 map
    }
    return s
}

设计意图 :支持容器使用非默认 snapshottersnapshotterServices 是个 map,首次访问某 snapshotter 时才创建 client,后续直接复用。

3.3 新后端的依赖关系

scss 复制代码
                ┌──────────────────────────────────────┐
                │   containerd.ImageService (门面)     │
                └──────────────────────────────────────┘
                            │
              ┌─────────────┴──────────────┐
              ▼                            
        containerd.Client (gRPC)          
              │                           
       ┌──────┼──────┬──────┬──────┐     
       ▼      ▼      ▼      ▼      ▼     
   images  content  snapshots containers leases  
   .Store  .Store   (overlayfs) .Store   .Manager
       │      
       ▼      
   containerd 的 boltdb + blob 存储

特点 :所有存储都委托给 containerd 守护进程,docker daemon 只是个 client。


4. 同一接口、两种实现:对照表

把两个 service.go 的方法实现并排放,差异一目了然:

方法 images.ImageService(老) containerd.ImageService(新)
CountImages i.imageStore.Len()(O(1)) i.client.ListImages + 去重(网络调用)
LayerStoreStatus i.layerStore.DriverStatus()(driver 详情) 硬编码 [{"driver-type", "SnapshotPlugin"}]
GetLayerMountID i.layerStore.GetMountID(cid) NotImplemented
Cleanup i.layerStore.Cleanup() stopImageIdentityCacheRefresh + cacheStore.Close
StorageDriver i.layerStore.DriverName()(overlay2/vfs/...) i.snapshotter(overlayfs/...)
ImageDiskUsage 遍历 layerStore + 算 chainID 引用 snapshotter.Walk + 累加 image content size
UpdateConfig 调 download/uploadManager.SetConcurrency 仅打日志 "not yet implemented"
GetContainerLayerSize (老后端有自己实现) snapshotter.Usage + 算父链大小
DistributionServices 返回真实下载管理器等 返回空 struct dimages.DistributionServices{}

4.1 关键差异解读

(1) CountImages:O(1) vs O(N) 网络调用

go 复制代码
// 老后端:本地内存 map 的 Len()
func (i *ImageService) CountImages(ctx context.Context) int {
    return i.imageStore.Len()
}

// 新后端:走 containerd gRPC,然后客户端去重
func (i *ImageService) CountImages(ctx context.Context) int {
    imgs, err := i.client.ListImages(ctx)
    ...
    uniqueImages := map[digest.Digest]struct{}{}
    for _, i := range imgs {
        dgst := i.Target().Digest
        if _, ok := uniqueImages[dgst]; !ok {
            uniqueImages[dgst] = struct{}{}
        }
    }
    return len(uniqueImages)
}

为什么新后端要去重? containerd 的 images 表存的是"引用"(name → descriptor),同一个镜像可能被多个 tag 引用。docker 的"镜像数"语义是"独立镜像数",所以按 digest 去重。

(2) LayerStoreStatus:详情 vs 占位符

scss 复制代码
// 老后端:返回 overlay2/vfs 等驱动的 BackingFS、SupportsDType 等
func (i *ImageService) LayerStoreStatus() [][2]string {
    return i.layerStore.DriverStatus()
}

// 新后端:只有一个简陋的占位符
func (i *ImageService) LayerStoreStatus() [][2]string {
    // TODO(thaJeztah) do we want to add more details about the driver here?
    return [][2]string{
        {"driver-type", string(plugins.SnapshotPlugin)},
    }
}

这反映了抽象层级的变化:老后端能直接访问 graphdriver 的内部状态;新后端只把 containerd 当黑盒,拿不到 snapshotter 的实现细节。

(3) GetLayerMountID:直接拒绝

go 复制代码
// 新后端
func (i *ImageService) GetLayerMountID(cid string) (string, error) {
    return "", errdefs.NotImplemented(errors.New("not implemented"))
}

注释说 "needs to be refactored to Unmount"。这种"先 NotImplemented,等调用方重构"的策略是迁移期的常见做法。

(4) DistributionServices:返回空

go 复制代码
// 新后端
func (i *ImageService) DistributionServices() dimages.DistributionServices {
    return dimages.DistributionServices{}
}

老后端把内部的下载管理器等暴露给 builder 用,新后端不再支持这种紧耦合,builder 必须改用其他方式。


5. 同一方法的两套实现:PullImage 深度对比

最能体现"门面+策略"的例子是 PullImage。两个实现签名完全相同:

scss 复制代码
PullImage(ctx context.Context, ref reference.Named, options imagebackend.PullOptions) error

5.1 老后端的 PullImage(images/image_pull.go:40)

go 复制代码
func (i *ImageService) PullImage(ctx context.Context, ref reference.Named, options imagebackend.PullOptions) error {
    if len(options.Platforms) > 1 {
        return cerrdefs.ErrInvalidArgument.WithMessage("multiple platforms is not supported")
    }
    ...
    // 调 docker 自己的 distribution.Pull(自己实现 pull 协议)
    err := i.pullImageWithReference(ctx, ref, platform, options.MetaHeaders, options.AuthConfig, options.OutStream)
    ...
}

特点:

  • distribution.Pull,这是 docker 自己实现的 Docker Registry HTTP API V2 客户端。
  • 拉取的层写入 layerStore(graph driver 管理)。
  • 镜像元数据写入 imageStore + referenceStore
  • 进度通过 channel 异步写到 OutStream。

5.2 新后端的 PullImage(containerd/image_pull.go:64)

go 复制代码
func (i *ImageService) PullImage(ctx context.Context, baseRef reference.Named, options imagebackend.PullOptions) (retErr error) {
    if len(options.Platforms) > 1 {
        return cerrdefs.ErrInvalidArgument.WithMessage("multiple platforms is not supported")
    }
    ...
    // ★ 用 containerd lease 把所有 content 绑到临时 lease
    ctx, done, err := i.withLease(ctx, true)
    if err != nil {
        return err
    }
    defer done()

    if !reference.IsNameOnly(baseRef) {
        return i.pullTag(ctx, baseRef, platform, ...)   // 拉 tag
    }

    // 不带 tag:先列所有 tag,再逐个拉
    tags, err := distribution.Tags(ctx, baseRef, ...)
    for _, tag := range tags {
        ref, _ := reference.WithTag(baseRef, tag)
        i.pullTag(ctx, ref, platform, ...)
    }
    return nil
}

特点:

  • containerd lease 把本次拉取涉及的所有 content(manifest/config/layer)绑到临时 lease 上。
  • 失败时 lease 自动释放 → content 被 GC;成功时把 lease 升级成持久引用。
  • pullTag 内部最终调 containerd.Client.Pull,containerd 守护进程负责实际 HTTP 通信和层下载。
  • 不带 tag 时(docker pull redis),先列所有 tag,再循环 pullTag。

5.3 对照分析

维度 老后端 新后端
协议实现 docker 自管的 distribution 包 containerd 守护进程
层存储 layerStore (graph driver) snapshotter (overlayfs 等)
失败回滚 手动清理已下载层 containerd lease 自动 GC
并发控制 xfer.LayerDownloadManager containerd 内部调度
多 tag 拉取 单次 distribution.Pull 处理 显式循环调 pullTag
进度上报 写 OutStream 写 OutStream(签名一致)

最关键的差异是 lease 机制:新后端把"原子性"下沉到 containerd 的 content store,失败时不需要应用层做复杂回滚。这是抽象层级的提升。


6. 门面内部的"小门面":ImageManifest / DistributionServices

门面里还有更细粒度的子门面,体现分层抽象的设计:

6.1 imageIdentityState:身份映射的子门面

源码位置:containerd/service.go:82-90(嵌入字段)

go 复制代码
identity: imageIdentityState{
    cache: make(map[string]imageIdentityCacheEntry),
    cacheStore: func() identitycache.Backend {
        if config.IdentityCacheBackend != nil {
            return config.IdentityCacheBackend
        }
        return identitycache.NewNopBackend()
    }(),
},

什么需要这层? containerd 用镜像 manifest 的 digest 作为 ID,但 docker 历史 API 用随机 sha256 作为 image.ID。两者的映射需要持久化(重启后还要能恢复),所以引入 identitycache.Backend

Cleanup 方法(service.go:148-154)关闭的就是这个 cache:

go 复制代码
func (i *ImageService) Cleanup() error {
    i.stopImageIdentityCacheRefresh()
    if i.identity.cacheStore != nil {
        return i.identity.cacheStore.Close()
    }
    return nil
}

6.2 DistributionServices:配置的子门面

源码位置:images/service.go:91-108

go 复制代码
type DistributionServices struct {
    DownloadManager   *xfer.LayerDownloadManager
    V2MetadataService metadata.V2MetadataService
    LayerStore        layer.Store
    ImageStore        image.Store
    ReferenceStore    refstore.Store
}

func (i *ImageService) DistributionServices() DistributionServices {
    return DistributionServices{
        DownloadManager:   i.downloadManager,
        V2MetadataService: metadata.NewV2MetadataService(i.distributionMetadataStore),
        LayerStore:        i.layerStore,
        ImageStore:        i.imageStore,
        ReferenceStore:    i.referenceStore,
    }
}

用途:builder 在 build 时需要直接访问 layer/image store,不能走门面的高级 API。所以门面提供一个"逃生舱口",一次性把这些 store 打包给 builder。

新后端直接返回空(dimages.DistributionServices{}),意味着 builder 必须改造------这是迁移的代价之一。


7. 配置对象:ImageServiceConfig

两个实现的构造函数都用 config struct,这是一种参数对象模式:

7.1 老后端 config(images/service.go:35-49)

go 复制代码
type ImageServiceConfig struct {
    ContainerStore            containerStore
    DistributionMetadataStore metadata.Store
    EventsService             *daemonevents.Events
    ImageStore                image.Store
    LayerStore                layer.Store
    MaxConcurrentDownloads    int
    MaxConcurrentUploads      int
    MaxDownloadAttempts       int
    ReferenceStore            refstore.Store
    RegistryService           distribution.RegistryResolver
    ContentStore              content.Store
    Leases                    leases.Manager
    ContentNamespace          string
}

13 个字段,大部分是 store。

7.2 新后端 config(containerd/service.go:52-63)

go 复制代码
type ImageServiceConfig struct {
    Client                 *containerd.Client
    Containers             container.Store
    Snapshotter            string
    IdentityCacheBackend   identitycache.Backend
    RegistryHosts          docker.RegistryHosts
    Registry               distribution.RegistryResolver
    EventsService          *daemonevents.Events
    RefCountMounter        snapshotter.Mounter
    IDMapping              user.IdentityMapping
    PolicyVerifierProvider func() (*policyverifier.Verifier, error)
}

11 个字段,关键字段是 Client(所有 store 都从它派生)。

7.3 配置对象的优点

  • 可扩展:加字段不破坏调用方。
  • 自文档:字段名比位置参数清晰。
  • 可选性 :零值字段可以用默认值(看 IdentityCacheBackend 为 nil 时退回 nop)。
  • 构造函数简洁 :NewXxxService(config) 一个参数。

8. 门面里的小方法:逐个分析

挑几个能体现"门面价值"的小方法细看:

8.1 CountImages:简单转发 vs 网络调用

go 复制代码
// images/service.go:112
func (i *ImageService) CountImages(ctx context.Context) int {
    return i.imageStore.Len()
}

// containerd/service.go:113
func (i *ImageService) CountImages(ctx context.Context) int {
    imgs, err := i.client.ListImages(ctx)
    if err != nil {
        return 0
    }

    uniqueImages := map[digest.Digest]struct{}{}
    for _, i := range imgs {
        dgst := i.Target().Digest
        if _, ok := uniqueImages[dgst]; !ok {
            uniqueImages[dgst] = struct{}{}
        }
    }
    return len(uniqueImages)
}

门面意义 :调用方 info.gocount := imageService.CountImages(ctx),完全不关心是查本地 map 还是发 gRPC。

8.2 ImageDiskUsage:不同算法

go 复制代码
// images/service.go:210(老后端)
func (i *ImageService) ImageDiskUsage(ctx context.Context) (int64, error) {
    var allLayersSize int64
    layerRefs := i.getLayerRefs()
    allLayers := i.layerStore.Map()
    for _, l := range allLayers {
        ...
        if _, ok := layerRefs[l.ChainID()]; ok {
            allLayersSize += size
        }
    }
    return allLayersSize, nil
}

// containerd/service.go:164(新后端)
func (i *ImageService) ImageDiskUsage(ctx context.Context) (int64, error) {
    diskUsage, err := i.layerDiskUsage(ctx)   // 遍历 snapshotter
    if err != nil {
        return 0, err
    }
    imgs, err := i.images.List(ctx)
    ...
    for _, img := range imgs {
        i.walkPresentChildren(ctx, img.Target, func(ctx context.Context, desc ocispec.Descriptor) error {
            ...
            diskUsage += desc.Size
            return nil
        })
    }
    return diskUsage, nil
}

算法差异:

  • 老后端:只算 layerStore 里的层 + chainID 引用过滤。
  • 新后端:snapshotter 占用 + content blob 大小

两者结果不一定相等(比如新后端还会算 manifest/config 这些小 blob 的大小,老后端忽略)。

8.3 Cleanup:不同的清理对象

go 复制代码
// images/service.go:177(老后端)
func (i *ImageService) Cleanup() error {
    if err := i.layerStore.Cleanup(); err != nil {
        return errors.Wrap(err, "error during layerStore.Cleanup()")
    }
    return nil
}

// containerd/service.go:148(新后端)
func (i *ImageService) Cleanup() error {
    i.stopImageIdentityCacheRefresh()
    if i.identity.cacheStore != nil {
        return i.identity.cacheStore.Close()
    }
    return nil
}

关键区别:

  • 老后端:清理 layerStore(graph driver 的临时 mount 等)。
  • 新后端:清理本地缓存(identity cache),不清理 containerd 那边(containerd 是独立守护进程,自己管自己的生命周期)。

9. 门面背后的工程模式总结

9.1 五个模式叠加

ruby 复制代码
┌─────────────────────────────────────────────────────────┐
│  ① Facade(门面):ImageService 接口统一 30+ 操作         │
│     └─ 入口:daemon/image_service.go                    │
├─────────────────────────────────────────────────────────┤
│  ② Strategy(策略):两个可替换实现                      │
│     ├─ images.ImageService   (老:graph driver)         │
│     └─ containerd.ImageService(新:containerd)          │
├─────────────────────────────────────────────────────────┤
│  ③ Decorator(装饰器):imageStoreWithLease 等           │
│     └─ 在不修改 Store 接口的前提下增加 lease 行为        │
├─────────────────────────────────────────────────────────┤
│  ④ Builder(构造):ImageServiceConfig 参数对象          │
│     └─ 13/11 个字段的自文档化构造方式                   │
├─────────────────────────────────────────────────────────┤
│  ⑤ Adapter(适配):NotImplemented + 空返回              │
│     └─ 老接口里不适用新后端的方法用 NotImplemented 兜底 │
└─────────────────────────────────────────────────────────┘

9.2 设计原则验证

  • 依赖倒置原则(DIP) :daemon 主流程依赖 ImageService 接口,不依赖具体实现。
  • 开闭原则(OCP) :加新后端(如未来可能加 buildkit 后端)不需要改 daemon 主流程。
  • 单一职责原则(SRP) :每个 image_xxx.go 文件只处理一个操作(pull/push/list/delete)。
  • 接口隔离原则(ISP) :imagebackend 包还细分了 imageBackend/importExportBackend/registryBackend 等小接口(daemon/server/router/image/backend.go),路由层按需组合。

10. 学习路径建议

10.1 推荐阅读顺序

  1. daemon/image_service.go:27-77 ------ 50 行的接口定义,先把契约刻进脑子。
  1. daemon/server/imagebackend/image.go ------ 看 PullOptions/PushOptions/RemoveOptions 等参数对象。
  1. daemon/server/router/image/backend.go ------ 看路由层怎么用接口隔离原则细分 Backend。
  1. daemon/images/service.go ------ 老后端门面(本文档主入口)。
  1. daemon/containerd/service.go ------ 新后端门面(本文档对照实现)。
  1. 挑一个操作做对照阅读 :推荐 PullImage,看 daemon/images/image_pull.go vs daemon/containerd/image_pull.go
  1. 挑一个简单方法看差异 :如 CountImagesCleanupLayerStoreStatus
  1. 挑一个 NotImplemented 方法 :如 GetLayerMountID,理解迁移代价。

10.2 配读文件

文件 作用
daemon/image_service.go 接口定义(契约)
daemon/server/imagebackend/image.go 选项参数对象
daemon/server/router/image/backend.go 路由层的接口隔离
daemon/images/image_*.go 老后端各操作实现(20+ 文件)
daemon/containerd/image_*.go 新后端各操作实现(20+ 文件)
daemon/containerd/leases.go 新后端的 lease 机制
daemon/containerd/resolver.go 新后端的 registry resolver
daemon/containerd/image_manifest.go ImageManifest 子门面
daemon/internal/distribution/ docker 自实现的 pull/push 协议(老后端用)
daemon/internal/image/ image.Store 接口(老后端用)
daemon/internal/layer/ layer.Store 接口(老后端用)
daemon/snapshotter/ snapshotter 抽象(新后端用)
相关推荐
一缕清烟在人间6 小时前
CalendarYearPage 年视图:12 个月微型日历网格
后端
xianjixiance_6 小时前
月份切换与年份导航:prevMonth / nextMonth 逻辑
后端
花开彼岸天~6 小时前
有日记日期标记:小圆点 + 加粗字重 + 点击跳转
后端
AskHarries6 小时前
Google 登录配置
后端
全堆鸿蒙7 小时前
时间轴组件:Circle 节点 + 竖线连接 + 内容列表
后端
爸爸6197 小时前
WindowStage 窗口管理:透明状态栏与沉浸式布局
后端
进击的前栈7 小时前
Grid 双栏网格与 List 单栏列表的视图切换实现
后端
xianjixiance_7 小时前
@Entry 与 @Component 装饰器:组件声明、导出与页面入口
后端
w139548564228 小时前
Shadow 阴影体系:多层级阴影提升视觉层次
后端