大家在清理Harbor镜像仓库的时候,肯定都踩过坑:明明执行了GC(垃圾回收),却发现硬盘空间一点没减,那些本来要删掉的镜像层还占着位置。很多人第一反应是GC坏了,但其实90%的情况是Harbor的GC没找到「真正该删的层」——核心问题出在引用计数的残留和未完成上传的垃圾上,今天就把这些坑挖透。

一、为什么Harbor GC后镜像层没释放

1.1 先搞懂Harbor的GC是啥

Harbor是企业级的镜像仓库,GC就是把那些没被任何镜像用的「单独镜像层」删掉,给硬盘腾空间。但它不是乱删,是靠「引用计数」来判断的:每个镜像层都有个计数器,记录它被几个镜像(或者其他资源)引用着,只有计数器为0的层,GC才会删。 举个生活化的例子:你有一堆乐高积木(对应镜像层),你用这些积木搭了3个机器人(对应3个镜像),每个机器人都用到了蓝色的积木——那蓝色积木的引用计数就是3。如果删掉1个机器人,蓝色积木的计数变成2,GC就不会删它;只有3个机器人都删了,计数变0,GC才会收走蓝色积木。

1.2 核心问题:不是GC懒,是「计数器没清零」

很多人执行GC之后,以为删掉镜像就完事了,但实际上,Harbor的引用计数有时候会「卡住」,明明对应的镜像已经删了,计数器还挂着1,或者干脆有一些没被正常记录的「虚拟引用」在,导致GC不敢动手。

二、关键机理:引用计数的坑

2.1 啥是Harbor里的镜像层引用?

Harbor里的镜像层引用,不是我们肉眼能看到的,而是存在数据库里的数字。每个层在创建的时候,都会被记录它被哪些镜像、哪些项目甚至哪些上传会话引用。举个例子:你上传一个nginx镜像,它有10个层,这10个层的引用计数都加1,只要镜像存在,计数就不会掉。 那什么时候计数会卡住?比如:上传镜像到一半,突然断网了,这个上传没完成,Harbor数据库里会留下一个「未完成的上传会话」,里面还记着这个层的引用,虽然这个层根本没被完整创建成可用的镜像,但它的引用计数还是1——GC看到计数不是0,就不会删这个层,哪怕你后来删掉了没完成的镜像(其实根本没建完)。

2.2 怎么查看引用计数有没有问题?

这里给个具体的操作示例,全用shell命令(单一技术栈):

# 先找到Harbor的数据库容器名,一般是harbor-db,用docker ps命令确认
docker ps | grep harbor-db
# 然后登录到PostgreSQL数据库(Harbor默认用PostgreSQL存数据)
docker exec -it harbor-db容器名 psql -U postgres
# 进入数据库后,查询所有镜像层的引用计数,找那些计数不为0但实际已经没被使用的层
SELECT digest, size, reference_count FROM harbor.repositories WHERE name LIKE 'library/%';

这个示例里,digest就是镜像层的唯一标识,size是占的空间,reference_count就是我们说的计数器。比如你删了一个叫library/nginx:1.18的镜像,但它的某个层的reference_count还是1,那大概率是残留问题。

三、最容易踩的坑:未完成上传的残留

3.1 未完成上传的层是怎么来的?

日常操作中,我们经常会遇到上传镜像时断网、服务器重启、手动中断上传的情况,这些都会留下「未完成的上传会话」——也就是Harbor里的uploadsession表。这些会话里存着已经上传了一部分的镜像层数据,虽然层没完整建好,但会话会记着这个层的引用,导致它的引用计数一直是1。 举个生活化的例子:你拼乐高机器人,拼到一半,突然有事停了,这个半拉的机器人(对应未完成上传的层)会占着蓝色积木的位置(对应引用计数),哪怕这个半拉机器人永远不会拼完,蓝色积木也没法被其他模型用。

3.2 怎么排查和清理这些残留?

还是用shell命令,示例:

# 登录数据库后,查询所有未完成超过7天的上传会话(默认7天是残留的临界值,可根据情况调整)
SELECT * FROM harbor.uploadsession WHERE created_at < NOW() - INTERVAL '7 days';
# 然后把这些会话删掉,这样对应的层的引用计数就会清零,GC下次执行就能删掉这些层了
DELETE FROM harbor.uploadsession WHERE created_at < NOW() - INTERVAL '7 days';

如果执行完还是没效果,可以手动触发一次GC,用shell命令:

# 触发Harbor的GC,替换成你自己的admin用户token
curl -X POST "http://你的Harbor地址/api/v2.0/garbage-collections" -H "Authorization: Bearer 你的admin-token" -H "Content-Type: application/json" -d '{"dry_run": false}'
# dry_run设为false就是真执行回收,设为true是模拟,建议先模拟确认要删的内容

四、应用场景、技术优缺点、注意事项

4.1 应用场景

这个问题的应用场景非常广,尤其是在企业级使用Harbor的场景:比如团队每天要上传大量测试镜像,经常遇到上传中断;或者项目迭代快,频繁删除旧镜像;或者Harbor用了很久,积累了很多未完成的上传层——这些场景下,很容易出现GC后磁盘空间没释放的情况,导致仓库磁盘占满,没法上传新镜像。

4.2 技术优缺点

先讲Harbor引用计数机制的优点:它非常可靠,不会随便删层,避免了误删有用的镜像层,适合对数据可靠性要求高的企业场景;但缺点也很明显:当出现未完成上传或者引用计数卡住的情况,GC就失效了,而且普通用户很难排查出来,需要操作数据库,门槛有点高。

4.3 注意事项

这里要列几个关键的注意事项,避免踩坑:

  1. 一定要先备份数据库,再执行任何删除操作,尤其是删uploadsession或者修改引用计数的时候,万一删错了,数据库恢复麻烦;
  2. 手动触发GC之前,先执行dry_run模式,看看GC要删哪些层,确认没用了再真删;
  3. 日常操作中,上传镜像尽量不要中断,尤其是大镜像,避免留下未完成的会话;
  4. 定期(比如每周)检查uploadsession表,清理7天以上的残留会话,避免积累太多垃圾。

五、总结

其实Harbor GC后镜像层没释放,根本不是GC的问题,而是引用计数的「假阳性」——要么是有未完成的上传会话占着引用,要么是引用计数没正常清零。只要找到这些残留的会话或者卡住的计数,清理之后,GC就能正常工作,磁盘空间也能真正释放。不用怀疑GC的能力,90%的情况都是我们没找到GC要删的层而已。