生产环境 DevOps 实战 | 第 19 天:Harbor 镜像清理与存储策略——回收日程与 GC 机制

第 19/60 天

引言

前面我们完成了 kaniko 构建镜像(第 15 天)、Harbor 安装(第 16 天)、项目隔离(第 17 天)和跨机房复制(第 18 天),一套能”把镜像存进去”的 Harbor 已经跑起来了。但真正做过生产运维的人都知道:能存进去不难,能持续存下去才难

随着发布频率提升,Harbor 的存储会以肉眼可见的速度膨胀:每次 CI 构建都产生新 digest、漏配清理策略的测试项目、被复制规则”搬”过来的镜像、无人维护的旧版本……几个月后 df -h 就会开始告警。更让人困惑的是:明明在界面上删除了几十个镜像,磁盘占用却一点没降——这是因为 删除 Tag 只是删除了”引用”,真正释放空间要靠 GC(Garbage Collection)

本文围绕「Harbor 镜像清理与存储策略」展开,覆盖:Harbor 存储结构剖析、Tag 保留策略(Retention Policy)、GC 机制与在线回收、配额控制、审计日志清理,以及如何用 REST API 和 Python 把清理工作全部自动化,最后附上生产环境常见问题与排查思路。

核心概念

1. 镜像的三层结构:Tag、Digest、Blob

要理解清理机制,必须先分清三个概念:

概念 说明 类比
Tag 人类可读的版本号(如 v1.2.0),本质是指向 Manifest 的引用 便利贴
Digest Manifest 内容的 SHA256 校验和,同一内容必有同一 digest 身份证号
Blob 镜像的实际分层数据(layer),可被多个镜像共享 文件本身

关键推论:

  • 一个 digest 可以挂多个 tag,删掉其中一个 tag,其他 tag 依然引用同一份数据;
  • tag 全部被删后,Manifest 和 Blob 并不会立即消失,它们变成了”孤儿数据”;
  • 不同镜像之间可能共享同一层 Blob(基础镜像层),GC 时 Harbor 会做引用计数,不会误删还在被引用的层

2. 为什么”删除镜像是假的删除”

Harbor 的删除操作分两步:

  1. 删除引用:删除 Tag / Artifact 记录(UI 上立刻”看不到”了,但磁盘没变);
  2. 垃圾回收:扫描 registry 存储,找出没有任何引用指向的 Blob 并物理删除(此时磁盘才释放)。

所以生产环境的标准姿势是:Tag 保留策略定期清理引用 + GC 定期物理回收 + 配额兜底,三管齐下。

3. 四类清理手段对比

手段 作用对象 是否释放磁盘 建议频率
手动删除 Tag/Artifact 引用 否(需配合 GC) 按需
Tag 保留策略(Retention) 按规则自动删 Tag 否(需配合 GC) 每天/每周
GC 垃圾回收 孤儿 Blob 每天低峰期
项目存储配额(Quota) 项目级上限 预防性,不删除 配置一次

另有两类”存储之外”的清理:Harbor 数据库(PostgreSQL)的冗余数据、audit_log 审计日志,它们同样会拖慢 Harbor,需要定期 purge。

4. 在线 GC:Harbor 2.x 的关键升级

Harbor 1.x 时代,GC 必须先把 registry 切到只读模式,否则并发推拉会出错;Harbor 2.x 引入在线 GC(Online GC),GC 期间无需停服,配合 manifest 引用计数保证并发安全。生产环境升级到 2.x 后,可以放心把 GC 放进凌晨定时任务。

实战步骤

第一步:评估存储现状

动手清理前,先搞清楚”钱花在哪了”。登上海报机或通过 API 查看:

# 1. 宿主机上查看 Harbor 各数据目录占用(registry 是镜像本体)
sudo du -sh /data/registry/docker/registry/v2
sudo du -sh /data/database /data/redis /data/jobservice

# 2. 找出占用最大的前 10 个仓库目录
sudo du -sh /data/registry/docker/registry/v2/repositories/* | sort -rh | head -10

# 3. 通过 Harbor v2 API 查看各项目的配额使用情况(配额需先在 UI 开启)
HARBOR="https://harbor.internal.local"
curl -s -u "admin:ChangeMe123!" "$HARBOR/api/v2.0/quotas?reference=project" 
  | jq '.[] | {id: .ref.id, name: .ref.name, hard: .hard, used: .used}'

输出示例(配额按项目展示):

{
  "id": 6,
  "name": "devops",
  "hard": { "storage": 107374182400 },
  "used": { "storage": 42949672960 }
}

这个命令能快速定位”哪个项目在吃磁盘”,通常答案都是那几个忘了配清理策略的 CI 项目。

第二步:配置 Tag 保留策略(自动删引用)

在 UI 上「项目 → 标签保留」可以图形化配置,但生产环境推荐用 API 做成代码,便于版本化。以下规则实现:每个仓库保留最近 30 个被拉取过的 Tag(latestPulledK),超过 90 天未推送的 Tag 全部删除(nDaysSinceLastPush),每晚 2 点执行

curl -s -u "admin:ChangeMe123!" -X POST "$HARBOR/api/v2.0/retentions" 
  -H "Content-Type: application/json" 
  -d '{
    "algorithm": "or",
    "rules": [
      {
        "disabled": false,
        "action": "retain",
        "template": "latestPulledK",
        "params": { "latestPulledK": 30 },
        "tag_selectors": [
          { "kind": "doublestar", "decoration": "matches", "pattern": "**" }
        ],
        "scope_selectors": {
          "repository": [
            { "kind": "doublestar", "decoration": "repoMatches", "pattern": "**" }
          ]
        }
      },
      {
        "disabled": false,
        "action": "retain",
        "template": "nDaysSinceLastPush",
        "params": { "nDaysSinceLastPush": 90 },
        "tag_selectors": [
          { "kind": "doublestar", "decoration": "matches", "pattern": "**" }
        ],
        "scope_selectors": {
          "repository": [
            { "kind": "doublestar", "decoration": "repoMatches", "pattern": "**" }
          ]
        }
      }
    ],
    "trigger": {
      "kind": "Schedule",
      "settings": { "cron": "0 0 2 * * *" }
    }
  }' | jq

注意 algorithm: "or":多条规则取并集保留,最终”不在任何保留规则内”的 Tag 才会被删。常见模板:latestPushedK(最近 N 个推送)、latestPulledK(最近 N 个被拉取)、nDaysSinceLastPush/nDaysSinceLastPull(按天数)。

首次配置建议在使用 ?dry_run=true 的预览模式(UI 上「试运行」按钮)确认不会误删核心版本,再正式启用。

第三步:配置并触发 GC(真正释放空间)

GC 可以在 harbor.yml 中预配置并发度,重新 ./install.sh 生效:

# harbor.yml(Harbor 2.4+ 支持 gc 段)
hostname: harbor.internal.local
http:
  port: 80
https:
  port: 443
  certificate: /data/cert/harbor.crt
  private_key: /data/cert/harbor.key

gc:
  # 垃圾回收并发 worker 数量,默认 1,磁盘 IO 紧张的机器不要开太大
  worker_num: 4
  # 写成 true 时 GC 只扫描不删除,用于演练
  dry_run: false

database:
  password: "SuperSecretDbPwd"

用 API 立即触发一次 GC 并查看结果:

# 立即执行一次 GC(type 为 Manual 表示手动触发)
curl -s -u "admin:ChangeMe123!" -X POST 
  "$HARBOR/api/v2.0/system/gc/schedule" 
  -H "Content-Type: application/json" 
  -d '{"schedule": {"type": "Manual"}}' | jq

# 查看最近 GC 任务状态
curl -s -u "admin:ChangeMe123!" "$HARBOR/api/v2.0/system/gc" 
  | jq '.[] | {id, job_status, creation_time, update_time}'

# 查看指定 GC 任务的执行日志(GC 干了什么、删了多少)
curl -s -u "admin:ChangeMe123!" "$HARBOR/api/v2.0/system/gc/16/log" 
  | jq -r '.content' | tail -30

定时 GC 建议:生产环境把 GC 排进每天凌晨低峰期(镜像构建少、拉取少),与保留策略错开 1 小时,避免两个大任务挤在一起。Harbor 2.x 在线 GC 不会阻塞推拉,但大量 IO 仍会影响性能,选凌晨最稳妥。

第四步:清理审计日志(Purge Audit Log)

时间一长,audit_log 表会膨胀到几十 GB,拖慢 API。通过 API 配置定期清理——保留最近 30 天(720 小时)的审计记录,每天凌晨 3 点执行

curl -s -u "admin:ChangeMe123!" -X POST 
  "$HARBOR/api/v2.0/system/purgeaudit/schedule" 
  -H "Content-Type: application/json" 
  -d '{
    "schedule": { "type": "Custom", "cron": "0 0 3 * * *" },
    "audit_retention_hour": 720,
    "include_operations": ["create", "delete", "pull"]
  }' | jq

如果合规要求必须留痕,把 audit_retention_hour 调大,或者把审计日志接入外部日志系统后,再放心执行 purge。

第五步:用 Python 全自动化——无标签制品批量清理

CI 一旦跑起来,”无 tag 制品”(untagged artifact)是存储膨胀的头号来源——构建失败、重复推送、被策略删除 tag 后都会残留。以下脚本扫描指定项目,列出所有无标签制品供人工复核(DELETE 请求默认注释,确认无误后取消注释启用):

#!/usr/bin/env python3
"""Harbor 无标签制品清理脚本:默认 dry-run,加 --delete 才真正删除"""
import base64
import json
import sys
import urllib.request

HARBOR = "https://harbor.internal.local"
USER, PASS = "robot$devops+cleaner", "RobotSecret2026"  # robot 账号需 delete 权限
PROJECT = "devops"
DELETE = "--delete" in sys.argv


def api(path, method="GET"):
    req = urllib.request.Request(f"{HARBOR}/api/v2.0{path}", method=method)
    token = base64.b64encode(f"{USER}:{PASS}".encode()).decode()
    req.add_header("Authorization", f"Basic {token}")
    req.add_header("Content-Type", "application/json")
    with urllib.request.urlopen(req) as r:
        body = r.read()
        return json.loads(body) if body else None


repos = api(f"/projects/{PROJECT}/repositories?page_size=100")
total = 0
for repo in repos:
    name = repo["name"]
    arts = api(f"/projects/{PROJECT}/repositories/{name}"
               f"/artifacts?page_size=100&with_tag=true")
    untagged = [a for a in arts if not a.get("tags")]
    if not untagged:
        continue
    print(f"[{name}] 发现 {len(untagged)} 个无标签制品")
    for a in untagged:
        short = a["digest"].split(":")[1][:12]
        print(f"    {short}  created={a.get('extra_attrs', {}).get('created')}")
        if DELETE:
            api(f"/projects/{PROJECT}/repositories/{name}"
                f"/artifacts/{a['digest']}", method="DELETE")
            total += 1

print(f"完成:{'已删除' if DELETE else '待删除(dry-run)'} {total} 个制品")

把脚本挂进 crontab:先 dry-run 一周确认结果稳定,再换 --delete 并加 2>&1 | tee -a /var/log/harbor_clean.log 留痕。

第六步:监控与告警闭环

清理策略只是”节流”,还得有”水位线”:

# Prometheus node_exporter 或黑盒脚本定期上报
# 简单做法:cron 里检查 Harbor 数据目录使用率,超过 80% 告警
THRESHOLD=80
USED=$(df /data/registry | awk 'NR==2 {print $5}' | tr -d '%')
if [ "$USED" -gt "$THRESHOLD" ]; then
  curl -s -X POST "https://oapi.dingtalk.com/robot/send?access_token=REPLACE_ME" 
    -H "Content-Type: application/json" 
    -d "{"msgtype":"text","text":{"content":"[Harbor] 存储使用率 ${USED}% 超过阈值 ${THRESHOLD}%"}}"
fi

配合 Harbor 导出的 Prometheus 指标(harbor_project_quota_usage_bytes 等),可以在 Grafana 上画出”存储增长曲线”,趋势异常时提前介入,而不是等磁盘告警。

常见问题

Q1:我在界面上删了几十个镜像,为什么磁盘空间一点没少?
删除 Tag/Artifact 只移除了引用,不会删除底层 Blob。必须再执行一次 GC,让 Harbor 扫描并回收没有引用的孤儿数据,磁盘才会真正释放。这也是”Retention + GC”必须成对出现的原因。

Q2:保留策略(Retention)和 GC 有什么区别?我能只配一个吗?
Retention 负责”按规则删除 Tag 引用”(比如只保留最近 30 个),GC 负责”物理删除无引用的 Blob”。只配 Retention 不配 GC,引用没了但空间不释放;只配 GC 不配 Retention,无引用数据很快又会被新镜像堆满。生产环境两者都要配。

Q3:Harbor 2.x 的在线 GC 期间,拉取镜像会失败吗?
不会。在线 GC 通过 Manifest 引用计数保证并发安全:正在被拉取的 Blob 会被标记为”在用”,GC 跳过它们。不过 GC 是磁盘 IO 密集型任务,建议还是安排在凌晨低峰期,避免与大量构建/拉取争抢 IO。

Q4:多个镜像共享基础层,GC 会不会误删?
不会。Harbor 基于 Blob 的引用计数做回收,只要有一个 Manifest 仍引用该层,该 Blob 就不会被删除。跨项目共享层同样安全(前提是所有项目都在同一个 Harbor 实例内)。

Q5:GC 任务卡住或失败了怎么办?
常见原因:磁盘满导致扫描写日志失败、数据库连接异常、同时多个 GC 任务排队。排查步骤:先 GET /api/v2.0/system/gc 看任务状态,再拉 /system/gc/{id}/log 看日志;如果是文件系统问题,清理 /data/registry 所在磁盘的临时文件后重试。若 GC 一直失败,可考虑把 harbor.ymlgc.worker_num 调小以降低 IO 压力。

Q6:如何防止某些”金贵”镜像被保留策略误删?
保留规则支持按仓库/标签 pattern 匹配,把生产核心仓库用精确 pattern(如 prod-appmatches v*)单独配一条 always 保留规则;更稳妥的是配合项目配额 + 只读权限,对生产项目禁止运维之外的删除操作。

总结

  1. 分清引用与数据:删 Tag ≠ 释放空间,Tag 保留策略负责删引用,GC 负责回收 Blob,二者缺一不可。
  2. 清理要自动化、要定时:用 REST API 把 Retention、GC、Purge Audit、无标签制品清理全部代码化,排进凌晨低峰期 cron,避免”想到才清”。
  3. 配额是最后一道防线:给每个项目配置存储配额,防止单个项目失控拖垮整个 Harbor。
  4. 先 dry-run 后执行:保留策略试运行、Python 清理脚本先跑 dry-run、GC 用 dry_run: true 演练,确认影响面再真正动手。
  5. 监控闭环:用 Prometheus/Grafana 盯住存储增长与配额水位,趋势预警永远比事故处理便宜。

明天(第 20 天)我们继续深入 Harbor 的安全扫描:集成 Trivy/Clair 扫描镜像漏洞,让仓库里的每个镜像都”知根知底”。

© 版权声明
THE END
喜欢就支持一下吧
点赞0 分享
评论 抢沙发
头像
欢迎您留下宝贵的见解!
提交
头像

昵称

取消
昵称表情代码图片快捷回复

    暂无评论内容