第 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 的删除操作分两步:
- 删除引用:删除 Tag / Artifact 记录(UI 上立刻”看不到”了,但磁盘没变);
- 垃圾回收:扫描 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.yml 的 gc.worker_num 调小以降低 IO 压力。
Q6:如何防止某些”金贵”镜像被保留策略误删?
保留规则支持按仓库/标签 pattern 匹配,把生产核心仓库用精确 pattern(如 prod-app 且 matches v*)单独配一条 always 保留规则;更稳妥的是配合项目配额 + 只读权限,对生产项目禁止运维之外的删除操作。
总结
- 分清引用与数据:删 Tag ≠ 释放空间,Tag 保留策略负责删引用,GC 负责回收 Blob,二者缺一不可。
- 清理要自动化、要定时:用 REST API 把 Retention、GC、Purge Audit、无标签制品清理全部代码化,排进凌晨低峰期 cron,避免”想到才清”。
- 配额是最后一道防线:给每个项目配置存储配额,防止单个项目失控拖垮整个 Harbor。
- 先 dry-run 后执行:保留策略试运行、Python 清理脚本先跑 dry-run、GC 用
dry_run: true演练,确认影响面再真正动手。 - 监控闭环:用 Prometheus/Grafana 盯住存储增长与配额水位,趋势预警永远比事故处理便宜。
明天(第 20 天)我们继续深入 Harbor 的安全扫描:集成 Trivy/Clair 扫描镜像漏洞,让仓库里的每个镜像都”知根知底”。















暂无评论内容