🧱 AgentMeshOS 内部镜像缓存仓库(2026-10-26)
📌 结论摘要
AgentMeshOS 已把构建缓存从分散的 Runner 本地层和外部远程缓存改为 102 上的内部 OCI Registry,正式地址为 http://100.64.0.4:5000。构建仍由 101/102 自建 Runner 执行,最终镜像继续推送 GHCR;内部 Registry 保存 BuildKit 缓存、固定摘要的基础镜像,以及经摘要校验的 transfer/sha-<40位SHA> 部署传输副本。GHCR 始终是生产镜像权威,内部传输 tag 只缩短 102 standby 的部署拉取路径。依据是 102 已运行容器 agentmeshos-internal-registry,101 可访问 /v2/,101/102 的 github-runner 都完成了跨 builder CACHED 回读,内部基础镜像和 ctr --plain-http 导入均通过真实测试。影响是缓存、基础层和控制面镜像可以走局域网复用,减少对 GHCR 和 Docker Hub 的长尾等待。
内部 Registry 是共享缓存后端。 本方案不改变项目代码权威、生产发布权威或 GHCR 最终镜像边界。102 只增加一个 Tailnet-only 基础服务;Registry 不可用时,构建失败关闭,不回退到旧的 GHA/GHCR buildcache 模型。
🗂️ 目录
🧩 背景与目标
此前慢构建的主要问题不是最终镜像推送,而是每次需要重新获取基础镜像层和重建依赖层。Runner 本地 Docker 缓存虽然存在,但至少有三个限制:Job 可能落到 101 或 102;GitHub Actions 工作目录和 BuildKit builder 可能被替换;本地镜像清理、节点重启和不同 builder 之间无法保证共享。单纯依赖某台 Runner 的本地缓存,会让同一提交在不同节点上重复完整构建。
目标固定为四项:
- 缓存和固定摘要基础镜像可以在 101 与 102 Runner 之间共享。
- 缓存不进入公网,也不改变 Tailnet 作为控制面的边界。
- 最终镜像仍以完整提交 SHA 的 GHCR 镜像为准,内部缓存不能替代可发布产物。
- 缓存目录、日志、磁盘水位、垃圾回收和回滚都有可执行入口。
📊 采用方案与取舍
| 方案 | 优点 | 缺点 | 结论 |
|---|---|---|---|
| 只保留 Runner 本地 Docker/BuildKit 缓存 | 无新增服务,速度最快时接近本地命中 | Job 落到其他节点或 builder 被重建时失效;无法证明跨节点共享 | 保留为第一层,但不作为唯一方案 |
| 102 内部 OCI Registry | 跨 101/102 和跨 builder 共享构建缓存;可保存固定摘要基础镜像;Tailnet-only;不依赖 GitHub Cache;缓存故障时仅跳过缓存 | 多一个宿主服务、存储目录和运维入口 | 采用 |
| GitHub Actions Cache / GHCR buildcache | 托管、无需自建服务 | 网络和平台依赖更强,现有构建曾受远程缓存慢影响 | 退役为缓存后端;GHCR 仅保留最终镜像 |
| Docker Hub 自有缓存仓库 | 工具成熟 | 需要额外账户、认证、网络和可见性治理 | 不采用 |
内部 Registry 使用固定镜像 public.ecr.aws/docker/library/registry@sha256:a3d8aaa63ed8681a604f1dea0aa03f100d5895b6a58ace528858a7b332415373,避免 2.8.3 浮动 tag 在重启或重部署时悄悄换层。容器只绑定 100.64.0.4:5000,UFW 只允许 100.64.0.2 和 100.64.0.4。这意味着 Registry 没有应用层认证,安全性依赖 Tailnet 网络身份和精确来源规则;不得把它发布到公网、局域网泛地址或其他 Tailnet 节点。
🔧 运行与工作流配置
十个 Docker 构建工作流统一使用:
每个工作流使用独立的 agentmeshos/cache/<service>:buildcache 标签,并在 BuildKit cache 参数中固定 registry.insecure=true,ignore-error=true。显式使用 setup-buildx-action 的工作流必须配置 driver-opts: network=host,否则 BuildKit 位于 Docker bridge 网络时无法访问 Tailnet Registry,会出现 import 30 秒、export 约 2 分钟的超时。前者只表示 Tailnet HTTP Registry,不允许把 Registry 改绑公网;ignore-error=true 保证缓存故障不会阻断镜像构建。最终 push: true 的目标仍是 GHCR 的 sha-<40位SHA> 镜像,GHCR 推送失败仍会阻断发布。
固定基础镜像
Runner 访问内部 HTTP Registry 时,setup-buildx-action 通过 buildkitd-config-inline 注入以下 BuildKit 配置:
Python 和 Nginx 基础镜像固定为 Linux amd64 单平台摘要,来源与目标摘要记录在 deploy/internal-registry/base-images.json:
| 用途 | 内部镜像摘要 | 原来源索引摘要 |
|---|---|---|
| Python | 100.64.0.4:5000/agentmeshos/base/python@sha256:a249c9f47e05708dd367f3fe8ada03cf347390fad66fb8b0518c0ef55ae3cb84 |
python@sha256:09f7da3bc104798d0afb40bc08d23ab2da20a76130cec1f2ef170848f5d85217 |
| Nginx | 100.64.0.4:5000/agentmeshos/base/nginx@sha256:62223d644fa234c3a1cc785ee14242ec47a77364226f1c811d2f669f96dc2ac8 |
nginx@sha256:65645c7bb6a0661892a8b03b89d0743208a18dd2f3f17a54ef4b76fb8e2f2a10 |
CI 通过 AGENTMESHOS_BASE_PYTHON_IMAGE 和 AGENTMESHOS_BASE_NGINX_IMAGE 变量传入内部摘要;仓库变量未设置时使用上述固定摘要。Dockerfile 的 PYTHON_BASE_IMAGE / NGINX_BASE_IMAGE 默认值保留同样的上游固定摘要,便于仓库外构建。更新基础镜像时先在 101 选定并测速来源、docker pull 到本地,再执行:
python3 scripts/deploy/internal-registry/mirror-base-image.py \
--source python:3.12-slim \
--registry http://100.64.0.4:5000 \
--repository agentmeshos/base/python \
--tag 3.12-slim
随后必须回填 base-images.json、Dockerfile 默认摘要和 CI 固定摘要,并重新执行真实构建。内部 tag 只用于人工发现,CI 实际引用 manifest 摘要;不得用可变 tag 代替摘要。
备用控制面不复建已存在镜像。三个独立工作流必须发布与 standby 相同的 sha-<40位提交SHA> 标签;standby 在 GitHub-hosted fan-in Job 中等待三项完成并并行检查 manifest,全部存在时只运行部署 Job。目标 SHA 同时写入 run 标题,用于精确识别同一部署目标,不能按 workflow_run 包装运行的 head_sha 去重。目标 SHA 在准备和部署前都必须等于当前 main,旧提交不得占用自建 Runner 或在后续提交之后继续部署。仅 standby 脚本变更不再由普通 push 自动触发,使用 workflow_dispatch,避免与镜像工作流并发补建三套镜像。
控制面镜像传输副本
Auth、Console、API 构建同时发布:
| 仓库 | 标签 | 作用 |
|---|---|---|
| GHCR | sha-<40位提交SHA> |
唯一生产镜像权威与长期回滚来源 |
| 102 内部 Registry | agentmeshos/transfer/<service>:sha-<40位提交SHA> |
与 GHCR 同 SHA 的不可变局域网传输副本 |
GitHub-hosted fan-in 只检查 GHCR 同 SHA 标签,避免从公网 Runner 访问 Tailnet Registry。随后短时 verify Job 在自建 Runner 上并行比较 GHCR manifest 摘要和内部 Docker-Content-Digest;只有两者完全相等才允许部署或跳过补建。102 通过 ctr -n moby images pull --plain-http 导入内部镜像,再由 Docker 以同一 digest 启动容器。内部传输 tag 也使用完整 SHA,旧工作流无法覆盖新版本;GHCR 继续承担正式版本、回滚和审计权威。
102 的持久化目录为 /srv/agentmeshos-internal-registry/data,配置目录为 /etc/agentmeshos/internal-registry。容器使用 local 日志驱动,单文件上限 20 MiB,最多保留 5 个文件。部署和检查统一由 scripts/deploy/internal-registry/agentmeshos-internal-registry-deploy.sh 管理:
| 操作 | 命令 | 作用 |
|---|---|---|
| 部署/更新 | deploy |
固定摘要启动、设置来源规则、健康检查 |
| 只读检查 | check |
验证容器、固定摘要、Tailnet 监听、UFW、101 连通和磁盘水位 |
| 状态 | status |
显示容器、缓存目录和文件系统 |
| 回收预览 | gc-plan |
维护窗口短暂停写并运行 dry-run,不删除对象 |
| 回收 | gc |
维护窗口删除未引用层和旧的无标签缓存清单 |
| 回滚 | rollback |
删除容器、配置和 UFW 规则,保留缓存目录 |
🧪 已验证证据
| 验收项 | 证据 | 结论 |
|---|---|---|
| 102 运行态 | docker inspect 显示容器 running,镜像 ID 为 sha256:a3d8aaa63ed8681a604f1dea0aa03f100d5895b6a58ace528858a7b332415373 |
固定摘要镜像已实际运行;重部署后仍需再次回读 |
| 101 到 102 | 101 请求 http://100.64.0.4:5000/v2/ 成功 |
Tailnet 客户端路径可用 |
| 缓存目录 | 更新部署并通过固定摘要检查后,/srv/agentmeshos-internal-registry/data 约 31 MiB,根文件系统使用率 32% |
当前容量没有压力 |
| 101/102 Runner 写缓存 | 两端都以 github-runner 身份创建独立 builder A,并成功写入各自的 cache-runner*-smoke:buildcache |
两个 Runner 用户都具备 Docker 和 Registry 写路径 |
| 101/102 Runner 读缓存 | 删除各自 builder A,再创建全新的 builder B;两端日志均显示 importing cache manifest、CACHED |
跨节点和跨 builder 命中真实成立,不是本地旧缓存 |
| 内部基础镜像 | 102 新建 builder 从内部 Python/Nginx 摘要拉取,Python 最大层 29.79 MB 和 Nginx 最大层 15.52 MB 均在约 0.1–0.7 秒完成;101 实际上构建设站点和认证镜像成功 |
内部基础镜像可直接用于项目 Dockerfile |
| 基础层外部对比 | 同一 Docs Nginx 15.52 MB 层从 Docker Hub 曾耗时 221.6 秒 |
基础镜像内网化直接消除了该长尾 |
| 控制面 GHCR 拉取长尾 | 2026-10-26 实跑中 Auth 从 GHCR 到 102 的单次拉取约 9 分 45 秒,成为发布主瓶颈 |
增加同 SHA transfer 局域网传输副本和摘要对等门禁 |
ctr 内部导入 |
102 从内部 Nginx 摘要导入约 0.1 秒,随后 docker image inspect 可见同一 image ID |
证明 ctr -n moby 与 Docker 镜像存储可协同,不需要重启 Docker 或放开 daemon-wide insecure registry |
| 最终生产提速 | fe9db86c 发布总时长 8分20秒,相对 eb3d2ba 的 14分27秒 缩短约 6分07秒;standby build=skipped、deploy=1分35秒 |
fan-in、基础镜像和内部传输三项收益共同生效 |
| GC 预览 | gc-plan 输出 24 blobs marked, 1 blobs and 1 manifests eligible for deletion,随后 Registry 恢复并通过 101 健康检查 |
删除流程可预演,当前没有删除任何对象 |
| 工作流契约 | internal_registry_cache_contract=passed workflows=10 |
十个目标工作流均未保留旧 GHA/GHCR cache 回退 |
| 缓存故障降级 | 101 Runner 对不可达 127.0.0.1:5999 实测 ignore-error=true,BuildKit 给出缓存错误但构建仍成功 |
缓存是加速层,不能阻断 GHCR 镜像构建 |
| BuildKit 网络 | Actions 日志显示 bridge 网络下 Registry import/export 持续超时;手工 network=host builder 可命中 CACHED |
七个显式 setup-buildx 工作流统一使用 host 网络 |
上述证据只证明缓存基础设施和一条真实 Runner 路径可用。生产 Actions 日志仍需在首次统一构建后核对实际 cache-from/cache-to 行为。
💾 容量与垃圾回收
BuildKit 会覆盖相同服务名的 buildcache 标签。Registry 的默认垃圾回收不会删除未打标签的旧清单,因此如果不做维护,历史和层对象会持续增长。脚本把回收固定为人工维护操作,而不是定时自动执行,原因是 GC 扫描期间不能安全接受新的并行写缓存。
回收流程如下:
- 暂停可能写同一 Registry 的构建,或等待当前所有 Docker 工作流结束。
- 执行
gc-plan,读取日志中的可回收对象和registry_disk_used_percent。 - 确认没有构建窗口后执行
gc。脚本会停止 Registry,运行registry garbage-collect --delete-untagged=true,随后启动容器并执行健康检查。 - 回读
/v2/、缓存目录大小和下一次构建的CACHED状态。
磁盘预警线为 85%,硬门禁为 95%。达到预警线时必须安排回收或扩容;达到硬门禁时 check 失败,必须先处理容量。缓存数据不得直接删除;rollback 只删除运行对象并保留 /srv/agentmeshos-internal-registry/data。
🔁 回退方案
回退分两级。第一级只回退容器:执行 rollback,保留全部缓存数据;随后把十个工作流的 AGENTMESHOS_INTERNAL_REGISTRY 临时指向可用内部地址,或回退到上一版工作流文件。第二级恢复旧构建缓存:只有在内部 Registry 无法恢复且已记录故障原因时,才允许单独回退工作流到上一提交;不得让新旧缓存后端同时写两套标签,以免同一构建出现不可解释的缓存血缘。
DNS、Globalping、Console 和 102 只读备用控制面不属于本任务回退范围;它们各自使用独立回滚材料,不能因为 Registry 故障一起回退。
⚠️ 风险与未决项
| 风险 | 影响 | 处置 |
|---|---|---|
| Tailnet-only 但没有应用认证 | 获得允许来源身份的主机可读写缓存 | UFW 固定两个来源;不暴露公网;后续如需更强隔离再评估 token |
| Registry 单节点在 102 | 102 故障时共享缓存不可用 | 构建通过 ignore-error=true 回退到本地无命中构建;最终 GHCR 推送仍为硬门禁 |
| 内部基础镜像为单平台 | 当前只镜像 Linux amd64,不能服务多架构构建 | 101/102 Runner 均为 amd64;若未来增加 arm64,必须另建平台清单并更新摘要 |
| 内部基础镜像更新需回填摘要 | tag 被覆盖不会影响已固定摘要,但也意味着更新不会自动生效 | 先执行 mirror,再更新 JSON、Dockerfile 和 CI 变量并完成真实构建;禁止直接覆盖后宣称已升级 |
| 内部传输镜像增长 | 每个完整 SHA 都保留 tag 与层,需要容量维护 | 延续 85%/95% 容量门禁和人工 gc-plan/gc;tag 不自动删除,GHCR 仍是长期权威 |
| 内部传输副本增长 | 未打标签旧 manifest 与层会占用磁盘 | 延续 85%/95% 容量门禁和人工 gc-plan/gc;current 不替代 GHCR 长期版本 |
| GC 时并行构建 | 可能产生 layer 上传与删除竞态 | GC 只在维护窗口人工执行,脚本停写并恢复 |
| 缓存持续增长 | 占满根文件系统影响 102 业务 | check 执行 85% 预警、95% 硬门禁;按维护流程 GC |
| GHCR 仍为最终仓库 | GHCR 故障时缓存命中也不能完成发布 | 保留为正式发布依赖,不在本任务内引入第二最终镜像仓库 |
📌 未决项 / 需要用户决策
当前没有需要用户选择的新增架构项。下一次多镜像生产发布需要记录排队、构建、standby 和部署时长,确认 fan-in 与基础镜像收益;若后续要求 Registry 双活或增加应用认证,应另立任务,不与本次单节点缓存基础设施改造混在一起。