开发机缓存清理实战:用 repo-sweeper 安全回收磁盘空间
2026/9/16 5:13:12 网站建设 项目流程

如果你是个靠命令行吃饭的开发者,大概率经历过这一幕:IDE 运行到一半突然弹出磁盘空间不足,随后整个系统开始卡顿;或者你在凌晨收到 CI 的失败通知,原因不是代码问题,而是构建节点的磁盘被依赖缓存塞满。提到开源仓库、包缓存这些词,很多人第一反应是 Maven 中央仓库、npm registry 这类远程服务,但真正让开发者磁盘告急的,往往是本地各种包管理器默默堆起的依赖缓存。我以前处理这种问题的方式很简单——按大小排序,找到那些藏在 home 目录下的仓库缓存,然后手动删除。删完之后确实爽了,可下次构建又会重新下载,而且如果手一抖删错了目录,等着你的就是更长的构建时间。

后来我实在受不了这种反复折腾,干脆动手写了一个开源的仓库包缓存清理工具。它做的事情很专一:识别开发机上常见的包管理器缓存目录,统计每个目录里到底有多少东西,然后用尽量安全的方式帮你回收磁盘空间。几个月用下来,几台开发机都靠它续了命。这篇就聊聊这个工具的来龙去脉、核心实现,以及使用过程中几个容易被忽略的坑。

1. 磁盘告急的元凶:各家仓库缓存的真实体量

1.1 Maven、Gradle、npm 和 pip 的缓存目录分别有多大

先给一张我平时排查开发机磁盘时最常看到的表,这些目录都属于"远程仓库的本地缓存",也是这个工具默认扫描的范围。

包管理器常见缓存路径典型体积说明
Maven~/.m2/repository5 - 30 GBjar、pom、lastUpdated 文件长期累积
Gradle~/.gradle/caches/modules-28 - 50 GB依赖转换产物比依赖本身更占空间
npm~/.npm/_cacache2 - 15 GB内容寻址缓存,同一个包可能多份元数据
pnpm~/.local/share/pnpm/store3 - 20 GB硬链接节省空间,但旧版本不自动清理
pip~/.cache/pip1 - 10 GB下载的 wheel 和源码包缓存
Cargo~/.cargo/registry2 - 8 GBcrate 源码与索引

这张表里的数据是我从自己维护的几台开发机和一些用户反馈里汇总出来的。不同项目差异很大,但有一个规律很稳定:只要你在一个环境里长期切换多个项目,这些目录的体积就会一路往上走,而且几乎不会主动降下来。

可能有人会问,Docker 镜像仓库算不算?Docker 的/var/lib/docker确实也是磁盘杀手,但它的清理逻辑和包缓存完全不同,镜像之间的层共享关系很复杂,直接按目录删非常容易造成浪费甚至污染。所以我在工具里把 Docker 镜像缓存排除在外,只单独提示用户用docker system prune去处理。

1.2 缓存只增不减:版本累积与失效文件的叠加

包缓存为什么会长这么大?很多人以为就是"依赖下载多了",其实有三个机制在背后叠加。

第一是版本累积。一个项目升级依赖之后,旧版本不会被自动删除。Maven 仓库里同一个groupId:artifactId可能同时躺着四五个版本,每个版本都包含 jar 和 pom,时间一长自然膨胀。Gradle 更夸张,它除了模块缓存,还有transforms目录,这个目录里放的是依赖处理过程中生成的转换产物,每次构建环境变化都可能生成新的一份,旧的不清理。

第二是失败残留。Maven 的lastUpdated文件、pip 的.part临时文件、npm 的日志,这些都是在下载失败或中断时留下的。单个文件不大,但数量可以到几十万,扫出来全是几 KB 的小文件,特别占 inode。

第三是"不删策略"。很多包管理器的缓存设计目标是确保并发安全和网络不佳时的可用性,所以没有主动清理逻辑。npm 的_cacache就是一个内容寻址存储,新增缓存永远安全,但旧数据永远不被回收。设计上这是特性,现实里就成了磁盘压力来源。

明白了这些机制,你就知道为什么需要一个专门的清理工具:不是简单删一个目录,而是要把"可删的"和"不能删的"分开,把"旧版本的"和"当前正在用的"分开。

2. 设计取舍:为什么我不建议直接执行 rm -rf

2.1 手动删除缓存的三类隐患

在没有工具之前,我也是靠durm走天下,踩了几次坑之后才认真反思。

第一个隐患是删除正在使用的缓存。如果 IDE 或构建进程正在读某个 jar 或某个缓存索引,你这边直接rm -rf,那边可能就会出现找不到文件的报错,最典型的是 Maven 在构建中突然报Could not resolve dependencies。虽然重新下载能解决,但那种中断非常烦人。

第二个隐患是权限。很多 CI 镜像或 Docker 容器里,缓存目录是由 root 创建的,普通用户去删会提示 Permission denied。更麻烦的是,如果你用了sudo rm -rf删掉了但又没有立刻重建目录,后续构建工具可能会以错误的所有权重新创建目录,导致其他用户无法读写。

第三个隐患是不可恢复。删除之后如果需要离线构建,或者网络很差,重新拉取依赖的成本可能比磁盘空间本身更昂贵。尤其是那种还需要从内部私有仓库拉取快照版本的项目,一旦缓存没了,构建时间能从两分钟变成二十分钟。

2.2 工具定位:只删"能删的",不碰"正在用的"

所以我给工具定了一个非常克制的目标:默认情况下,只删三类内容。

  • 明确标记为临时或失效的文件,比如.lastUpdated.part、下载中断产生的临时文件。
  • 超过指定天数没有被访问的缓存项,默认 30 天。
  • 同一个依赖的旧版本,比如某个groupId:artifactId下保留最近 2 个正式版本,更旧的版本才清理。

这个思路是从"回收空间"和"保留可靠性"之间取一个平衡。正式版本通常还能从远程仓库拉回来,但保留最近两个版本是为了避免项目突然切回旧版本时需要重新下载。快照版本则默认保留最近 3 天的记录,因为快照更新频繁,旧的很快失效,但三天内的内容可能是当前正在用的。

工具还提供--aggressive参数,允许用户明确表示可以清空某个目录下的全部缓存。这个参数我特意放在命令末尾,并且会再次确认,就是为了避免有人手滑。

2.3 技术选型与开源边界

选择用 Python 实现,主要是看中它的跨平台能力和开发速度。路径处理、文件遍历、符号链接判断都有现成的标准库,不需要额外依赖。CLI 部分用了 Click,输出格式用了 JSON 和表格两种模式,方便脚本调用。

项目一开始只是我一个人内部用的小脚本,后来发现团队里其他人也有同样的磁盘问题,才决定把它开源出来。开源之后收到最多的不是需求,而是"这个工具会不会误删我的缓存"。为了回答这个问题,我把默认行为做得非常保守,并且把所有删除动作都设计成先回收站、后物理删除。所谓开源边界,就是明确告诉用户:这个工具只处理仓库包缓存,不碰你的项目源码,不碰构建输出目录,不碰系统日志,也不碰 Docker 镜像。

3. 核心实现:路径探测、空间估算与安全删除

3.1 跨平台路径探测:环境变量与常见目录的兜底策略

工具第一件事是找到所有缓存目录。这里没有想象中那么简单,因为不同操作系统的路径差异很大。比如 npm 在 Linux 和 macOS 上默认是~/.npm,但在 Windows 上可能是%LocalAppData%\npm-cache。Maven 在 Windows 上同样是~/.m2/repository,但~的展开方式不同。

我的实现分三步走。

  • 先读环境变量,比如XDG_CACHE_HOMEMAVEN_OPTSNPM_CONFIG_CACHE,如果用户显式配置了自定义路径,优先使用。
  • 再检查平台相关的默认路径,通过Path.home()获得用户目录,然后拼接/.m2/.gradle/.npm等。
  • 最后做存在性检查,只有目录真实存在才纳入扫描范围。

这种兜底策略解决了大部分情况。不过还是有一个容易翻车的地方:用户通过 Maven 的settings.xml把本地仓库指向了自定义目录,或者 Gradle 的GRADLE_USER_HOME被修改过。工具目前会在扫描时自动读取settings.xml中的localRepository配置,以及环境变量GRADLE_USER_HOME,避免出现"扫了半天扫了个寂寞"的尴尬。

3.2 可回收空间估算:区分真正缓存与临时文件

扫描缓存目录的时候,如果单纯计算总大小,意义不大,因为用户真正关心的是"我能安全回收多少"。所以工具会把每个目录内部细分成几类。

  • cached_artifacts:已经完整下载的依赖包,属于"可以删但可能要重下"的部分。
  • temporary:下载中断产生的临时文件、Markers、lastUpdated,属于"删了没有任何影响"的部分。
  • metadata:索引、描述文件、锁文件,这类文件体积小但数量多,删除可能导致重新生成索引,所以默认保留。
  • transforms:Gradle 依赖转换产物,这类最占空间,但删除后下次构建会自动重新生成,只是会变慢,默认纳入清理范围。

统计大小用的是os.scandir加递归遍历,同时对符号链接做了严格处理,避免循环引用导致死循环。遍历过程中会遇到权限不足的目录,工具会记录到 warning 列表,而不是直接报错退出。

还有一个细节是时间判断。很多人以为可以用 atime(访问时间)来判断缓存是否最近被用到,但 Linux 默认开启relatime,atime 并不可靠。所以我实际用的是 mtime(修改时间)和 ctime(状态变更时间)结合,一个文件如果超过 30 天都没被修改过,说明它大概率是"只读的旧缓存",删除风险较低。

3.3 删除策略:回收站优先、分批处理、保留最新版本

删除这一步是所有安全设计里最重要的。工具默认不会直接物理删除,而是把符合条件的文件移动到一个由工具管理的回收站目录里,路径类似~/.cache/repo-sweeper/trash。移动操作跨文件系统时可能会慢,所以在同一分区内会优先用os.rename,不同分区则用shutil.move

为了应对几十万小文件的场景,删除是分批进行的。每处理完 1000 个文件,就暂停 0.5 秒,这样不会让磁盘 IO 长时间冲高,也保留了一点容错空间。如果某个文件正在被进程占用导致无法删除,工具会记录下来并在最后生成一份报告,而不是中断整个流程。

"保留最新版本"这个逻辑主要针对 Maven 和 Gradle 的模块目录。实现上会先解析目录结构,把groupId/artifactId/version拆出来,然后对同一个groupId:artifactId下的 version 做排序。默认保留最高的两个 release 版本和一个最新的 snapshot 版本,其余标记为可清理。npm 和 pip 的内容寻址缓存不太好按版本粒度清理,所以工具直接走"时间阈值 + 临时文件"策略,不硬套版本保留逻辑。

下面是简化后的核心清理逻辑示例,方便你理解思路,实际代码里还会加入更多异常分支。

def clean_module_dir(module_path, keep_releases=2): versions = list_version_dirs(module_path) releases = sorted([v for v in versions if is_release(v)]) for old_version in releases[:-keep_releases]: target = module_path / old_version move_to_trash(target)

4. 实际使用与实测:一条命令能找回多少磁盘空间

4.1 安装、扫描与清理的三条命令

工具的使用方式非常直接。项目主页提供源码和打包好的可执行文件,安装之后就三个常用命令。

repo-sweeper scan repo-sweeper dry-run --keep-releases 2 --keep-snapshot-days 3 repo-sweeper clean --keep-releases 2 --keep-snapshot-days 3 --strategy trash

scan用于快速了解当前机器上有哪些缓存目录,每个目录多大,可回收空间是多少。dry-run会在不删任何文件的情况下,打印一份"将要被清理的文件列表",同时给出总大小和文件数量。clean才是真正执行清理,默认策略是回收站模式。

第一次清理新机器时,我强烈建议先跑scandry-run各一次,确认输出符合预期再执行clean。即便clean用了回收站模式,也应该尽量避免在构建过程中做这件事,因为并发访问还是可能产生一些不可预期的问题。

4.2 一台混合开发机器的实测数据

我在自己一台 512GB 的笔记本上跑过一次完整的清理。这台机器同时用于 Java、Node、Python 项目,还经常构建 Docker 镜像,磁盘压力一直很大。清理前手动用du统计了相关目录:

目录清理前体积可回收空间清理后体积
~/.gradle/caches23.6 GB18.9 GB4.7 GB
~/.m2/repository9.8 GB4.1 GB5.7 GB
~/.npm/_cacache6.2 GB4.5 GB1.7 GB
~/.cache/pip4.5 GB4.1 GB0.4 GB
~/.cargo/registry2.3 GB1.2 GB1.1 GB

总计回收了将近 33GB,接近整块系统盘剩余空间的六成。清理后跑了一次日常项目的构建,除了第一次构建因为要重新下载一些依赖而慢了一点之外,后续构建基本没有影响。Gradle 的transforms缓存重新生成时稍微花了些时间,但也没有出现奇怪的错误。

这个数据不是个例。后来我在一台全新的 CI 节点上也跑了一遍,那台机器上只有一套 Maven 仓库,清理前 18GB,回收了大约 7GB。主要原因是有大量旧版本 jar 和 failed-to-download 标记文件,属于非常典型的"冗余缓存"。

4.3 与镜像仓库配合:删除之后如何更快恢复依赖

很多开发者会担心清理缓存后,重新拉依赖太慢。这个问题的解法其实不复杂,尽量给包管理器配置一个架构内合适的镜像仓库。比如 Maven 可以在settings.xml里配置阿里云仓库,npm 可以配置淘宝镜像,pip 可以配置清华 PyPI 镜像。这些镜像仓库的意义在于,清理本地缓存后再次构建时,下载速度会明显更快,而且镜像节点上的包版本通常比较完整,不容易碰到依赖缺失。

我自己的习惯是:每半年做一次全面清理,清理前把当前项目所用的依赖版本导出成文件(比如requirements.txtpackage-lock.jsonpom.xml),万一构建出现问题,直接用这些锁文件恢复。这不代表缓存可以随便删,而是在"磁盘空间"和"重建成本"之间做一个可恢复的权衡。

如果你的团队有统一的构建环境,也可以把这份工具和镜像配置一起写进初始化脚本。新机器一装好就能用,既避免了"每个人手工清理"的麻烦,也减少了在配置镜像仓库时的各种笔误。

5. 使用前必须知道的三类风险与规避方式

5.1 离线与内网环境:缓存可能比网络更可靠

这是我在开源社区收到最多的一条使用反馈。很多做嵌入式或内网开发的同学,开发机并不总是能访问外网,甚至只能访问公司内部的私有制品库。对他们来说,~/.m2/repository~/.gradle/caches几乎是唯一的依赖来源,一旦误删,问题就严重了。

针对这类场景,工具专门加了一个--offline-mode参数。在这个模式下,工具只扫描并输出统计信息,默认不进入删除流程。如果你确实要清理,推荐配合--keep-all-releases,只删除临时文件和快照残留,不动任何正式版本。这样既回收了一部分空间,又保留了离线构建的核心依赖。

我个人的建议更简单:离线环境的机器,不要用任何清理工具,除非你确定有完整的本地备份。磁盘告急是麻烦,但无法构建是事故,两者级别完全不同。

5.2 多用户和 CI 并发:清理时机比清理命令更重要

一台开发机如果有多个用户,或者一个构建节点同时跑多个任务,清理缓存就很容易踩并发问题。最常见的情况是:用户 A 正在用 Maven 构建,用户 B 执行清理工具把~/.m2/repository下部分目录移动到了回收站,用户 A 的构建就会因为找不到 jar 而失败,而且报错信息会比较隐晦,容易让人误以为是网络问题或依赖冲突。

为了防止这个情况,工具在清理前会检查相关的锁文件。比如 Gradle 的.lock文件、Maven 的仓库更新锁,如果检测到有活跃进程正在使用,就自动跳过对应目录。但这只能防君子不防小人,最稳妥的做法还是把清理动作安排在 CI 空闲时间段,或者在本地开发机上避免在构建过程中执行。

CI 环境还要注意一点:很多 CI 系统会把包缓存作为"缓存目录"挂载到工作区,清理后如果不重新缓存,下一次构建就需要全量下载。所以我通常建议在 CI 节点上不要频繁清理,可以设置一个比较长的保留周期,比如只清理 90 天前的旧版本,并且保留最近 3 个 release。

5.3 别把构建产物和镜像缓存搅在一起

刚开始设计工具的时候,我一度想过把target/build/node_modules/这些项目目录也纳入清理范围。后来测试发现,这些目录虽然确实占空间,但它们属于"构建产物"而不是"仓库包缓存",删除策略和使用者的意图差异太大,混在一起反而会降低工具的可靠性。

举个实际例子:Gradle 项目的build/目录删了之后,下次构建虽然会重新生成,但增量编译状态也会丢掉,大型项目的构建时间可能从几十秒变成十几分钟。node_modules删了之后,只要没有 lock 文件,还原的依赖版本可能和之前不一致。这些风险不是"包缓存清理工具"应该承担的。

所以工具明确不扫描项目目录,也不会对/var/lib/docker做任何处理。如果你确实想清理 Docker 镜像和构建缓存,建议单独使用 Docker 原生的docker system prune -a,或者docker builder prune。包缓存清理和镜像清理是两个维度,搅在一起很容易出问题。

5.4 误删恢复与构建加速的平衡

即便工具做了回收站策略和 dry-run 提示,依然有用户会误删依赖。我自己也遇到过一起:某次我执行repo-sweeper clean --aggressive时,忘了加--keep-releases 2,结果把某个本地仓库的旧版本全清了,当时没觉得有什么问题,直到一个老项目需要切回旧版本重新验证,才发现要重新下载。

从那以后,我在使用上给自己定了几条纪律。

  • 清理前永远先dry-run,并把输出 JSON 保存到/tmp下,方便后续审计。
  • 尽可能不要用--aggressive,除非你明确知道某些内容永远用不上。
  • 真正需要加速构建时,不要依赖"不清理缓存",而应该用构建缓存工具或远程缓存服务,比如 Gradle Build Cache、Maven 的离线模式配合私有仓库。

包缓存清理的本质,不是把缓存全部消灭,而是把"确定没用的"换成空间,把"可能还用的"留在磁盘上。工具能帮你做分类,但最终判断还得靠你对自己使用习惯的认知。

如果你也正在被磁盘告急困扰,不妨先扫一眼自己的仓库缓存目录,看看有多少旧版本和临时文件躺在那里。很多时候,清理不是要放弃缓存,而是让缓存回归它该有的样子,只保留值得保留的内容。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询