全局视角:为什么要看这两个文件
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) 。
学习这两个文件的价值:
- 理解"接口隔离" :daemon 主流程不依赖具体后端,只依赖
ImageService接口,这是 docker 能完成这种大规模重构的关键。
- 理解"门面"的意义 :
images.ImageService和containerd.ImageService各自都是一组复杂子系统的门面------对外暴露简化的 API,对内组合 imageStore/layerStore/snapshotter/contentStore 等。
- 理解"迁移代价" :两个实现各 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 团队的取舍:
- 老后端的某些操作(如
SquashImage、GetLayerMountID)在 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 的协作:
- 从
imageStore拿镜像(根据 container.ImageID)。
- 从镜像的 RootFS 算出 chainID。
- 用 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
}
设计意图 :支持容器使用非默认 snapshotter 。snapshotterServices 是个 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.go 写 count := 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 推荐阅读顺序
daemon/image_service.go:27-77------ 50 行的接口定义,先把契约刻进脑子。
daemon/server/imagebackend/image.go------ 看 PullOptions/PushOptions/RemoveOptions 等参数对象。
daemon/server/router/image/backend.go------ 看路由层怎么用接口隔离原则细分 Backend。
daemon/images/service.go------ 老后端门面(本文档主入口)。
daemon/containerd/service.go------ 新后端门面(本文档对照实现)。
- 挑一个操作做对照阅读 :推荐
PullImage,看daemon/images/image_pull.govsdaemon/containerd/image_pull.go。
- 挑一个简单方法看差异 :如
CountImages、Cleanup、LayerStoreStatus。
- 挑一个 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 抽象(新后端用) |