
作为一个常年和磁盘空间作斗争的程序员我太清楚那种看着磁盘空间一点点变红的感受了。前几天编译一个 Java 项目IntelliJ IDEA 直接弹红框报错No space left on device。打开系统设置一看C 盘就剩 3.2GB。用磁盘分析工具扫了一遍才发现~/.m2 一个目录吃掉了 40 多 GB~/.npm 的 _cacache 占了 15GBDocker 的 build cache 和 overlay2 合计又吞了 60 多 GB。那一刻我才彻底想明白真正吃掉磁盘空间的不是代码而是日积月累、只进不出的仓库包缓存。于是我把这个痛点做成了开源解决方案花了两个周末写了一个专为开发者打造的仓库包缓存清理工具项目名叫repurgerepo purge。它支持 Maven、Gradle、npm、Yarn、pnpm、pip、Go Modules、Docker Build Cache 这八类最常见的构建缓存目录通过分析文件访问时间、版本号、引用状态把“确定没用”的缓存安全清掉同时保留最近还在使用的部分。这个工具最适合谁经常在多项目之间切换、本机构建缓存动辄几十 GB、又不放心系统“一键清理大师”乱删开发者目录的朋友。本文我会把整个项目的设计思路、核心实现、完整操作流程和踩坑记录都拆开讲一遍。1. 先搞清楚一个核心问题包缓存为什么越攒越多1.1 缓存机制的本意和副作用包管理器的缓存机制本质上是“用空间换时间”的经典思路。Maven 每次构建都要从中央仓库拉取 jar 包如果没有本地缓存每次构建都相当于重头下载带宽和耗时都受不了npm 的 _cacache 会把所有下载过的 tarball 和 metadata 以内容寻址方式存储哪怕一个依赖只用过一次它也会留在磁盘里。这套机制单独看没毛病问题在于包管理器很少思考“缓存什么时候该被清理”。Maven 默认对 .m2 里的 SNAPSHOT 会做定期更新检查但正式版的 jar 几乎不会被自动删除npm 的缓存默认不设强制过期时间Docker 的 build cache 更是出了名的“只进不出”。哪怕你每天只正常开发和构建一年下来也能攒出几十甚至上百 GB 的“不动资产”。我在多个团队里都看到过同一种情况大家只知道磁盘满了让 IT 帮忙清理但 IT 也不知道那些 .m2、.gradle 目录是什么直接全删了结果第二天全团队都在痛苦地重新下载依赖。1.2 现有清理方案的三个明显痛点动手写这个工具之前我把市面上已有的清理方案都试了一圈发现它们都有点“隔靴搔痒”。第一个痛点是分区不均。很多“电脑清理工具”能清系统垃圾、浏览器缓存但对开发者的构建缓存目录识别能力很差.gradle、.pnpm-store、GOMODCACHE 这些目录它根本不认识要么跳过不处理要么暴力删除。第二个痛点是一刀切式清理太粗暴。比如网上流传的“Maven 空间不够删掉 .m2 重新下载”这话半对半错清理是真干净但下次构建要全量下载等于把节省的时间又全部还了回去。第三个痛点是缺乏通盘视角。开发者的机器上通常同时跑着 Java、Node、Python、Docker 多套工具链却没有一个工具能统一识别、统一分析、统一清理导致每个目录看起来都“没那么大”加起来却撑爆了整个磁盘。基于这三个痛点我决定自己做。项目目标定得很明确能识别主流开发缓存能区分“可用缓存”和“垃圾缓存”能一键清理但必须让人清楚删掉了什么并且跨平台一致。1.3 给自己定的四条设计底线项目动工之前我先写了四条“军规”后续所有功能都围绕它们展开一旦违背就主动砍需求。安全第一。只处理“明确可删”的内容比如过期的 SNAPSHOT、npm _cacache 中未被引用的孤儿文件、Docker 的 dangling build cache绝不碰用户的源代码、配置文件、未提交内容。可解释性优先。任何清理动作之前都要先给出完整预览删除的每个字节都要有依据。保留策略可配置。最近保留几天、保留几个版本这些参数都应该允许开发者按需调整。跨平台一致。Windows、Linux、macOS 的缓存路径不同但命令用法和输出格式完全一致。这四条底线在后期的开发和推广中帮我省了非常多事尤其“可解释性”这一点让工具在同事之间传播时完全不需要我反复解释大家自己看输出就能明白每个动作的含义。2. 整体设计与技术选型为什么是 Go目录覆盖和清理策略怎么定2.1 技术选型Go 的三个核心理由先聊技术选型。仓库包缓存清理工具本质是一个 CLI 工具可选的实现语言不少Python、Rust、Go 都能做。我最终选了 Go核心原因有三个。第一是分发简单。Go 编译出来就是单文件二进制Windows 上是一个 .exeLinux 和 macOS 上是不带动态库依赖的可执行文件用户下载后直接运行不需要装运行时。对于清理工具这种用户普遍“不敢乱装东西”的场景这一点非常重要。Rust 可以做得更好但学习曲线和编译时间对开源项目的初期迭代来说偏重Python 写起来最快但“用户机器上没有 Python 3.10”这种问题很现实还得考虑 PyInstaller 打出来的包被杀毒软件误报的尴尬。第二是跨平台编译成熟。一次写业务代码通过 GOOS 和 GOARCH 环境变量就能交叉编译出三个平台的二进制再配合 GitHub Actions 的 CI 矩阵每次发版都能稳定产出四到六个压缩包。第三是并发模型顺手。扫描几十 GB 缓存目录时用 goroutine 并发遍历目录树、汇总文件大小和访问时间比串行遍历快一个数量级而且代码写起来不像手写线程池那样繁琐。2.2 支持的八类缓存目录一览不同包管理器的缓存目录结构差异非常大我第一版先覆盖了身边开发者最常用的八类包管理器默认路径Linux/macOS默认路径Windows缓存特征Maven~/.m2/repositoryC:\Users\xxx.m2\repository按 groupId/artifactId/version 分目录Gradle~/.gradle/caches/modules-2C:\Users\xxx.gradle\caches\modules-2含 modules-2、wrapper、daemon 等目录npm~/.npm/_cacacheC:\Users\xxx\AppData\Local\npm-cache内容寻址存储孤儿文件比例高Yarn~/.cache/yarn/v6C:\Users\xxx\AppData\Local\Yarn\Cache按包名加版本缓存 tarballpnpm~/.pnpm-store/v3C:\Users\xxx.pnpm-store\v3内容寻址目录硬链接机制特殊pip~/.cache/pipC:\Users\xxx\AppData\Local\pip\cacheHTTP 响应缓存体积波动大Go Modules$GOMODCACHE默认 ~/go/pkg/modC:\Users\xxx\go\pkg\mod按 moduleversion 分目录只读属性Docker Build Cache/var/lib/docker 或 Docker Desktop 虚拟机C:\Users\xxx\AppData\Local\Docker通过 buildx 接口查询不能直接删文件表格里列的是默认路径实际环境中因为环境变量覆盖的情况很常见所以我在实现里加了一层“自动探测 手动指定”的逻辑。环境变量如 MAVEN_OPTS、NPM_CONFIG_CACHE、GOMODCACHE、GRADLE_USER_HOME 都会影响缓存位置工具必须优先读取这些变量再回落到默认路径。2.3 清理策略怎么定义“可以安全清理”这是整个项目最核心的算法部分我花了最多时间设计。整体思路是把缓存文件分成三类分别处理。第一类是临时文件和孤儿文件直接清。例如 npm cache 的 tmp 目录下中断下载留下的半成品文件pnpm store 里没有被任何项目引用的孤儿对象这些是包管理器在并发下载或中断时产生的垃圾删了不会有任何影响。第二类是过期版本按策略清。对 Maven 的 SNAPSHOT 只保留最近 N 个版本对 npm 的 _cacache 按最后访问时间保留最近 N 天内访问过的内容对 Go Modules 按 moduleversion 保留最新版本。第三类是异常大小的缓存只提醒不自动处理。单个文件超过 1GB、某个依赖目录总大小超过 5GB 的工具会在报告里标为“建议人工确认”默认不碰。判断“最后访问时间”看似简单实际操作中有一个很深的坑Linux 的 noatime 挂载选项可能导致文件访问时间不更新Windows 的 LastAccessTime 语义与 macOS 也有差异。我第一版直接用 atime 做判断结果在 Linux 服务器上扫描结果严重失真后来改成“atime mtime 双维度 文件元数据变更时间”组合判断虽然保守一些但不会误删项目刚下载完、还没来得及使用的缓存。2.4 CLI 交互设计让用户每一步都心里有数命令行交互设计参考了很多 Unix 工具的习惯命令尽量少语义尽量直白。一共五个子命令scan、clean、stats、config、daemon。scan扫描预览clean执行清理stats查看缓存总量分布config查看和生成配置文件daemon后台监听提醒。设计上最核心的一条原则叫dry-run 优先。无论执行哪个子命令第一步永远是模拟计算输出完整表格告诉用户“本次可以释放多少空间要删除哪些目录是否有风险项”等用户确认后才真正动手。清理完成后再输出 JSON 格式的清理报告方便接入日志系统。这个设计思路源于我自己踩过的一次大坑。之前我写过一个“自动化清理脚本”逻辑很简单删除超过 30 天没访问的文件。结果它把我 Docker 里一个长时间没启动但后续还要用的容器镜像缓存层删了害我重新构建整个镜像浪费了一个晚上。从那之后凡是涉及删除的工具我都坚持 dry-run 优先宁可让用户多按一次回车也绝不做全自动的“黑箱删除”。3. 核心功能拆解与实操步骤3.1 安装方式三个平台都做好准备安装方式目前提供三种。第一种是直接下载编译好的二进制GitHub Releases 页面提供 Windows、Linux、macOS 三个平台的 amd64 和 arm64 架构包下载解压后放到 PATH 目录即可。第二种是用包管理器安装macOS 支持 HomebrewWindows 支持 Scoop。第三种是源码编译适合想二次开发的用户README 里有完整的编译命令和依赖说明。一个很多人会问的问题这个工具需要管理员权限吗答案是不需要。默认只清理当前用户目录下的缓存~/.m2、~/.npm、~/.cache 这些都属于用户级目录普通权限完全够用。只有 Docker Build Cache 的清理要求执行用户具备 docker 命令权限而且是通过调用 docker CLI 完成的也不需要使用 root。刻意避开 sudo一方面降低使用门槛另一方面也减少用户对“又要装一个需要提权的工具”的天然防备心。3.2 scan 扫描先看清战况再决定怎么办第一次使用直接运行repurge scan --all。它会自动探测本机存在的包管理器缓存目录逐个扫描最后输出汇总表格包括缓存类型、缓存路径、当前总大小、可清理大小、清理依据比如“最后访问时间超过 180 天”或“SNAPSHOT 版本数超过 3 个”。扫描耗时是个需要优化的点。npm 的 _cacache 动辄几万个小文件如果串行遍历会非常慢。我在这里做了并发目录遍历每个一级子目录启动一个 goroutine 处理最后通过 channel 汇总结果。实测在普通机械硬盘上扫描 40GB 的 Maven 仓库能从三分钟压缩到二十秒左右在 NVMe 固态上效果更明显。如果某个目录正在被编译中的项目占用扫描时会出现文件句柄冲突工具会跳过该目录并提示“该目录正在使用中”避免清理到被占用的文件。3.3 clean 清理带策略的删除而不是 clearrepurge clean是实际的清理命令下面这些参数最常用--keep-days 30保留最近 30 天内访问过的缓存文件--keep-snapshots 3Maven SNAPSHOT 只保留最近 3 个版本--keep-latest-versions 5Go Modules 保留每个模块的最新 5 个版本--keep-orphans 0孤儿文件全部清理--include-docker-build同时清理 Docker Build Cache默认不启用--dry-run只输出模拟结果不执行删除--yes跳过手动确认用于 CI 等自动化场景这套参数组合起来可以覆盖绝大多数场景。我自己的开发机一般这样用repurge clean --keep-days 30 --keep-snapshots 3 --include-docker-build执行时它会先展示干跑结果和风险提示确认后才删。如果用--yes跳过确认终端会打出醒目的警告提醒你后续无法撤销。有一点要特别提醒清理不是清空。repurge clean的语义是“有策略地删除冗余缓存”不会像rm -rf ~/.m2那样把整个目录删干净恰恰相反它会努力保留最近仍在使用的缓存确保下次构建不会全量重新下载。3.4 白名单与配置文件给不想被动的目录留一条后路有些目录虽然理论上可以清理但实际开发中有特殊需求。比如公司强制使用某个特定版本的依赖即使很久没访问也最好保留避免重新下载超大文件。为此我在配置里加入了白名单和黑名单机制。配置文件默认放在~/.config/repurge/config.toml核心结构如下[clean] keep_days 30 # 全局保留最近访问天数 keep_snapshots 3 # Maven SNAPSHOT 保留版本数 keep_latest_versions 5 # Go Modules 保留每个模块的版本数 [[rules]] type maven path ~/company-repo keep_days 180 # 对特定路径覆盖全局配置写配置文件时最容易踩的坑是 TOML 语法里数组和表的区别。[[rules]]表示一个规则数组元素而[rules]表示一个名为 rules 的表两者混用会导致解析失败。我在自己的使用过程中也踩过这个坑所以在 README 里特别标注了示例。如果只是想简单排除某个目录直接在exclude数组里写路径即可不需要拆到 rules 里。3.5 daemon 模式不做自动删除只做有节制的提醒daemon 模式是后来应需求加的功能。有用户提 issue 说自己总是忘记定期清理等想起来的时候磁盘已经红了。我在设计里加了一个轻量监听进程默认每天 12 点执行一次 scan如果发现可释放空间超过阈值默认 5GB就通过系统通知弹一条提示“你的仓库包缓存已有 XX GB 可以安全清理运行 repurge clean 即可处理”。为什么 daemon 不做自动清理这背后有个产品取舍。自动清理看似省心但只要有一次误删用户对这个工具的信任就彻底崩塌。所以我宁可让 daemon 当一个“提醒者”而不是“执行者”把最终决定权留给用户。有朋友说这不够智能但在我看来开发者工具最重要的特质就是“可预测、不越界”。与其追求不必要的自动化不如把每个动作做得足够透明。4. 实战演示一次完整的磁盘清理过程4.1 清理前的实际情况用我自己的主力开发机做一次完整演示。这台机器是 Windows 11 WSL2 混用环境主要做 Java 和前端开发偶尔用 Docker 跑中间件。清理前 C 盘 512GB 只剩 18GBIDE 索引和编译都开始卡顿。我先执行repurge scan --all扫描完成后输出的汇总表如下缓存类型当前大小可清理空间主要清理依据Maven42.3GB31.8GBSNAPSHOT 旧版本 超 180 天未访问npm15.7GB7.2GB孤儿文件 超 30 天未访问pnpm8.4GB1.2GB孤儿对象Gradle22.6GB12.5GB旧版本依赖 构建缓存Go Modules6.1GB2.3GB保留版本之外的重复模块Docker Build Cache32.8GB19.6GBdangling 镜像层汇总下来本机可安全释放 74.6GB 空间。看到这个数字时我更加确信这个工具存在的价值如果不借助这类扫描大多数人对缓存目录的占用情况完全没有概念。4.2 执行清理与参数解析我最终执行的清理命令是repurge clean \ --keep-days 30 \ --keep-snapshots 3 \ --include-docker-build \ --yes这里逐个说明参数逻辑。--keep-days 30保留最近 30 天内访问过的缓存文件这是为了避免频繁使用的依赖被反复下载--keep-snapshots 3保留 Maven SNAPSHOT 最近 3 个版本因为 SNAPSHOT 代表“持续变化的开发中版本”旧版本几乎没有保留意义--include-docker-build打开 Docker 清理能力--yes跳过确认因为这个场景我已经看过 dry-run 的完整结果。注意这里没有传--keep-latest-versionsGo 模块走的配置默认值保留每个 module 的最新 5 个版本。整个清理过程耗时 47 秒主要时间花在删除大量小文件时 Windows 文件系统的元数据更新上。最终实际释放了 68.2GB比预估少了约 6GB原因是部分文件正被后台进程占用被工具自动跳过。对于被占用的文件工具会单独列出一个“未清理清单”方便后续排查。4.3 清理后的验证与方法论总结清理完成后我做了两件验证工作。第一重新编译之前那个 Java 项目。Maven 需要重新下载被清理掉的部分旧依赖但下载量可控因为超过 30 天没使用的旧版本已不是构建链的必需品首次构建多了约 4 分钟第二次构建就恢复正常速度。第二跑了一遍前端项目的npm install所有常用依赖命中了保留缓存几乎没有额外等待。这个结果验证了工具设计的核心原则清理的是“冗余”不是“必需品”。作为开发者工具目标不是把磁盘清理到最干净而是在“磁盘够用”和“构建高效”之间找到平衡。如果你也想在不破坏开发环境的前提下释放磁盘我的建议是先scan观察一周了解自己机器上缓存增长的速度再决定保留参数。而不是一上来就把 keep_days 调到 7结果每天构建都在全量下载。5. 常见问题与排查技巧实录5.1 高频问题速查表这个项目开源后收到了大量 issue我把出现频率最高的问题整理成了一张速查表问题现象可能原因解决方案扫描结果与磁盘分析工具不一致符号链接目录未被解析加--follow-symlinks参数清理后目录还在但大小没变文件被进程占用关闭 IDE、终端窗口后重试Maven 重新构建下载极慢保留策略太激进调大--keep-days和--keep-snapshotsWindows 上提示权限不足缓存目录被标记只读以当前用户运行不要用管理员模式Docker 清理失败Docker Desktop 未启动先启动 Docker 再执行扫描特别慢目录中文件数量巨大用--depth限制扫描深度这里要强调一下 Windows 权限问题。很多用户觉得“权限不足我就用管理员跑”但实测发现管理员运行时 CACL 权限模型反而可能造成更多意外错误。工具本身已经做了充分的错误捕获和跳过机制普通用户权限下运行其实是最安全、最省心的方式。5.2 最值得分享的三个详细踩坑记录第一个坑是pnpm store 的硬链接冲突。pnpm 的 store 目录大量使用硬链接来节省磁盘空间常规的“按文件大小累加”统计会导致同一个物理文件被多个硬链接引用扫描结果严重虚高。我在代码里必须分别处理Windows 上通过文件 ID 去重Linux 上通过 inode 去重否则用户会看到“扫描出 50GB 可清理实际删完只释放 2GB”的情况。第二个坑是Go Modules 的只读权限陷阱。Go 的 module cache 默认设置成只读和不可变强行执行os.RemoveAll会不停报 permission denied。我做了兼容处理删除前先chmod -R uw删除完成后再恢复目录权限。这个逻辑在 Linux 上很关键在 Windows 上则需要区分文件属性和 ACL 的差异最终按平台拆分了两套删除实现。第三个坑是清理时机选择。不要在编译进行中清理 Gradle 缓存。Gradle daemon 会持有缓存文件句柄Linux 上删除后不一定会立即报错但后续构建会随机出现“模块找不到”的诡异问题。现在的工具会自动检测 Gradle daemon 是否运行如果处于运行状态就提示用户先执行gradle --stop。这类“看不见的副作用”是清理工具最容易踩中的雷区也是设计时最容易忽略的部分。这三个坑如果在设计评审阶段就预判到能省下大量调试时间。我给所有想用类似工具的朋友一个建议第一次运行务必用scan模式多观察几天了解各类缓存的行为模式再启用自动清理策略。清理工具就像动手术最重要的不是技术多高而是术前评估做到位。最后说一点我在这个项目里的个人体会。写清理工具这件事技术难度不算高真正难的是克制。这里的克制指每加一条删除规则都要反复问自己这个文件删掉以后用户会不会有一天还需要它如果不能百分百回答“不会”那就宁可保守一点把决定权交还给用户。repurge 目前已经在我自己的开发机上稳定运行了三个月磁盘告急的警报再也没有出现过。后续我还在考虑两个方向一是支持更多包管理器比如 Rust 的 cargo、Ruby 的 bundler二是做一套 TUI 界面让不习惯命令行的人也能通过交互方式完成清理。如果你也有同样的磁盘焦虑欢迎直接拿来用或者到仓库里提 issue、提交 PR。一个人解决一个痛点一群人就能把它打磨成真正好用的工具这本来就是开源项目最迷人的地方。