containerd/metadata DB · 资源垃圾回收

沿镜像与快照引用图回收无用资源

containerd 从镜像、有效租约等根资源出发,用显式栈遍历引用图;保留可达内容与快照,再扫描并清理不可达元数据。

v2.0.0存储与数据放置核验 2026-10-12

工程问题#

镜像层和快照会被多个镜像或容器共享,下载中的内容还可能由租约临时保护。删除一个镜像时立即递归删除它引用的对象,会误删仍被其他根引用的数据;只看对象年龄也无法判断是否仍在使用。

实际实现#

GarbageCollect 先取得元数据写锁,getMarked 在一致的数据库视图中收集根,并把资源引用解析函数交给 gc.Tricolor。灰色栈保存已发现但未完成的节点,初始根原样入栈,seen 对随后展开的引用去重;每次弹出一个节点、展开其引用,再写入 reachable。随后在数据库更新事务中扫描全部资源,跳过已标记者,删除其余元数据并登记需要清理的后端。内容与 snapshotter 的实际清理另行执行,不能把标记完成等同于磁盘空间已经释放。

处理流程#

  1. 取得元数据写锁,收集未过期镜像、租约保护对象等根资源
  2. 将根放入显式栈,并标记为已发现
  3. 弹出节点,通过类型、命名空间和键读取其引用,把未发现的目标压栈
  4. 完成所有可达节点后,在事务中扫描全体资源,保留 marked 集合成员
  5. 删除不可达元数据,触发内容库与 snapshotter 的后续清理

源码片段#

固定版本 · 原文摘录
core/metadata/db.gov2.0.0 · L371–L390
查看原文件
371func (m *DB) GarbageCollect(ctx context.Context) (gc.Stats, error) {372	m.wlock.Lock()373	t1 := time.Now()374	c := startGCContext(ctx, m.collectors)375376	marked, err := m.getMarked(ctx, c) // Pass in gc context377	if err != nil {378		m.wlock.Unlock()379		c.cancel(ctx)380		return nil, err381	}382383	events := []namespacedEvent{}384	if err := m.db.Update(func(tx *bolt.Tx) error {385		ctx, cancel := context.WithCancel(ctx)386		defer cancel()387388		rm := func(ctx context.Context, n gc.Node) error {389			if _, ok := marked[n]; ok {390				return nil
GarbageCollect · 标记与清扫入口

先持有 wlock,再求可达集合;扫描阶段遇到 marked 节点直接保留。后续 remove 和 scanAll 在同一更新事务中执行。

core/metadata/db.gov2.0.0 · L506–L521
查看原文件
506		refs := func(n gc.Node) ([]gc.Node, error) {507			var sn []gc.Node508			if err := c.references(ctx, tx, n, func(nn gc.Node) { // From gc context509				sn = append(sn, nn)510			}); err != nil {511				return nil, err512			}513			return sn, nil514		}515516		reachable, err := gc.Tricolor(nodes, refs)517		if err != nil {518			return err519		}520		marked = reachable521		return nil
getMarked · 引用适配与实际调用

把数据库引用解析包装为 refs,再实际调用 gc.Tricolor;这里不是仅在通用库中找到了一个未使用的图算法。

core/metadata/gc.gov2.0.0 · L380–L394
查看原文件
380		ibkt := nbkt.Bucket(bucketKeyObjectImages)381		if ibkt != nil {382			if err := ibkt.ForEach(func(k, v []byte) error {383				if v != nil {384					return nil385				}386387				if !isExpiredImage(ctx, k, ibkt.Bucket(k), expThreshold) {388					fn(gcnode(ResourceImage, ns, string(k)))389				}390				return nil391			}); err != nil {392				return err393			}394		}
scanRoots · 未过期镜像作为根

有效镜像是保留资源的一个入口。租约、进行中的内容等也有各自的根扫描分支,不是只保留被运行中容器引用的数据。

core/metadata/gc.gov2.0.0 · L529–L544
查看原文件
529	case ResourceImage, resourceImageFlat:530		bkt := getBucket(tx, bucketKeyVersion, []byte(node.Namespace), bucketKeyObjectImages, []byte(node.Key))531		if bkt == nil {532			// Node may be created from dead edge533			return nil534		}535		target := bkt.Bucket(bucketKeyTarget)536		if target != nil {537			ctype := ResourceContent538			if node.Type == resourceImageFlat {539				// For flat leases, keep the target content only540				ctype = resourceContentFlat541			}542			contentKey := string(target.Get(bucketKeyDigest))543			fn(gcnode(ctype, node.Namespace, contentKey))544		}
references · 镜像到内容的引用

镜像节点通过 target digest 关联内容节点;资源类型区分普通引用与 flat 引用,命名空间也保存在节点身份中。

pkg/gc/gc.gov2.0.0 · L64–L83
查看原文件
64func Tricolor(roots []Node, refs func(ref Node) ([]Node, error)) (map[Node]struct{}, error) {65	var (66		grays     []Node                // maintain a gray "stack"67		seen      = map[Node]struct{}{} // or not "white", basically "seen"68		reachable = map[Node]struct{}{} // or "black", in tri-color parlance69	)7071	grays = append(grays, roots...)72	for _, root := range roots {73		seen[root] = struct{}{} // pre-mark this as not-white74	}7576	for len(grays) > 0 {77		// Pick any gray object78		id := grays[len(grays)-1] // effectively "depth first" because first element79		grays = grays[:len(grays)-1]80		rs, err := refs(id)81		if err != nil {82			return nil, err83		}
Tricolor · 显式栈与已发现集合

根先入栈并记入 seen,随后从栈尾取出,实现深度优先展开;refs 错误会终止标记,调用者不会继续按不完整结果清扫。

pkg/gc/gc.gov2.0.0 · L85–L100
查看原文件
85		// mark all the referenced objects as gray86		for _, target := range rs {87			if _, ok := seen[target]; !ok {88				grays = append(grays, target)89				seen[target] = struct{}{}90			}91		}9293		// strip bits above max resource type94		id.Type = id.Type & ResourceMax95		// mark as black when done96		reachable[id] = struct{}{}97	}9899	return reachable, nil100}
Tricolor · 入栈去重与标记完成

随后发现的引用目标在入栈时立即加入 seen,避免因共享边或环重复入栈;初始根列表未去重,重复根仍会展开。完成节点时去掉类型高位控制标记,得到用于实际资源保留的身份。

六段原文连接实际调用、根与引用、显式栈标记以及清扫决策。DB.GarbageCollect 后半段的后端清理与锁释放另附完整来源。

收益与取舍#

工程分析

带来的收益

  • 共享层只需从任意一个有效根可达即可保留,不依赖删除请求逐个维护共享计数
  • seen 集合使引用环不会导致无限递归;显式栈避免调用栈随引用深度增长
  • 资源类型和命名空间参与节点身份,统一处理内容、镜像、快照等不同对象

付出的代价

  • 成本随根条目数和实际展开的引用数增长;重复根可能重复读取其出边,不能无条件声称每条可达边只展开一次。随后还需扫描全部资源,数据库查询和后端删除的成本另计。
  • R 为根条目数(含重复),Vr 为按遍历身份计算的可达节点数;seen、reachable 和显式栈合计占用 O(R+Vr) 空间,读取单个节点的引用列表还需要临时空间。flat 类型标记可使同一底层对象以不同遍历身份出现。
  • 此路径在标记与元数据删除期间保持写锁,不是应用可自由修改引用的并发或增量 GC;后端删除和事件通知还有额外时延

可比较的选择

引用计数可在计数归零时局部回收,但每次修改引用都需要维护计数,引用环还需其他策略。

按访问时间或保留期淘汰适合容量管理策略,却不能单凭年龄保证共享资源和进行中的下载仍然有效。

版本与适用范围#

固定 containerd v2.0.0,实际调用的是 gc.Tricolor,而非同文件中的 ConcurrentMark。这里回收容器运行时持久化资源,不是 Go 进程的堆内存 GC。根集合还包括进行中的资源、显式根标签和扩展收集器;本文用镜像与租约说明主要路径,完整规则见 scanRoots。 初始根列表没有先行去重,不能把后续引用去重的性质直接推广到根集合。

参考来源#

源码、官方文档与原始论文
01
containerd · GarbageCollect · 标记与清扫入口

core/metadata/db.go · GarbageCollect · 标记与清扫入口

源码v2.0.0
02
containerd · getMarked · 引用适配与实际调用

core/metadata/db.go · getMarked · 引用适配与实际调用

源码v2.0.0
03
containerd · scanRoots · 未过期镜像作为根

core/metadata/gc.go · scanRoots · 未过期镜像作为根

源码v2.0.0
04
containerd · references · 镜像到内容的引用

core/metadata/gc.go · references · 镜像到内容的引用

源码v2.0.0
05
containerd · Tricolor · 显式栈与已发现集合

pkg/gc/gc.go · Tricolor · 显式栈与已发现集合

源码v2.0.0
06
containerd · Tricolor · 入栈去重与标记完成

pkg/gc/gc.go · Tricolor · 入栈去重与标记完成

源码v2.0.0
07
containerd · GarbageCollect · scanAll 与 remove

core/metadata/db.go · GarbageCollect · scanAll 与 remove

源码v2.0.0
08
containerd · GarbageCollect · 后端清理与锁释放

core/metadata/db.go · GarbageCollect · 后端清理与锁释放

源码v2.0.0
09
containerd · scanRoots · 租约保护及 flat 规则

core/metadata/gc.go · scanRoots · 租约保护及 flat 规则

源码v2.0.0
10
containerd · references · 内容、快照、镜像的完整引用规则

core/metadata/gc.go · references · 内容、快照、镜像的完整引用规则

源码v2.0.0

相关实现与比较#

4 个相关案例
QEMU·TCG 翻译块v9.2.0

QEMU TCG 用 QHT 保存 TranslationBlock 索引,帮助复用与当前客体执行状态一致的翻译结果。

附源码片段 · 缓存与复用
9 个匹配案例