k3s 镜像自动导入(Auto Import)机制深度解析:从 ADR 决策到 agent/images 目录监视器实现
【免费下载链接】k3sLightweight Kubernetes项目地址: https://gitcode.com/GitHub_Trending/k3/k3s
导读
本文以 k3s 仓库中的架构决策记录(ADR)add-auto-import-containerd.md 为主线,深入剖析 k3s 内嵌 containerd 镜像存储的**自动导入(auto import)**能力:用户只需把镜像文件(tar 压缩包或 txt 镜像清单)放入agent/images目录,k3s 的监视控制器便会自动将其导入 containerd 镜像存储。读者将掌握该功能的设计动机、fsnotify 事件驱动原理、文件状态缓存机制,以及如何在边缘/离线环境中把它当作"免操作"的镜像预加载通道来使用。
1. 背景:从嵌入式 registry 到"手动导入"的痛点
自 k3s 引入嵌入式 registry 功能之后,用户频繁提出一个问题:镜像必须手动导入,尤其是在边缘(edge)环境中。边缘节点往往处于弱网、断网或离线状态,每次部署新镜像都手动执行k3s ctr images import或k3s ctr images pull既繁琐又容易出错。
为此,k3s 团队提出了一个设计目标:提供一个专门的目录,由控制器持续监视其中的镜像文件,一旦发现新文件或文件变化,就自动将其加入 containerd 镜像存储。该方案在 2024 年 10 月以 ADR 形式记录并标记为Accepted(已采纳),即 docs/adrs/add-auto-import-containerd.md。
决策要点(Decision)
通过在
agent/images目录上部署一个 watcher(监视器),实现镜像到 containerd 镜像存储的自动导入。
agent/images是默认镜像目录,第一版控制器主要围绕该默认目录工作,未来可以扩展监视更多目录。
2. 架构设计:文件状态 Map + fsnotify 事件流
2.1 核心数据结构:以完整文件路径为 key 的状态 Map
ADR 明确描述了控制器的核心思路:为文件维护一份状态 Map,通过状态对比判断文件是否被修改、大小是否变化。
map[string]fs.FileInfoMap 的key是文件的完整路径,之所以这样设计,是因为 fsnotify 事件中可以通过event.Name直接拿到该路径,从而以 O(1) 复杂度索引到对应状态。
在最终实现 pkg/agent/containerd/watcher.go 中,这个设计演化为一个更完整的watchqueue结构体:
type fileInfo struct { Size int64 `json:"size"` ModTime metav1.Time `json:"modTime"` Images []string `json:"images"` seen bool // 不参与序列化,用于标记重启后是否已见过该文件 } type watchqueue struct { cfg *config.Node watcher *fsnotify.Watcher filesCache map[string]*fileInfo workqueue workqueue.TypedDelayingInterface[string] }相比 ADR 的原始设计,实现版本做了两处关键增强:
- 状态从
fs.FileInfo升级为自定义的fileInfo,额外记录Size、ModTime 和上次导入成功的镜像名列表(Images),并把ModTime用metav1.Time序列化,支持持久化到磁盘(见下文缓存章节); - 引入workqueue(延迟队列)将 fsnotify 事件与导入动作解耦,事件先入队、再由 worker 消费,避免高频写事件导致 containerd 连接频繁建立。
2.2 为什么选择 fsnotify
ADR 给出的选型理由是:
- 跨发行版:fsnotify 可轻松用于任意 Linux 发行版,无需针对特定发行版移植;
- 跨平台:同时支持 Windows;
- 事件工具集完善:通过 channel 接收
CREATE、RENAME、REMOVE、WRITE等事件。
从仓库证据看,fsnotify 被声明在 go.mod 中,并在 pkg/agent/containerd/watcher.go 以github.com/fsnotify/fsnotify直接引入。ADR 同时指出一个额外收益:fsnotify 本就是上游(kubernetes)使用的间接依赖,因此引入它不会显著增加供应链成本。
2.3 事件处理语义(ADR 原设计)
ADR 为四类事件定义了明确的处理语义:
| 事件 | 控制器行为 |
|---|---|
CREATE(文件创建) | 将文件加入状态 Map;若事件对象不是目录,则立即导入镜像 |
WRITE(文件写入) | 依据状态中记录的时间与大小,校验文件大小是否变化、修改时间是否更新 |
RENAME(重命名) | 从状态中删除旧文件记录;重命名实际会创建同内容的新文件,因此 watcher 会随后收到一个CREATE事件 |
REMOVE(删除) | 从状态中删除该文件 |
2.4 实现中的事件驱动闭环
源码 pkg/agent/containerd/watcher.go 完整实现了上述语义:
- watch 循环:
watchImages启动一个 goroutine 持续消费watcher.Events与watcher.Errorschannel,仅当事件路径位于cfg.Images目录内才将其加入延迟队列(AddAfter(event.Name, time.Second*2),2 秒去抖窗口可吸收边写边刷新的文件内容); - worker 消费:
runWorkerForImages预先建立 containerd 客户端与 CRI 连接(避免每个事件都重建),循环调用processNextEventForImages处理队列项; - 变更判定:
processImageEvent中通过os.Stat判断文件是否存在——不存在即视为 RENAME/REMOVE,删除缓存记录;存在则比对file.Size()、file.ModTime()与缓存状态,只有大小变化或修改时间更新时才真正触发导入(watcher.go); - 目录递归:fsnotify 不递归监视子目录,因此遇到目录事件时,控制器会
watcher.Add(key)加入新监视点,并用os.ReadDir列出子项批量入队(watcher.go)。
一个值得注意的细节:watcher 监视的是images 目录的父目录(filepath.Dir(cfg.Images)),因为 images 目录本身在启动时可能尚不存在,监视父目录可以捕获目录的创建事件(watcher.go)。
3. 支持的镜像文件格式与导入链路
3.1 默认目录位置
从 pkg/agent/config/config.go 可以看到默认路径的拼接逻辑:
nodeConfig.Images = filepath.Join(envInfo.DataDir, "agent", "images")即默认数据目录下的agent/images,对应常见安装路径:
/var/lib/rancher/k3s/agent/images3.2 文件格式支持
isFileSupported通过扩展名白名单判定(watcher.go):
for _, ext := range append(tarfile.SupportedExtensions, ".txt") { if strings.HasSuffix(path, ext) { return true } }由此确认两类受支持文件(tarfile.SupportedExtensions来自 wharfie 库,涵盖常见压缩格式):
- 镜像 tar 归档包(含
.tar、.tar.gz、.tgz等压缩格式)——直接导入 containerd 镜像存储; .txt镜像清单——按行读取镜像引用,通过 CRI 服务从远程仓库预拉取(pre-pull)。
对应导入逻辑在 pkg/agent/containerd/containerd.go 的preloadFile中分叉处理:
.txt文件:调用prePullImages,走CRI 客户端拉取。源码注释特别强调:镜像拉取必须走 CRI 而非 containerd 直连,因为仓库镜像(mirror)与 rewrite 配置由 CRI 服务处理(containerd.go);- tar 归档:使用
client.Import(ctx, imageReader, containerd.WithAllPlatforms(true), containerd.WithSkipMissing())导入全部平台镜像,并跳过缺失内容。
3.3 启动期全量导入与监视的无缝衔接
PreloadImages(containerd.go)在 k3s agent 启动、containerd 就绪后被调用,它依次执行:
- 建立客户端连接,并将上下文切换到 CRI 命名空间;
clearLeases:清理旧版本 k3s 遗留的 lease(当前已不再用 lease 锁定 blob);clearLabels:清除所有此前被 k3s 打上 pinned 标签的镜像(每次启动先清零,随后由导入流程重新打标);importAndWatchImages:先把 images 目录本身加入工作队列,递归列出并导入已存在的全部镜像,等待队列清空后才返回,随后调用pruneCache清理上次运行遗留的失效记录(watcher.go)。之后 watcher 持续运行,接管后续所有增量变化。
4. 防回收机制:pinned 标签与.cache.json持久化
4.1 pinned 标签:防止镜像被 GC 回收
自动导入的镜像会被打上两个 pinned 标签(见 containerd.go 的labelImages,以及测试断言 tests/docker/autoimport/autoimport_test.go):
io.cattle.k3s.pinned=pinned io.cri-containerd.pinned=pinned这两个标签向 k3s/containerd 的镜像清理(prune)流程表明"该镜像由 k3s 管理且需保留",避免自动导入的镜像被当作垃圾回收。导入完成后还会执行retagImages与labelContent(可选,取决于AirgapExtraRegistry配置),把镜像重打为从额外私有仓库拉取的标签。
4.2 状态缓存:跨重启去重与自动补标
watchqueue内置.cache.json机制(watcher.go):
syncCache:将filesCache(含每文件的 Size、ModTime、Images 列表)序列化写入agent/images/.cache.json;loadCache:启动时读取该文件恢复状态;0 字节空文件合法且会被跳过——用户可以主动touch该文件来启用持久化,无需任何配置;- 配合
fileInfo.seen字段:k3s 每次启动都会清空所有 pinned 标签,因此重启后第一次见到某文件时,会依据缓存中的 Images 列表重新补打 pinned 标签(watcher.go),随后pruneCache剔除本次未出现过的失效条目,防止缓存无限膨胀。
也就是说,只要文件未变更,重启后不会重复导入,只会快速补标签,启动开销极低。
5. 实战验证:Docker 集成测试的完整操作序列
仓库提供了端到端的 Docker 测试 tests/docker/autoimport/autoimport_test.go,它完整演示了自动导入的用户可见行为,可直接作为边缘环境的使用手册:
| 步骤 | 操作 | 预期结果 |
|---|---|---|
| 1 | 创建目录mkdir /var/lib/rancher/k3s/agent/images | 目录创建即被父目录 watcher 捕获 |
| 2 | echo mirror.gcr.io/redis:latest \| tee /var/lib/rancher/k3s/agent/images/testautoimport.txt | 300 秒内k3s ctr images list可见 redis 镜像且带 pinned 标签 |
| 3 | mv testautoimport.txt testautoimportrename.txt(重命名) | 镜像仍在,标签保持 pinned(RENAME 触发新 CREATE) |
| 4 | 创建 → 删除 → 再创建bb.txt(含 busybox) | 每次操作镜像状态均保持 pinned |
| 5 | mv images test && mv test images(移动整个目录并在中途写入 mysql.txt) | 目录移回后 mysql 镜像被自动导入并 pinned |
| 6 | 重启集群 | busybox 镜像仍 pinned(缓存补标生效) |
| 7 | 删除bb.txt并再次重启 | 镜像 unpinned(不再被 k3s 保护) |
测试中的关键判定命令:
k3s ctr images list | grep mirror.gcr.io/redis # 期望输出包含 io.cattle.k3s.pinned=pinned 与 io.cri-containerd.pinned=pinned这套流程同时覆盖了 ADR 中描述的 CREATE、WRITE、RENAME、REMOVE 四类事件场景,以及"目录整体移动""跨重启标签恢复"等边界情况。
6. 结论与影响评估
正向收益(ADR 评估)
- 更好利用内嵌 containerd 镜像存储:边缘节点只需往目录里"丢文件",无需任何手工命令;
- 依赖成本低:fsnotify 是上游已在使用的间接依赖,无需引入全新生态。
代价与注意点
- 新增直接依赖:fsnotify 从间接依赖变为直接依赖,需纳入依赖审计范围;
- 监视范围限制:当前仅覆盖默认
agent/images目录(未来才考虑扩展多目录); - 离线语义:
.txt清单仍需要网络拉取,纯离线场景应优先使用 tar 归档包;.cache.json需用户主动创建才会跨重启生效。
对运维者而言,这套机制的实用价值在于:把"镜像分发"简化为"文件分发"——无论是scp、配置管理工具(如 Ansible)还是边缘网关下发,只要把镜像包放进/var/lib/rancher/k3s/agent/images,k3s 就会自动完成导入、打标与防回收,是离线与边缘部署场景下值得优先采用的镜像预加载通道。
延伸阅读
- ADR 原文:docs/adrs/add-auto-import-containerd.md
- 核心实现:pkg/agent/containerd/watcher.go、pkg/agent/containerd/containerd.go
- 默认路径来源:pkg/agent/config/config.go
- 集成测试:tests/docker/autoimport/autoimport_test.go
【免费下载链接】k3sLightweight Kubernetes项目地址: https://gitcode.com/GitHub_Trending/k3/k3s
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考