代码与诗都写成文字

碧玉扉

Harbor镜像存储异常占用清理记录

发布于 # 服务器

Harbor镜像存储异常占用清理记录

学院的AI Station算力服务器分配给全体教师和学生使用,大约有140+用户,仅有的2TB空间难以支撑这么多人的镜像存储需求。服务器基本上每隔一段时间就会因为系统盘爆满而宕机。

除去资源配比问题外,主要原因包括:

  1. 资源使用粗放。启动容器时,系统存储在本地的2TB nvme上,数据挂载盘在管理节点的115TB机械硬盘上,巨大的差速迫使用户将模型权重放在非数据目录下;另一方面,部分用户存在习惯问题,把数据、日志等也打包到镜像中,导致镜像越来越大;
  2. 镜像构建方式不当。习惯于从别人的已有镜像出发,如果缺少自己要的包,就打包,然后重新构建镜像。这种分层打包的方式让大家的镜像越来越大,服务器运行之初每个人的镜像一般在12G左右,现在运行3年,镜像普遍到了40G,部分账户下生产出了60-100G的大镜像
  3. K8S版本过旧,且配置存在不当,这是每次大清理都会发现的主要问题

粗糙管理

一直以来的做法都是搞一个统计面板,看看哪个老师的学生使用的镜像太大或者太多了,去跟对方沟通一下,删掉太大的镜像。 惯用的做法是告诉每个老师,让每个学生的账号下最多不超过2个镜像,且每个镜像不超过30G,如果有超过30G的镜像,则只允许保留1个。

Harbor垃圾回收

手动清理悬空镜像

通过AI Station前端平台进行镜像删除后,Harbor中存在一些未被清理掉的悬空docker,需要在管理节点上手动执行清理命令。

docker system prune

必要时增加-a -f参数保证清理干净。

手动调用Harbor的垃圾回收

这是前期主要问题,即使删除了镜像,并调用了docker system prune -a -f,在df -h查看时仍然发现overlay2目录的存储占用极高,核心原因是k8s旧版本的garbage-collect没有自动执行(即使在网页端配置自动执行也无效),必须手动执行一次。

两种路径:1. 通过Harbor网页端手动执行垃圾清理;2. 通过docker exec进入到harbor的管理shell,手动执行garbage collect的指令。

Harbor上传缓存

这个问题是今天新发现的。

解决过程

早期设置120镜像,基本都能正常运转,现在降到60镜像,镜像尺寸也没有大太多,磁盘占用达到99%,基本可以确定是镜像存储中有一些并非镜像本身的冗余。

空间分析

docker system df显示

TYPE TOTAL ACTIVE SIZE RECLAIMABLE 
Images 47 47 9.129GB 1.233GB (13%) 
Containers 81 79 335.4MB 37.26MB (11%) 
Local Volumes 576 11 1.357kB 1.309kB (96%) 
Build Cache 0 0 0B 0B

从K8S本身看不出来有多少镜像占用。

但是df -h显示磁盘空间被overlay占满:

Filesystem               Size  Used Avail Use% Mounted on
# ......
overlay                  1.9T  1.8T   21G  99% /var/lib/data/docker/overlay2/e4846fe7cfebc268d113044ce0cac1f3d2d0a42a6ab2db90eb1e57d8bbeea28f/merged
overlay                  1.9T  1.8T   21G  99% /var/lib/data/docker/overlay2/5d0963303d7c9b6845dc5d93469442146c02a6fee99cd797ec03cbe56ed89fed/merged
overlay                  1.9T  1.8T   21G  99% /var/lib/data/docker/overlay2/cbb3836190072aa0951cbf511e30b542502e75e542c61edbaaf4462c767ff6d9/merged

对具体的目录进行分析,执行du -xh --max-depth=1 /var/lib/data | sort -h得到如下信息,和预期一样,主要问题在harbor存储下。

452K /var/lib/data/ldap 
525M /var/lib/data/etcd 
1.3G /var/lib/data/etcd_backup 
2.1G /var/lib/data/influxdb_backup 
11G /var/lib/data/docker 
14G /var/lib/data/mariadb_backup 
17G /var/lib/data/mysql 
26G /var/lib/data/log 
221G /var/lib/data/influxdb 
1.6T /var/lib/data/harbor 
1.8T /var/lib/data

继续深入查看du -xh --max-depth=2 /var/lib/data/harbor/registry/dockerregistry/v2/repositories | sort -h | tail -50找到最大的几个仓库

# ......
19G /var/lib/data/harbor/registry/docker/registry/v2/repositories/pytorch/hongtauo_cv-t13 
28G /var/lib/data/harbor/registry/docker/registry/v2/repositories/pytorch/pytorch-t7-t7-t7
29G /var/lib/data/harbor/registry/docker/registry/v2/repositories/pytorch/pytorch_deepspeed_torch_llama_factory-12.10-h1 
63G /var/lib/data/harbor/registry/docker/registry/v2/repositories/pytorch/pytorch 
68G /var/lib/data/harbor/registry/docker/registry/v2/repositories/pytorch/zrh_docreasoning-stuzrh 
99G /var/lib/data/harbor/registry/docker/registry/v2/repositories/other/lerobot-t13-t13 
103G /var/lib/data/harbor/registry/docker/registry/v2/repositories/other/zrh_doc-stuzrh 
267G /var/lib/data/harbor/registry/docker/registry/v2/repositories/other 
296G /var/lib/data/harbor/registry/docker/registry/v2/repositories/pytorch 
563G /var/lib/data/harbor/registry/docker/registry/v2/repositories

整个repositories目录占了563G空间,其中pytorch和other两个项目底下占用最多,这都符合预期。但从倒数8个目录之前出现了存储占用的显著不同,60G+还属于正常,但陡增到100+的存储显然不对。

然而,harbor的真实layer数据存储在/var/lib/data/harbor/registry/docker/v2/blobs下,这里的repositories存储的只是一些manifest、layer digest之间的link、上传过程中留下的数据等(此信息由ChatGPT提供)。显然manifest和link不可能有上百G的存储,那问题大概率出在上传过程。

另一方面,通过对比实际看到的镜像列表,并与对应镜像的使用人实际沟通,这个103G的镜像并没有创建成功,这些信息也能确认该目录下存在的是某些缓存。

对比blobs和repositories也能明显发现比例问题:

du -sh /var/lib/data/harbor/registry/docker/registry/v2/blobs /var/lib/data/harbor/registry/docker/registry/v2/repositories
987G /var/lib/data/harbor/registry/docker/registry/v2/blobs 
563G /var/lib/data/harbor/registry/docker/registry/v2/repositories

具体对最大的镜像目录进行大小分析,进一步确定是_uploads目录占用,应该是上传过程中服务器崩溃,留存下来的缓存文件。

du -xh --max-depth=3 /var/lib/data/harbor/registry/docker/registry/v2/repositories/other/zrh_doc-stuzrh | sort -h | tail -30 
# 大量4.0K的目录
4.0K /var/lib/data/harbor/registry/docker/registry/v2/repositories/other/zrh_doc-stuzrh/_uploads/6d0af006-f4dd-4792-9519-75dca2459ea1/hashstates 
4.0K /var/lib/data/harbor/registry/docker/registry/v2/repositories/other/zrh_doc-stuzrh/_uploads/b8c08c77-c085-4769-99f3-9936626c8880/hashstates 
88K /var/lib/data/harbor/registry/docker/registry/v2/repositories/other/zrh_doc-stuzrh/_layers 
88K /var/lib/data/harbor/registry/docker/registry/v2/repositories/other/zrh_doc-stuzrh/_layers/sha256 
131M /var/lib/data/harbor/registry/docker/registry/v2/repositories/other/zrh_doc-stuzrh/_uploads/6d0af006-f4dd-4792-9519-75dca2459ea1 
103G /var/lib/data/harbor/registry/docker/registry/v2/repositories/other/zrh_doc-stuzrh 
103G /var/lib/data/harbor/registry/docker/registry/v2/repositories/other/zrh_doc-stuzrh/_uploads 
103G /var/lib/data/harbor/registry/docker/registry/v2/repositories/other/zrh_doc-stuzrh/_uploads/b8c08c77-c085-4769-99f3-9936626c8880

接下来就要确定这些有没有在日常运行中被正常清理掉,先docker ps | grep registry找到registry容器(本机上为registry),再docker exec registry cat /etc/registry/config.yml,查看其中关于存储的字段,发现uploadpurging字段被disable掉了(即一开始说的厂商配置不当):

maintenance:
 uploadpurging:
  enabled: false

正常情况下它应该是类似如下的配置比较合理。

storage:
  maintenance:
    uploadpurging:
      enabled: true
      age: 168h
      interval: 24h
      dryrun: false

接下来就是分析占用量了。

find /var/lib/data/harbor/registry/docker/registry/v2/repositories \
  -type d -name _uploads \
  -exec find {} -mindepth 1 -maxdepth 1 -type d -mtime +7 -exec du -sh {} \; \; \
  2>/dev/null | sort -h

至此,问题分析完毕,可清理的空间高达560G。在多次上传中缓存的文件没有得到及时处理。 这个问题在网络上很少能找到类似情况的,看到的大多数清理都是处理日志占用之类的问题,关于上传缓存的清理没见过。

处置

这个处置就相对容易了,先通过Harbor UI将仓库设置成只读,然后直接脚本清理:

find /var/lib/data/harbor/registry/docker/registry/v2/repositories \
  -type d -name _uploads \
  -print0 |
while IFS= read -r -d '' dir; do
    find "$dir" \
      -mindepth 1 \
      -maxdepth 1 \
      -type d \
      -mtime +7 \
      -exec rm -rf {} +
done

然后设置一个脚本,每天晚上清理一次。

#!/bin/bash

BASE="/var/lib/data/harbor/registry/docker/registry/v2/repositories"
DAYS=7
LOG="/var/log/harbor-clean-uploads.log"

echo "[$(date '+%F %T')] start" >> "$LOG"

find "$BASE" -type d -name _uploads |
while IFS= read -r dir; do
    [ -z "$dir" ] && continue

    find "$dir" \
        -mindepth 1 \
        -maxdepth 1 \
        -type d \
        -mtime +"$DAYS" |
    while IFS= read -r upload; do
        [ -z "$upload" ] && continue

        echo "[$(date '+%F %T')] removing: $upload" >> "$LOG"
        # echo "would remove: $upload" # dry run,测试时可以先运行这一句代替下面的rm,确定避免误删
        rm -rf -- "$upload"
    done
done

df -h /var/lib/data >> "$LOG"
echo "[$(date '+%F %T')] done" >> "$LOG"

crontab -e配置计划任务

0 3 * * * /usr/local/sbin/harbor-clean-uploads.sh

查看运行日志就tail -100 /var/log/harbor-clean-uploads.log

关于harbor config

其实可以不自己写脚本,通过docker inspect registry --format '{{range .Mounts}}{{println .Source "->" .Destination}}{{end}}'找到registry的映射目录,找到config.yml文件,然后在里面修改maintance字段进行修改,再docker restart registry重启。

但这个可能是厂商的Harbor安装配置自动生成的,这个奇怪的系统有什么暗坑也不知道,为了避免麻烦,这里没有选择改配置。