多彩编程 多彩编程MZPH · CODE BLOG
ARTICLE DETAIL

文章详情

深耕前端与后端开发技术的一线实战笔记与踩坑复盘。

CSpaceGuard:Windows磁盘空间扫描、告警与可回滚清理

CSpaceGuard:Windows磁盘空间扫描、告警与可回滚清理 上周三早上八点四十我端着咖啡打开 IDE右下角先弹了个磁盘空间不足的提示紧接着编译直接报错退出C 盘剩余 1.2GB。那一刻我意识到靠感觉卡了就手动清一下这套土办法已经彻底失效了。于是我用两个周末折腾出了一个自己用的小工具取名CSpaceGuard——一个跑在 Windows 上的盘空间守卫。它干的事情很朴素定时扫描指定盘符的目录树按规则库评估哪些内容是安全的清理候选按阈值分级别告警清理前后全留痕、可回滚。这篇文章不是产品说明书是我从需求拆解、方案选型、代码落地到踩坑调优的完整折腾记录面向的是和我一样天天跟系统盘空间搏斗的开发、运维、测试以及剪视频、装游戏、跑虚拟机的重度用户。你如果只是想找个一键清理的软件看完第一章就可以关掉了但如果你想搞明白自己机器上的空间到底被谁吃掉了并且想有一个不会删错东西的方案那接下来的内容应该能省你不少时间。1. 需求拆解CSpaceGuard 到底要解决什么问题动手之前我先逼自己把需求写清楚因为清理磁盘这个需求太容易膨胀成写一个全能系统优化大师而那种项目百分之百会烂尾。我把需求砍到只剩三件事看得见知道空间去哪了、说得准判断哪些能删哪些不能碰、动得稳真删的时候不出事。这三件事是有严格先后顺序的跳过第一步直接做清理基本等于闭眼扔东西。1.1 三个把我逼到动手的场景第一个场景是包管理器缓存堆积。npm、pip、NuGet、Gradle、Maven 这几个家伙的缓存目录全在用户目录下我统计过一次光是AppData\Local和用户目录下的几个缓存文件夹加起来就吃了 60 多 GB。这些缓存的特点是删了不致命但不删会一直涨而且它们散落在至少七八个不同位置靠手动找根本找不全。第二个场景是构建产物和日志的隐形增长。前端项目的node_modules、后端的target、build、dist还有各种框架的日志目录它们的特点是短期有用、长期没用。我有个两周没动的实验项目node_modules加构建产物占了 4.3GB而我完全忘了它的存在。人对自己手动创建的东西有记忆对工具自动生成的东西没有记忆这就是空间黑洞的来源。第三个场景最要命空间不足导致的生产力中断。系统盘低于某个水位线之后Windows 的虚拟内存文件、休眠文件、各种临时文件的写入都会开始报错表现是偶发性的诡异故障——某个软件莫名其妙启动失败某个编译任务随机崩掉。你不是每天遇到所以很难把因果联系起来。我真正下决心做这个工具就是因为有一次排查了两小时的编译失败最后发现只是临时目录写不进去了。1.2 现成清理工具为什么不够用市面上的清理软件我试过好几款它们的问题不在功能少而在决策不透明。你点一下扫描它给你列出 30GB 垃圾你点清理它就删了。中间那 30GB 具体是什么、从哪来的、删了会有什么后果它基本不告诉你。对于普通用户这可能够了但对于我这种机器上跑着数据库、虚拟机镜像、还有几个正在开发的项目的人这种黑盒决策我不敢用。另一个问题是粒度太粗。现成工具大多按类别清理比如浏览器缓存系统日志。但我需要的是按路径 文件年龄 文件类型三个维度组合判断。举个例子同样是AppData\Local\Temp下的文件昨天生成的可能是某个正在运行的程序的活动文件七天前生成的几乎肯定是垃圾。现成工具不会给你这种精细控制而自己写就可以。还有一点是留痕。误删了怎么办绝大多数工具清理完就完了没有操作清单没有回滚入口。我这人有个习惯凡是要批量删东西的操作必须留一份我删了什么、什么时候删的、删之前它多大的记录。这个要求直接决定了后面架构里必须有隔离区和清单机制。1.3 我给自己划的边界只做守卫不做屠夫这条边界是我踩过坑之后才明白的。第一版我做得很激进规则里写了删除所有超过 30 天的临时文件结果把自己正在用的一个构建缓存删了下一次编译从 40 秒变成了 6 分钟。那次之后我定了三条铁律。第一默认只读不写。CSpaceGuard 的默认运行模式是扫描加报告不做任何删除动作。想让它清理必须显式指定--apply参数而且每次 apply 之前会打印一份预估报告让你确认。这条规则的代价是多按一次回车收益是永远不会在你不注意的时候删东西。第二白名单优先于一切规则。系统目录、程序安装目录、用户文档目录、以及我自己标记的项目根目录全部进硬白名单任何规则都不能越权。而且白名单是路径前缀匹配不是模糊匹配避免看起来像这种危险判断。第三清理动作必须可回滚。所有被删的东西先进隔离区保留 14 天同时写一份 JSON 清单。14 天内你可以用一条命令全部还原。14 天之后才真正删掉释放空间。这个设计很反直觉——它并不能立刻释放空间但它让我在最初调规则的几周里敢放开了试。2. 方案选型为什么是 Python 扫描 PowerShell 落地选型这块我纠结了大概三天中间还推翻过一次。最后定下来的架构是Python 负责扫描、规则评估、数据存储和历史趋势计算PowerShell 负责系统级动作回收站、任务计划、通知弹窗两者通过 JSON 中间文件和命令行调用衔接。下面说说这个组合是怎么筛出来的。2.1 候选方案横向对比我列了四个候选方案从开发效率、执行性能、系统集成能力、可维护性四个维度打分结果如下表。这个表是当时的真实评估不是事后编的。方案开发效率扫描性能系统集成可维护性结论纯 PowerShell 脚本高中极强中脚本一长就乱只用来做系统动作纯 Python高高中需 pywin32高主力方案C# 桌面程序中极高极强低迭代慢放弃太重Go 单文件工具中极高中高备选交叉编译省事最后选择 Python PowerShell核心原因是迭代速度。这个工具的价值全在规则库上而规则库是要反复调的——今天发现漏了个缓存目录明天发现某条规则误伤了你希望改完之后五秒钟就能验证。Python 的交互式调试和热加载配置在这个场景下优势太大。C# 性能确实好但每次改一行规则都要重新编译我的耐心撑不住。至于为什么不全用 Python是因为有几件事 Python 做起来特别别扭调用系统回收站要 pywin32 或者 ctypes 调 Shell API注册计划任务要拿schtasks命令行去拼字符串弹 Windows 系统通知要找第三方库。这些事情 PowerShell 一行就搞定了。所以我把它们拆开Python 只管算PowerShell 只管做边界非常清楚。提示不要把 PowerShell 脚本内嵌在 Python 字符串里调用。我第一版这么干过结果引号转义问题折腾了两个小时。正确做法是把 PowerShell 脚本写成独立的 .ps1 文件Python 用 subprocess 调用文件路径参数通过命名参数传递。2.2 阈值怎么算容量警戒线与增长速率预测光设置一个剩余空间低于 10GB 就告警的静态阈值是不够用的因为不同盘、不同用途的机器10GB 的意义完全不同。我用了两层判断。第一层是容量比例警戒线。以系统盘为例我设了两条线剩余比例低于 15% 报注意低于 8% 报危险。为什么是这两个数因为我的系统盘是 512GB15% 大概是 76GB这个水位之下 Windows 的系统还原点、虚拟内存扩展、大版本更新都会开始挑毛病8% 大概是 40GB这个水位之下基本就是随时可能出故障的状态了。这两个数字是可以按盘大小调的配置里写成比例而不是绝对值这样换机器不用改。第二层是增长速率预测这个比静态阈值有用得多。CSpaceGuard 每次扫描会把当次的总容量、剩余空间、时间戳写进 SQLite形成一个采样序列。有了这个序列就能算日均净增长然后预测按当前速度还能撑几天剩余可用空间 / 最近 7 天日均净增长 预计可用天数我举个实际数字。上个月中旬我的采样显示日均净增长 3.8GB剩余 42GB预测可用天数只有 11 天。这条告警让我提前发现了问题——一个实验项目在后台持续写日志每天写 2GB 多。如果没有增长速率这个维度我只会看到还有 42GB挺宽裕然后在一周半后被打脸。计算日均增长有几个细节要注意排除掉清理动作当天产生的负增长否则均值会被拉低预测不准用中位数而不是平均数避免某一天拷贝了个大文件把结果带跑偏采样间隔要一致别今天扫三次明天扫一次那样算出来的速率没意义。我的做法是每 4 小时采样一次算 7 天窗口的中位数同时把清理日志里当天的释放量加回去。2.3 配置文件长什么样配置我用的 YAML比 JSON 好写注释比 TOML 嵌套表达能力强。一份典型的配置大概长这样version: 1 drives: - letter: C warn_ratio: 0.15 danger_ratio: 0.08 scan: roots: - C:\\Users\\me\\AppData\\Local - C:\\Users\\me\\AppData\\Roaming - D:\\workspace exclude: - C:\\Windows - C:\\Program Files follow_junction: false min_file_size: 1048576 rules: - name: 包管理器缓存 path: C:\\Users\\me\\AppData\\Local\\npm-cache max_age_days: 30 action: quarantine - name: 临时目录 path: C:\\Users\\me\\AppData\\Local\\Temp max_age_days: 7 action: quarantine skip_locked: true - name: 构建产物 path: D:\\workspace\\**\\node_modules max_age_days: 45 action: report_only这里有几个设计决定值得说一下。follow_junction默认是 false这是为了防重复计数后面会展开讲。min_file_size设成 1MB 是为了减少噪声扫描结果里全是几 KB 的小文件会让人没法判断。action有三个取值report_only只报告不动手quarantine进隔离区delete直接删——最后这个我留了接口但从来没用过因为没有任何场景值得冒这个风险。3. 核心实现从扫描、规则到安全清理架构定下来之后真正花时间的都是细节。这一章把几个关键实现拆开讲包括我为什么这么写、不这么写会出什么问题。3.1 扫描器用 os.scandir 加显式栈90 秒扫完 200 万文件扫描器的性能直接决定了这个工具能不能日常跑。我最开始的版本用的是os.walk实测扫 200 万个文件要 6 分半慢到我不想让它定时运行。原因有两个os.walk内部还是会 stat 每个条目而且它是递归生成的中途不能方便地做剪枝控制。换成os.scandir加显式栈之后时间压到了 90 秒左右。核心改动是三点。第一os.scandir返回的 DirEntry 对象在 Windows 上会缓存 stat 信息调用entry.stat()不需要额外的系统调用entry.is_dir()在很多情况下更是直接读缓存。第二改成显式栈一个 list 当栈用之后我可以在构造下一层路径时就做排除判断白名单和排除目录直接不进入避免先遍历再过滤的无用功。第三把每个目录的汇总大小算在遍历过程中而不是遍历完再回算。import os import stat def scan(root, exclude_prefixes, follow_junctionFalse): stack [root] results [] while stack: current stack.pop() try: with os.scandir(current) as it: for entry in it: try: if entry.is_dir(follow_symlinksfollow_junction): # 跳过分区挂载点和符号链接防止重复计数 st entry.stat(follow_symlinksFalse) if st.st_file_attributes stat.FILE_ATTRIBUTE_REPARSE_POINT: continue if any(entry.path.startswith(p) for p in exclude_prefixes): continue stack.append(entry.path) elif entry.is_file(follow_symlinksFalse): st entry.stat(follow_symlinksFalse) results.append((entry.path, st.st_size, st.st_mtime)) except OSError: # 单个条目失败不影响整体扫描 continue except (PermissionError, FileNotFoundError, OSError): continue return results这段代码里有两个地方是血泪换来的。一个是follow_symlinksFalse配合FILE_ATTRIBUTE_REPARSE_POINT检查这是防重复计数的关键。Windows 上有很多目录联接比如C:\Users\All Users指到C:\ProgramDataC:\Documents and Settings也有类似的重定向。如果不跳过同一批文件会被统计两遍甚至三遍扫描结果直接虚高一倍你的告警阈值就全乱套了。另一个是except OSError: continue。这个看起来像偷懒其实是必要的。Windows 上有大量文件在你扫描的瞬间被占用、被锁定、被删除遇到这些情况直接抛异常整个扫描就挂了。宁可漏掉几个文件也要保证整体跑完。我在日志里会记一个跳过计数如果某次扫描跳过数量突然暴增说明有程序正在疯狂操作那个目录值得看一眼。提示如果目录层级特别深显式栈版本理论上还是可能遇到 Windows 的路径长度限制。稳妥做法是在路径前加\\?\前缀走长路径 API或者提前开启系统的长路径支持。我两种都做了因为工作目录里确实有嵌套很深的构建产物。3.2 规则引擎白名单、候选、阈值三类规则怎么分工规则引擎我刻意做得很简单没有做那种表达式 DSL因为可维护性比灵活性重要。规则分三类按优先级从高到低执行。第一类是硬白名单优先级最高路径前缀匹配命中就永远不会被任何后续规则碰到。这里面放的是系统目录、程序安装目录、我的文档目录、以及所有正在活跃开发的项目的根目录。白名单是全局的不写在具体规则里就是为了避免某条规则写错了导致越权。第二类是清理候选规则也就是配置里rules那一段。每条规则由路径、文件年龄阈值、动作三个要素组成。评估逻辑是先检查路径是否命中再检查文件的 mtime 是否超过阈值注意是超过才候选不是小于这个搞反了就变成删新文件了我第一版就写反过好在是 dry-run 模式发现的。这里有个容易忽略的点判断年龄要用时间戳直接比较不要转成日期字符串再比。我一开始把 mtime 转成本地日期字符串再和今天减 N 天的日期字符串比结果因为跨时区和夏令时的问题出现了一整天的偏差部分当天的文件被判定为过期。第三类是阈值规则作用于目录级而不是文件级。典型配置是某个目录总大小超过 5GB 就报告用于那些不方便按年龄清理的地方比如虚拟机镜像目录、Docker 数据目录。这类规则只报告不清理因为目录级的删除决策太依赖上下文交给机器判断不靠谱。规则的执行顺序很重要是白名单过滤 → 候选规则匹配 → 阈值规则补充 → 去重 → 按大小排序。最后的排序是为了让报告好看人总是先看最大的那几个。3.3 安全清理dry-run、隔离区、清单回溯三件套这部分是整个工具里我最满意的设计。清理流程分四步走每一步都有明确的产物和退出条件。第一步是dry-run 生成预估报告。输出一份 Markdown 或表格列出每条规则命中了多少文件、总共多少字节、最老的文件的年龄、以及前 20 个大文件。这份报告的意义是让人做最终判断而不是让程序自己判断。第二步是确认执行。执行时把文件从原位置移动到隔离区目录而不是删除。隔离区目录的结构是quarantine/YYYYMMDD-HHMMSS/保留原始路径的目录结构这样还原的时候可以直接按相对路径搬回去。同时写一份manifest.json记录每个文件的原始绝对路径、大小、mtime、命中的规则名。第三步是保留与还原。隔离区保留 14 天。这 14 天里如果发现某个东西被误删了用csg restore --manifest xxx.json一条命令全部还原。还原逻辑是读 manifest把文件从隔离区搬回原路径如果原路径已经有同名文件了就跳过并报告冲突避免覆盖。第四步是过期清理。超过 14 天的隔离区目录在下次运行时被真正删掉空间这时候才释放。所以这个工具的空间释放是延迟的这点必须提前接受。{ created_at: 2025-11-14T09:12:3308:00, rule: 临时目录, entries: [ { origin: C:\\Users\\me\\AppData\\Local\\Temp\\build_cache_9182.tmp, size: 52428800, mtime: 1762046153, restored: false } ] }注意移动跨盘符时如果隔离区和源文件不在同一个物理盘上shutil.move会退化成复制再删除速度极慢而且中途失败会留下半个文件。我的做法是隔离区固定放在和主要扫描目标同一个盘上并且移动前先检查os.stat().st_dev是否一致不一致就走分批复制加校验的路径。3.4 告警通道与任务调度的配合告警这块我做了两个通道。本地通道是用 PowerShell 弹一条系统通知简单直接只在你坐在电脑前时有用。远程通道是往群机器人的 Webhook 推一条消息格式是纯文本加几个关键数字盘符、剩余空间、剩余比例、预测可用天数、本次发现的清理候选总量。这个通道的价值是你在外面也能知道机器状态。告警做了去重避免刷屏。规则是同一个盘符的同一级别告警24 小时内只推一次如果级别从注意升级到危险立刻推一次不受去重限制空间恢复到警戒线以上时推一条恢复通知这样你知道问题解决了。调度用的是 Windows 任务计划程序每 4 小时跑一次扫描每天凌晨 3 点跑一次隔离区过期清理。这里有个坑默认创建的任务是仅在用户登录时运行意味着你锁屏或者没登录的时候它不跑而凌晨 3 点你大概率是没登录的。解决办法是把任务配置成不管用户是否登录都运行但这会带来另一个问题——以系统身份运行时环境变量不一样用户目录定位不到后面第五章会细讲。4. 一次完整的部署与调优记录前面讲的都是设计和代码这一章记录一次从零到能用的完整过程包括实测数据和调参过程。你如果打算复现可以照着这个顺序走。4.1 部署步骤我把它整理成了一套可以照着敲的流程环境是 Windows 11 Python 3.11 PowerShell 7。先准备目录结构。我放在D:\tools\csguard\下分四个子目录bin放可执行脚本config放 YAML 配置data放 SQLite 数据库和日志quarantine放隔离文件。之所以放在 D 盘而不是 C 盘是因为一个讽刺的事实——一个清理 C 盘的工具如果自己把数据写在 C 盘上会持续占用它想拯救的空间。隔离区尤其要注意它可能临时占用几十 GB。然后是依赖安装。Python 侧只需要标准库加两个轻量包pyyaml读配置psutil拿磁盘剩余空间也可以用shutil.disk_usage但 psutil 给的字段更全。其他能不用第三方就不用依赖越少以后换机器或者升级 Python 版本时越省心。接着是配置首跑。第一次一定要把action全设成report_only然后手动执行一次扫描看看输出是否符合预期。这一步我强烈建议花时间做完不要急着改成 quarantine。最后是注册计划任务。我用的命令大概是这个形式schtasks /create /tn CSpaceGuard-Scan /sc HOURLY /mo 4 /tr pythonw.exe D:\tools\csguard\bin\csg.py scan --config D:\tools\csguard\config\csguard.yaml /ru SYSTEM /rl HIGHEST /f/ru SYSTEM让任务以系统身份运行/rl HIGHEST给最高权限避免扫描时遇到权限不足的目录一片红。/f是强制覆盖同名任务方便我反复改配置重新注册。4.2 第一次空跑的实测数据第一次完整扫描我用的是 report_only 模式扫描范围是用户目录下的 AppData 加上 D 盘的一个工作目录。实测数据我记得比较清楚指标数值扫描文件总数约 214 万个扫描目录数约 18.7 万个耗时96 秒跳过条目权限/占用3412 个结果文件体积约 176MB未压缩识别出的清理候选41.2GB其中确认为长期无用的约 27GB有几个数字值得琢磨。跳过条目 3412 个听着很多但相对于 214 万只占 0.16%可以忽略。真正让我意外的是结果文件 176MB——每个文件的路径、大小、mtime 全存下来就是这么大。后来我改成了只存聚合结果加 Top N 明细原始全量数据落成二进制或者干脆不落文件体积降到 3MB 以内。识别出 41.2GB 候选但人工复核后确认可以放心清理的只有 27GB。这 14GB 的差额就是工具判断和人的判断之间的差距主要来自几个正在使用的开发工具缓存和几个我还在用的构建产物。这个差距恰恰证明了 dry-run 环节不能省。4.3 参数调优把误报压下去有了第一次的数据调参就有方向了。我主要调了三个参数。文件年龄阈值从 7 天调到 30 天然后针对不同目录分别设置。临时目录保持 7 天没问题因为我确实不需要超过一周的临时文件。但包管理器缓存从 7 天调到了 30 天原因是有些缓存删了之后重新下载要花很久而它们的体积增长并不快没必要那么激进。调完之后候选体积从 41GB 降到 33GB但误报率明显下降。最小文件大小从 100KB 调到 1MB。这个参数对结果数量的影响比体积大得多——扫出来的文件条目从 3 万多个降到 6000 多个报告从看不过来变成能在两分钟内看完。代价是漏掉了一些小文件但它们加起来也不到 2GB不值得为它们牺牲可读性。扫描深度限制加了配置项默认不限制但给几个特别深的目录比如包管理器的嵌套依赖设了上限。有一次我发现某个包管理器目录下有 40 层嵌套扫描那一个目录就花了 20 秒。加了深度限制之后整体扫描时间从 96 秒降到 71 秒。调参之后我观察了两周规则库稳定下来误报基本清零。这个时候我才把几个最可靠的规则从report_only改成quarantine让它们自动跑。5. 常见问题与排查速查表这部分是我在不同机器上复现时积攒下来的问题清单挑最典型的几类说。5.1 扫描慢、卡死、内存涨扫描慢的第一个原因通常是踩到了目录联接或者网络映射盘。Windows 上有些目录看起来是本地目录实际上是重定向到别处的联接点甚至有些是映射的网络位置。一旦踩进去扫描会突然变得极慢因为每次 stat 都要走网络。排查方法是看扫描日志里的当前进度如果停在某个路径不动手动去看那个路径的属性是不是有重解析点。解决办法就是前面代码里的FILE_ATTRIBUTE_REPARSE_POINT检查。第二个原因是结果集全量保存在内存里。214 万个文件每个条目一个元组再算上路径字符串内存峰值能到 1.5GB 以上。我的做法是加一个内存阈值超过之后把结果分批刷到磁盘上的临时文件最后再做聚合。或者更简单粗暴扫描阶段只保留目录级的聚合结果明细只在需要出报告时才按需查询。卡死的另一个隐蔽原因我遇到过把扫描结果输出目录设在了被扫描的目录内部。结果就是每轮扫描都会扫到上一轮的输出文件输出文件越来越多扫描越来越慢形成正反馈。这个坑很小但很致命排查的时候很难想到。解决办法是在配置校验阶段就检查输出路径是否落在扫描根目录之内如果是直接报错拒绝启动。5.2 权限、占用与长路径以 SYSTEM 身份运行时%TEMP%之类的环境变量指向的是系统临时目录不是当前登录用户的临时目录。我踩这个坑的时候配置里写的是%TEMP%结果每次扫描都在扫一个空目录报告里显示清理候选 0 字节我还以为是过滤太严格。解决办法是配置里全部用绝对路径不要用环境变量或者显式列出多个候选路径让工具自己去判断哪个存在。文件被占用是另一个高频问题。Windows 不像类 Unix 系统那样可以删除正在被打开的文件遇到占用的文件移动或者删除都会抛WinError 32。我的处理是捕获这个错误把该文件加进跳过清单并在日志里记录。如果同一个文件连续多次扫描都被跳过说明有程序长期占着它这时候可以在报告里显式提示。长路径问题在国内用户的机器上特别常见因为很多项目的目录嵌套很深加上中文路径之后长度很容易超过 260 字符。不开长路径支持的话Python 的很多文件操作会直接失败。我的做法是统一在路径前加\\?\前缀同时在注册表里开启长路径支持作为兜底。注意加前缀之后必须用绝对路径而且不能用..这类相对路径片段否则系统不认。5.3 误删与恢复误删是必然会发生的事情关键是发生之后多久能恢复。我的恢复流程是这样的先通过清单文件定位到是哪个时间点的哪次清理然后执行还原命令最后检查冲突报告。python csg.py restore --manifest data/manifests/20251114-091233.json --dry-run python csg.py restore --manifest data/manifests/20251114-091233.json --apply先跑 dry-run 是必须的因为还原过程中可能遇到原路径已经被新文件占用的情况。冲突处理策略我选了保守——不覆盖只报告。这样最坏情况是有些文件没还原回去但不至于把新文件覆盖掉造成二次损失。还有一个细节是权限保留。移动文件的时候如果隔离区和源在同一个卷上shutil.move走的是重命名权限和所有者信息都保留。但如果跨卷就是复制加删除权限信息可能丢失。所以我在还原逻辑里加了一步还原后把文件的权限重置为继承父目录避免还原回来的文件带着奇怪的 ACL 导致打不开。5.4 问题速查表把上面这些整理成一张表方便现场对着查。现象最可能的原因排查动作处理方式扫描进度长时间停在同一路径踩到联接点或网络路径查看该路径属性是否有重解析点跳过重解析点加排除前缀扫描结果体积虚高近一倍目录联接重复计数对比两处路径的实际内容开启联接点跳过检查清理候选恒为 0路径用了环境变量且身份不匹配检查任务运行身份和展开后的路径改用绝对路径并显式列出移动时报 WinError 32文件被占用看是哪个进程占用跳过并记入跳过清单操作报路径不存在但路径确实存在超过 260 字符限制数一下路径长度加\\?\前缀或开长路径支持内存占用持续上涨结果集全量驻留内存观察文件数量级分批落盘或只存聚合隔离区越来越大不释放过期清理任务没跑看任务计划历史检查任务的运行条件配置告警反复推送同一问题去重逻辑失效检查去重键的构成用盘符加级别做去重键6. 踩过的坑和还能怎么玩写了两个周末跑了一个多月我觉得最有价值的部分其实是这些坑。6.1 七个印象深刻的坑第一个是删掉了自己的构建缓存。第一版规则太激进把超过 30 天的所有node_modules都列进了候选。结果是某个不常动但偶尔要编译的项目下次编译从 40 秒变成 6 分钟。教训是构建产物这类东西删除的价值是省空间成本是重建时间必须让用户自己权衡工具不能替用户决定。后来所有涉及构建产物的规则我都设成report_only。第二个是时间比较写反了。我第一版把mtime 截止时间当成了候选条件而正确的应该是mtime 截止时间代表文件够老……这里我写混过一次实际是把新文件判成了候选。幸好当时是 dry-run报告一出来我就发现所有候选都是当天的文件立刻意识到条件写反了。这个经历让我坚定了一个原则任何清理规则上线前必须至少跑三天 dry-run。第三个是 SYSTEM 身份下的路径差异。前面提过不重复了。但这个坑的隐蔽之处在于它不报错只是静默地扫了个空目录你看报告才发现不对。第四个是隔离区跨盘移动。一开始我把隔离区放在 C 盘扫描目标是 D 盘的工作目录结果移动一个 20GB 的目录花了十几分钟中途我还以为卡死了。后来检查代码才发现是跨卷导致的重命名退化成复制。改成同盘隔离之后同样的操作是秒级的。第五个是日志文件自己把盘占满了。这个有点黑色幽默——一个空间守卫工具因为日志级别设成了 DEBUG 而且没做轮转自己写了好几个 GB 的日志。后来加了按大小轮转单文件最大 10MB保留 5 个。第六个是任务重入。有一次扫描任务还没跑完下一个调度周期又启动了两个进程同时扫同一个目录磁盘 IO 直接跑满机器卡得没法用。解决办法是加了一个基于文件锁的单实例检查启动时尝试独占打开一个 lock 文件失败就直接退出并记日志。第七个是告警疲劳。最初我给每个盘都设了告警包括一个本来就常年紧张的移动硬盘结果每天收到三四条告警一周之后我就完全不看这些消息了。后来把移动硬盘的告警关掉只保留系统盘和数据盘并且把去重窗口从 12 小时延长到 24 小时。告警的价值在于少而准多了等于没有。6.2 后面还能怎么扩展现在的版本够我用但有几个方向我一直在想。一个是接入文件系统的变更日志。Windows 的 USN Journal 记录了卷上所有文件的变更如果用它来做增量扫描理论上可以把每次扫描从 90 秒压到几秒。代价是复杂度上一个台阶需要处理日志回绕、需要自己维护一个快照索引。我打算等现在这套跑顺了再说。另一个是把规则库做成可分享的。我现在的规则基本是手工攒的但如果能做成一份带注释的社区规则集新用户导入就能用价值会大很多。难点在于不同人的目录结构差异很大规则要做得足够参数化。还有就是加一个轻量的本地看板。现在看结果要么看命令行输出要么看 Markdown 报告都谈不上直观。如果能有个本地的页面显示空间趋势曲线和最近几次清理的记录会舒服很多。不过这属于锦上添花优先级排在最后。最后分享一个我自己觉得挺有用的小技巧别急着删先做一次全量扫描然后把结果存档。我第一次扫描的结果存了下来两周后再扫一次两份数据一对比谁在增长、增长了多少一目了然。这个两次快照对比的方法比任何清理都更能帮你找到真正的空间黑洞——因为清理只是治标搞清楚增长源头才是治本。我那个每天写 2GB 日志的失控项目就是这么被发现的。
返回列表