Jenkins 推送成功但 Harbor 查无镜像——retention 误删新镜像与 GC 竞态的问题
记录时间:2026-08-24
环境:Harbor 2.8.1(私有 Harbor,10 项目 / 270 仓库,registry 稳态 117G)/ Jenkins 2.568.1 共享库流水线(buildctl 推送)/ dev project 配额 49.6GiB、prod 38.4GiB / dev 与 prod 各自 retention 每小时执行 + 全局 GC 每小时执行
一、问题现象
最近流水线一定几率出怪事(jar/war 的 stageBuildAndPushImage、vue 的 buildctl 段都涉及):
- Jenkins 构建成功,日志明确输出
✅ 镜像构建并推送完成: <镜像名> - 我到 Harbor UI 查这个镜像:不在
- 什么都不改,重新构建一次:又正常出现了
二、排查过程
2.1 先确认推送本身可不可信
我先排除流水线侧。读共享库 vars/stageBuildAndPushImage.groovy 的推送实现:buildctl --output "type=image,name=...,push=true" 是同步推送,Harbor 返回 2xx 后 buildctl 才退出 0;sh 步骤有 set -e,推送失败构建必然红。结论:日志里的成功是真实的,问题在 Harbor 侧。
2.2 第一嫌疑:GC 竞态(部分成立,但分量不够)
我的第一反应是 GC 竞态:GC 扫描期间正在推送的镜像,其 blob 可能因”暂无 manifest 引用”被当成孤儿删除(官方文档写明的已知行为)。打开清理服务页面核对:
- GC 定时:每小时一次
- 历史任务显示每次执行仅 11~15 秒(09:00:01→09:00:16)
窗口这么窄:每天 24 个窗口 × 15 秒 ≈ 每天 6 分钟风险敞口。能解释”一定几率”,但我觉得对不上实际丢失频率,继续往下找。
2.3 真凶:retention 规则在删新镜像
翻 dev project 的保留规则时找到了主因:
保留最近拉取的 5 个 artifacts,每小时执行
这条规则按 pull_time 排序,而刚推送完、还没被任何节点拉取的 tag,pull_time 为空,被当作最旧的候选优先删除(Harbor keep-recently-pulled 规则的已知行为,goharbor 仓库有相关 issue)。
我把丢失时间线完整推演了一遍,每步都对上:
- CI 推送完成(
isDeploy=false的纯构建,或 ArgoCD 尚未 sync)→ tag 在仓库里,pull_time 为空 - 下一个整点,retention 执行:这个从未拉取的新 tag 落在”最近拉取 5 个”之外 → 删除;随后 GC 回收 blob
- 我到 UI 查看:没了
- 重新构建:新 tag 推上去立刻查看还在(没到整点);若这次带部署,被 K8s 拉过就有了 pull_time,之后也不会被删——”又正常了”
三、根因分析
我确认两个定时任务都配在每个整点,与白天高频 CI(dev 每天 50 次构建)相遇:
| 角色 | 机制 | 影响 |
|---|---|---|
| 主因:retention「保留最近拉取 5 个」 | never-pulled 的新 tag(pull_time 为空)被当最旧候选,整点确定性删除 | 丢的是”推送后未被拉取”的构建(isDeploy=false / 未 sync),每个整点全仓库执行 |
| 次因:GC 每小时执行 | mark/sweep 的 15 秒窗口内推送中的 blob 被判孤儿删除 | 撞上整点窗口的推送才中招,概率低 |
四、解决方案
4.1 我的第一版方案(自己推演后否决)
最初想直接把 retention 改成「保留最近推送的 5 个」。推演时发现不行:镜像 A 部署上线后,CI 又连推 5 个测试构建(都没部署),A 被挤出保留集 → 节点重启 / Pod 漂移时 ImagePullBackOff,正在跑的服务起不来。生产场景不能这么配。
4.2 第二个顾虑:降频会不会爆盘
我又担心 retention 不每小时删、磁盘会爆。算了笔账:每天 50 次构建 × 400MB 是名义量,镜像层内容寻址去重、基础层共享,每天净增只有 2~5G;retention 降频只抬高日内波峰几 GB。后来想明白了:retention 频率根本不用动,要动的只有 GC。
4.3 最终方案:多规则 OR 组合(命中任一规则即保留)
| project | 规则组合 | 频率 |
|---|---|---|
| dev | ① 保留最近拉取 3 个 + ② 保留最近推送 3 个 | 每小时(不变) |
| prod | ① 保留最近推送 3 个 + ② 保留最近拉取 2 个 | 每小时 |
| 全局 GC | — | 每小时 → 每天凌晨一次(避开 CI 时段) |
配完的保护矩阵我自己核了一遍:
| 镜像状态 | 命中规则 | 结果 |
|---|---|---|
| 新推送、未部署拉取 | ② keep-pushed | 保留,不再误删 |
| 运行中、较早推送 | ① keep-pulled(imagePullPolicy: Always 使 pull_time 持续刷新) | 保留,节点重启可拉 |
| 两边都不沾的真旧镜像 | 无 | 删除,空间回收 |
prod 还留了一个后手没上:加第三条「保留最近 15~30 天内推送的」时间规则,兜住 values 里显式配了 imagePullPolicy: IfNotPresent 的应用(pull_time 不刷新,运行 tag 会同时掉出两个数量规则),顺带把回滚窗口从”3 个版本”拉长到半个月以上。先观察,有需要再加。
五、验证
配置完成后我做了三步验证:
- 手动触发一次
isDeploy=false的构建,新 tag 跨过一个整点再去查看——还在,retention 执行历史的删除名单里也没有它(主因关闭) - GC 挪到凌晨后,白天推送与 blob 回收不再相遇(次因关闭)
- 核对磁盘:dev 49.6G + prod 38.4G + 静态仓库约 29G ≈ registry 117G,稳态不涨
# 复核某个镜像是否真的不在(绕过 UI)
curl -k -u <账号>:<密码> https://<harbor域名>/v2/<project>/<repo>/tags/list
六、注意事项
keep-recently-pulled 规则会删 never-pulled 的新镜像——pull_time 为空被当最旧候选。CI 场景选保留规则必须同时考虑”新镜像保护”(keep-pushed)和”运行镜像保护”(keep-pulled),单规则顾此失彼,用多规则 OR 组合。
生产项目 retention 首次执行前必须模拟运行(dry-run)——逐仓库核对删除名单,出现”正在运行的 tag”就是红色警报。规则配错 + 首次执行 = 一批生产镜像被删。
GC 时段必须避开 CI 推送——GC 期间推送的镜像有被删风险是 Harbor 官方写明的行为,不是 bug。
imagePullPolicy: Always是 keep-pulled 保护运行镜像的前提(每次 Pod 重建刷新 pull_time);配了 IfNotPresent 的应用要靠时间规则兜底- 空间控制的正解是 retention 锁 tag 数 + 低频 GC 回收孤儿 blob,高频 GC 只能清边角料(有 tag 引用的 400MB 镜像它动不了),还制造竞态
- 磁盘账别按名义量算:层内容寻址去重后,50 次 × 400MB 的真实净增是 2~5G/天
- Jenkins 日志”推送成功”可信(buildctl 同步推送 + set -e),日志与仓库不一致时先查仓库侧定时任务
七、参考资料
- Harbor 官方文档:Garbage Collection
- goharbor/harbor 仓库 issues 检索 “retention recently pulled”(never-pulled 误删的相关报告)