Jenkins 推送成功但 Harbor 查无镜像——retention 误删新镜像与 GC 竞态的问题

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 构建成功,日志明确输出 &#x2705; 镜像构建并推送完成: <镜像名>
  • 我到 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)。

我把丢失时间线完整推演了一遍,每步都对上:

  1. CI 推送完成(isDeploy=false 的纯构建,或 ArgoCD 尚未 sync)→ tag 在仓库里,pull_time 为空
  2. 下一个整点,retention 执行:这个从未拉取的新 tag 落在”最近拉取 5 个”之外 → 删除;随后 GC 回收 blob
  3. 我到 UI 查看:没了
  4. 重新构建:新 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),日志与仓库不一致时先查仓库侧定时任务

七、参考资料

暂无评论

发送评论 编辑评论


				
|´・ω・)ノ
ヾ(≧∇≦*)ゝ
(☆ω☆)
(╯‵□′)╯︵┴─┴
 ̄﹃ ̄
(/ω\)
∠( ᐛ 」∠)_
(๑•̀ㅁ•́ฅ)
→_→
୧(๑•̀⌄•́๑)૭
٩(ˊᗜˋ*)و
(ノ°ο°)ノ
(´இ皿இ`)
⌇●﹏●⌇
(ฅ´ω`ฅ)
(╯°A°)╯︵○○○
φ( ̄∇ ̄o)
ヾ(´・ ・`。)ノ"
( ง ᵒ̌皿ᵒ̌)ง⁼³₌₃
(ó﹏ò。)
Σ(っ °Д °;)っ
( ,,´・ω・)ノ"(´っω・`。)
╮(╯▽╰)╭
o(*////▽////*)q
>﹏<
( ๑´•ω•) "(ㆆᴗㆆ)
😂
😀
😅
😊
🙂
🙃
😌
😍
😘
😜
😝
😏
😒
🙄
😳
😡
😔
😫
😱
😭
💩
👻
🙌
🖕
👍
👫
👬
👭
🌚
🌝
🙈
💊
😶
🙏
🍦
🍉
😣
Source: github.com/k4yt3x/flowerhd
颜文字
Emoji
小恐龙
花!
上一篇