我给一台 AI 工作机做了一次磁盘清理。它常年开机,跑着 9 个自托管服务,磁盘里堆着几年来的构建缓存、旧运行时和一批没人说得清的 Docker 卷,可用空间掉到 46G。

清理本身不新鲜。真正麻烦的是:这台机器上有正在跑的数据库、有 agent 的会话和记忆、有没人记得的遗留资产。删错一个,服务可能起不来,数据可能永远回不来。

所以我把这次清理当成一次有标准的执行:先扫描再判定,备份之后逐批确认,最后复验和观察。最终可用空间回到 62G。

但故事没有结束。11 天后复验,磁盘又掉回 26G。

这篇文章是完整账本:哪些能删、凭什么判定、36G 去了哪里,以及一套可以直接交给 agent 的执行纪律。

先别删:两个会撒谎的数字

动手之前,先解释了两个差点让判断走错方向的数字。

第一个是 df 和 du 的差额。文件系统报”已用 114G”,但非 root 的 du 只能统计到 81G,中间差了 33G。这不是统计错误:Docker 的私有目录、root 权限的日志目录,普通用户根本读不到。

差额里藏着两类东西:一类是 Docker 管理的镜像、卷与构建缓存(它自己报告 16.81G + 5.02G + 1.15G),另一类是真正的盲区。结论只有一句。扫描一定有盲区,报告必须写明未覆盖范围。把”没测到”当成”没有”,是清理事故的第一步。

第二个是 Docker 的 RECLAIMABLE。界面上显示 5.87G 可回收,实际清理悬空镜像只回收了 13.77M。原因不复杂,镜像层会在多个镜像之间共享,标出来的”可回收”是上限,不是承诺。界面数字与真实收益之间隔着一层:谁还在引用它。

判定框架:可再生四问

每个候选都要过四个问题,全部为”是”,才能进入删除区:

  1. 可重建吗? 重新下载或重算的成本是否可接受;
  2. 是唯一数据吗? 有没有登录态、消息、配置、用户创作、未提交的工作;
  3. 现在没被占用吗? 有没有进程打开、是不是活动数据库(含 WAL/SHM)、是否正在写入;
  4. 不是”状态伪装成缓存”吗? 这条最反直觉:~/.rustup/toolchains 名字像缓存,其实是工具链本体;Docker 卷更是数据本身。

任何一问为”否”或”未知”,一律降级为”只报告”。判定之后再分四档:

级别含义动作
A可再生、低风险报告 + 确认后可删
B可再生、高成本给出重建命令与代价,确认后删
C可能含唯一数据只报告
D禁止项不碰

四个批次的账本

批次目标净释放
1包管理器缓存~2.5G
2Docker 构建缓存 + 悬空镜像~1.16G
3遗留 Docker 卷(扣除备份)~4.0G
4旧运行时 + 未使用镜像~7G
合计46G → 62G

批次 1 全是纯缓存:npm 缓存 2.2G、conda 包缓存从 989M 清到 708M,加上 pnpm、pip 的零头。重建成本只有一个,重新下载。

批次 2 清掉了 1.146G 构建缓存和 13.77M 悬空镜像。教训在本节开头的第二个数字里。

批次 4 清理了两个不用的 Node 版本,共 4.6G;两个没有容器引用的镜像,共 2.88G。但有一个镜像被跳过,另一个仓库的配置里还有引用。”当前没用到”和”没人再用”是两件事。

真正需要单独说的,是批次 3。

不可逆操作的标准动作

42 个遗留卷,没有任何容器引用,合计 4.9G,创建时间从 2023 年横跨到 2026 年。卷里是 PostgreSQL、MongoDB 和 Redis 的数据目录,旧时代的服务数据。

“没有容器引用”不等于”没有价值”。对来历不明的数据,默认动作只有一个:不删。

处置依据是三条证据:第一,外部配置仓库的历史记录显示,这批卷属于已经迁走的旧平台,当时的容器全部使用 bind mount,没有引用任何卷;第二,用户明确确认这批资产属于已退役的旧平台;第三,删除前完成完整备份:19 个数据库卷打包 1023MB,逐条 SHA256 校验通过。

然后是观察期。11 天后的复验结果:9 个运行中容器的重启计数为 0、启动时间与清理前一致,5 个关键端口响应正常,备份文件校验一致。到这里,才能说这批卷确实没有被需要。

不可逆操作的完整动作是”备份 → 删除 → 观察 → 复验”。

11 天后:36G 去了哪里

观察期复验时发现,可用空间从 62G 掉回 26G。

第一反应是”是不是删错了什么”。逐项对比后确认:服务全在,清理没有失败,是新的开发活动产生了新数据:

  • 一个 Rust 项目的 debug 构建产物:22G;
  • /tmp 里的项目残留:13G(一个源码检出 3.6G,几个评审目录 1.5–1.6G);
  • Docker 构建缓存:从 2.665G 涨到 7.3G。

这两项合计 35G,加上持续增长的构建缓存,足以解释这次回落。这带来一个比清理收益更重要的结论。磁盘清理不是一次性动作。只要机器还在干活,占用就会重新长出来。清理的价值是一套可以重复执行的流程,并且知道哪些地方会反复长。

顺带追查了一个异常:清理当天,构建缓存从 1.146G 变成 2.665G,数字不降反升。当时按纪律记为”未测量”,没有继续动手。复验时它涨到 7.3G,终于查清:机器上有一个构建型容器在持续运行,缓存是它正常活动的产物。追查清楚之前不动手,这个等待值了。

候选的处置:从 26G 到 86G

两天后,这批候选按同样的流程处置完了。

缓存先清:构建缓存 7.3G,语言工具链缓存 3.4G,通用缓存 0.3G。root 侧的项目交给人在终端执行,删除停用的交换文件 2.1G,清掉 2023 年的审计日志 1.1G,journal 截断后留 856M,六个禁用的 snap 版本和旧内核残留一并移除,合计约 9G。用户空间的旧编辑器服务、三个停更的 Python 环境和一份 2023 年的数据目录,回收 3.7G。

到这里,可用空间从 26G 回到 66G。真正的大头在最后:那个 22G 的 Rust 构建目录。

放弃直接删除之前,先做了一次成分分析,原因有三层。开发档默认生成完整调试信息,一个测试二进制 173M,其中 117M 是调试段。增量编译缓存攒到 7.8G。同一个依赖因为 feature 组合不同,存在十几份变体。删除重建只是让同一棵树再长一遍,下次还是这个体积。

处置方式是改构建配置:依赖不生成调试信息,项目自身保留行号级调试。随后清空重建,23G 变 1.7G,测试全部通过。

清理过程中出过一次意外,值得单独说。

机器的 /tmp 里堆着 13G 项目残留,其中有一份正在评审的源码副本,是要保留的。一次系统变更验证触发了重启,启动清空机制把 /tmp 整个清掉,那份副本也没了。

好在它来自远端仓库,工作内容也已经推送,重新克隆就恢复了。这件事把”可再生四问”的第二问又敲了一遍:是唯一数据吗?/tmp 里的东西默认都算临时文件,这次丢掉的却是一份待处理的工作。在开机清空面前,所有”先放这,回头再处理”都可能直接消失。

最后是防复发。一次性清理之后留下三个周期动作:每周自动回收构建缓存,每周记录一次磁盘占用变化,日志总量设上限。占用重新长出来的时候,至少能被看见,也能被处置。

账本到这里合上:46G → 62G → 26G → 86G。从复验那天算起,可用空间多了 60G。

可以直接交给 agent 的执行纪律

如果你也想让 agent 帮忙清理磁盘,这套纪律可以直接复用:

  • 先扫描后动手:任何删除前先完成只读扫描并提交报告;
  • 逐条命令、禁止通配:不用 rm -rf *、不用 find -delete、不用变量拼路径;
  • 未测量 ≠ 0:读不到、超时、状态不明的,一律记”未测量”,既不当 0,也不当删除依据;
  • fail closed:任何检查失败,放弃该项并说明原因,不缩小范围蒙混;
  • 不提权:需要 root 的项目只输出命令,由人在终端执行;
  • 逐批确认:每批不超过 10 条,确认一批、执行、复验、再进下一批;
  • 失败即停:批内一条命令失败,立即停止该批剩余动作;
  • 全程留痕:候选、判定依据、执行命令、结果,四样都要出现在报告里。

默认只报告的清单:用户文档与相册、凭据与密钥、浏览器数据、AI agent 的会话与记忆、活动数据库文件、Docker 卷、备份与快照、正在写入的文件。

结尾

这台机器现在仍然跑着 9 个服务,新的占用也还会长出来。区别是,处置它们不再需要凭感觉:按四问判定、按批次执行、按周期复验。

顺带一提,Mole 和 Disk Raccoon 是两个可参考的开源清理项目:前者提供只读的 analyze/status 接口,后者的缓存地图把”名字像缓存、其实是状态”的目录逐个标出来。但它们更适合当线索与参考,不适合直接当执行层,尤其是非交互模式下的自动删除。

清理的收益按天计算,纪律按次计算。和 agent 协作,后者更重要。

说明

本文由 AI 依据作者一手素材起草,文中事实、数字与结论均经作者核对。

本文首发于公众号(2026-10-13);本页为博客存档版本。