机制深度解析:从 ADR 决策到 agent/images 目录监视器实现)
k3s 镜像自动导入Auto Import机制深度解析从 ADR 决策到 agent/images 目录监视器实现【免费下载链接】k3sLightweight Kubernetes项目地址: https://gitcode.com/GitHub_Trending/k3/k3s导读本文以 k3s 仓库中的架构决策记录ADRadd-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 的状态 MapADR 明确描述了控制器的核心思路为文件维护一份状态 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 为什么选择 fsnotifyADR 给出的选型理由是跨发行版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.gofor _, 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.gotar 归档使用client.Import(ctx, imageReader, containerd.WithAllPlatforms(true), containerd.WithSkipMissing())导入全部平台镜像并跳过缺失内容。3.3 启动期全量导入与监视的无缝衔接PreloadImagescontainerd.go在 k3s agent 启动、containerd 就绪后被调用它依次执行建立客户端连接并将上下文切换到 CRI 命名空间clearLeases清理旧版本 k3s 遗留的 lease当前已不再用 lease 锁定 blobclearLabels清除所有此前被 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.goio.cattle.k3s.pinnedpinned io.cri-containerd.pinnedpinned这两个标签向 k3s/containerd 的镜像清理prune流程表明该镜像由 k3s 管理且需保留避免自动导入的镜像被当作垃圾回收。导入完成后还会执行retagImages与labelContent可选取决于AirgapExtraRegistry配置把镜像重打为从额外私有仓库拉取的标签。4.2 状态缓存跨重启去重与自动补标watchqueue内置.cache.json机制watcher.gosyncCache将filesCache含每文件的 Size、ModTime、Images 列表序列化写入agent/images/.cache.jsonloadCache启动时读取该文件恢复状态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 捕获2echo mirror.gcr.io/redis:latest \| tee /var/lib/rancher/k3s/agent/images/testautoimport.txt300 秒内k3s ctr images list可见 redis 镜像且带 pinned 标签3mv testautoimport.txt testautoimportrename.txt重命名镜像仍在标签保持 pinnedRENAME 触发新 CREATE4创建 → 删除 → 再创建bb.txt含 busybox每次操作镜像状态均保持 pinned5mv images test mv test images移动整个目录并在中途写入 mysql.txt目录移回后 mysql 镜像被自动导入并 pinned6重启集群busybox 镜像仍 pinned缓存补标生效7删除bb.txt并再次重启镜像 unpinned不再被 k3s 保护测试中的关键判定命令k3s ctr images list | grep mirror.gcr.io/redis # 期望输出包含 io.cattle.k3s.pinnedpinned 与 io.cri-containerd.pinnedpinned这套流程同时覆盖了 ADR 中描述的 CREATE、WRITE、RENAME、REMOVE 四类事件场景以及目录整体移动跨重启标签恢复等边界情况。6. 结论与影响评估正向收益ADR 评估更好利用内嵌 containerd 镜像存储边缘节点只需往目录里丢文件无需任何手工命令依赖成本低fsnotify 是上游已在使用的间接依赖无需引入全新生态。代价与注意点新增直接依赖fsnotify 从间接依赖变为直接依赖需纳入依赖审计范围监视范围限制当前仅覆盖默认agent/images目录未来才考虑扩展多目录离线语义.txt清单仍需要网络拉取纯离线场景应优先使用 tar 归档包.cache.json需用户主动创建才会跨重启生效。对运维者而言这套机制的实用价值在于把镜像分发简化为文件分发——无论是scp、配置管理工具如 Ansible还是边缘网关下发只要把镜像包放进/var/lib/rancher/k3s/agent/imagesk3s 就会自动完成导入、打标与防回收是离线与边缘部署场景下值得优先采用的镜像预加载通道。延伸阅读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),仅供参考