1. “rea”到底是什么:一台挤爆的开发机逼出的资源普查工具
先说结论,我再解释为什么。
你看到的“rea”,是我自己在一台快被日志、容器镜像和缓存文件塞满的台式机上,临时写出来的一个资源普查命令集合。它不是某个毛坯界面,也不是什么商用产品,更不是一段只在演示环境里能跑通的演示脚本——”rea“就是Reduce Everything Approach(什么都瘦身)三个单词的首字母缩写,本质是一组把 disk、memory、CPU 和 container 四类资源一次性盘清楚的 Bash 脚本。
为什么会有它?上个月我碰上一台开发机,磁盘使用率到了 94%,每次docker build之后系统就卡到几乎没法敲命令。我一开始以为是内存不够,结果free -h一看还有 12G 空闲;以为是某个服务把 CPU 吃满了,top排序好几轮也没发现异常。最后打开 NCDU 才发现,问题的元凶是/var/lib/docker/overlay2和~/Library/Caches下面堆了几百 GB 的陈旧文件。那台机器系统变慢,根本原因是磁盘到了临界线,页面文件、临时目录和日志服务在这个状态下疯狂争抢 IO。
刚听到“rea”这个名字的人,通常第一反应是:这是不是某个新框架?或者是某种运行时环境?我干脆把话说清楚:它不是,也无意成为大而全的监控平台。它解决的,是每个搞开发的人都绕不开的那类“破事”——磁盘突然告警、容器镜像占满空间、journalctl日志体积爆炸、缓存目录一个比一个大。要是在生产环境,你一般会有完善的 Prometheus 告警和 Grafana 面板;但本地开发机呢?基本上就是“等卡到受不了了才手动清理”的状态。
这个工具适合谁?三种人最需要它:一是经常在本机起 Docker Compose 做测试的开发者,镜像和构建缓存消耗量极大;二是做前端或者移动端构建的人,node_modules和各类缓存动不动就是几十 GB;三是有多台 Linux 或 macOS 机器,平时不想一一登录上去执行命令的运维和效率控。它没有守护进程、没有服务端、没有数据上报,就是几个脚本,拷过去就能用,跑完就在终端里把结果列清楚,清理动作由你自己决定,而不是由某个工具擅自删文件。
从设计思路上说,我一开始也想图省事,直接用现成的 Batocera 之类的工具。但用了一圈发现,大部分图形化磁盘分析工具只解决“看”的问题,不会把内存、CPU 和容器空间统一汇总,更不会在同一个输出里给出“可操作建议”。我不想要一个只会画方块的工具,我想要的是**一次命令之后,能直接告诉我‘谁该清、谁必须留着、清完能省多少空间’**的诊断结果。这就是我把 rea 设计成“四维普查 + 定向清理建议”的原因。
2. Rea 的设计逻辑与三个核心原则
写 rea 之前,我给自己定了三条死规矩,这三条规矩后来也被证明是这套脚本能落地、能传播、能长期用下去的根本原因。
第一,只读优先,清理必须显式化。默认执行rea的时候,脚本只做扫描、统计和展示,不删任何东西。所有涉及删除或停用容器的操作,都必须额外加--clean之类的后缀参数。这么设计的理由很简单:自动删除太危险。尤其是 Docker 的孤儿卷和 build cache,有些数据可能是某个还没提交到远端仓库的中间结果,删了就得重新跑一遍。我宁可多打一个参数,也不想在凌晨三点误删数据。
第二,视图分离,扫描与解读分开。rea默认输出的是一个简明的总览表,只会列出排名靠前的几个隐患点,免得输出上百行把人淹没;加上-v(verbose)之后,才会打印每个进程的详细信息、每个缓存目录的明细和每个容器的状态。这个设计是从一个真实蠢坑里学来的。早期版本我试过一次性把所有信息都打出来,结果终端滚了几百行,真正要紧的内容反而被淹没在里面。现在默认输出只显示 25 行以内的摘要,用户想看细节再手动打开。
第三,跨平台命名一致,行为有差异。macOS 和 Linux 在命令细节上有很多不兼容的地方,比如du在 macOS 上是-sh,部分 Linux 发行版则需要配合--block-size使用;再比如内存信息,Linux 可以直接读/proc/meminfo,macOS 就需要用vm_stat。rea 在内部做了一层薄薄的封装,把平台差异全部吸收掉,但对使用者的命令入口完全统一。换句话说,Linux 和 macOS 上打出的是同一个rea命令,行为一致。
这三个原则看上去都是最简单朴素的工程判断,但恰恰是它们保证了脚本的鲁棒性。下面我会把四个模块(Disk、Memory、CPU、Docker)一一拆开,讲一下每个模块具体做了什么,以及里面有什么值得抄走的经验。
3. 四维盘点拆解:磁盘、内存、CPU 与容器的扫描实现
3.1 磁盘模块:不只查磁盘满了,还定位“谁吃掉了空间”
磁盘模块不复杂,但里面有一个关键细节:扫描深度和排序策略。
rea disk默认执行三步操作:
- 用
df -h拿到所有挂载点的使用率,过滤掉tmpfs、devtmpfs这类虚拟文件系统,只剩对业务有实际影响的真实分区。 - 对
/var/lib/docker、~/Library/Caches、~/.cache、/tmp、$HOME下的常用缓存目录做du -sh扫描,按人可读格式排序,列出 Top 20 大目录。 - 输出后备注清理建议。这一步是纯规则的:比如
/var/lib/docker超过 30G,就提示“建议运行 docker system prune”;~/.npm或yarn缓存超过 5G,就提示“可以用 npm cache clean --force”。
为什么要把缓存目录单独扫一遍,而不是像 NCDU 那样全盘展开?
因为全盘展开的树形扫描虽然信息全,但输出噪音太多。在一个正常开发机上,/usr和系统目录加起来动辄几十万个文件,你真正该关心的是几个体积膨胀到恶性的目录。rea 直接从已知的“嫌疑犯”开始查,秒级出结果。为了查得更快,我在脚本里对每个目标目录限定了timeout 30,如果扫描超时就跳过并在结果里标记 SKIP,避免某些目录因为特殊的文件系统挂载问题把整个命令卡死。
check_dir_size() { local dir="$1" local limit="$2" if [ -d "$dir" ]; then local size size=$(du -sh "$dir" 2>/dev/null | cut -f1) echo -n "$dir $size" if [ -n "$limit" ]; ]; then # 简单比较大小,超过 limit 时输出 WARN ... fi fi }这段代码是示意性的,真实脚本里我把每个目录的大小阈值都做成了数组,未来维护起来很方便。顺带提一句,du在一些并行文件系统上跑会比较慢,如果第一次运行觉得卡,可以把--max-depth=1改成--max-depth=0,减少一层目录展开。
3.2 内存模块:从/proc/meminfo和vm_stat里挖出真实压力
内存模块的核心是判断“我这台机器究竟是内存不够,还是缓存占用过多”。
在 Linux 上,我读取的是/proc/meminfo,这个文件虽然年代久远,但到今天依然是信息量最大的内存真相来源。关键字段有三个:MemAvailable直接反映可用内存量,不用自己减;Cached显示文件缓存占了多少;SwapTotal和SwapFree的组合能看出有没有swap颠簸现象。
macOS 侧则读vm_stat,单位是页(page),需要乘以页面大小才能换算成字节,命令行里执行vm_stat的时候通常会带有page size of 4096 bytes这行提示。脚本里我会先sysctl hw.pagesize拿到实际的页大小,再依次计算Pages free、Pages inactive、Pages active和Pages wired。
很多开发者只看“可用内存 8G”“空闲内存 6G”就说内存没问题,其实这是个常见误区。Linux 的 Cached 内存是内核主动用于加速读文件的缓存,在内存有压力时会被回收复用,不应该当作浪费;但Pages wired或不可回收内核内存积累太多,那才是真的占着不放。rea 在输出时会把这两类分开:一类是“可以回收的缓存”,一类是“不可回收的真实占用”,这样排错时就能立刻判断是否需要加 SWAP 或减少并行任务数。
if [ -f /proc/meminfo ]; then MemAvailable=$(awk '/MemAvailable/ {print $2}' /proc/meminfo) SwapTotal=$(awk '/SwapTotal/ {print $2}' /proc/meminfo) SwapFree=$(awk '/SwapFree/ {print $2}' /proc/meminfo) fi如果 SwapTotal 虽然是 0,但 MemAvailable 已经低于 1G,脚本会输出强警告;还有个比较实用的扩展,配合pidstat或者/proc/*/status去查具体进程的VmRSS: /proc/[pid]/status里每个进程的 VmRSS 一加,就是整机物理内存使用量的近似视图。rea 里我用了ps -eo pid,rss,comm --sort=-rss | head -20来拿前 20 个内存大户,抓住了 RSS 破 GB 的进程,内存问题基本就可以定位了。
3.3 CPU 模块:用“平均负载拐点”识别真正的瓶颈,而不是只看百分比
CPU 的部分,我强调一个反直觉的判断方式——不要只看top里的 CPU 使用率数字。
很多朋友在遇到卡顿时,看到top里某进程 CPU 90% 就以为是它把机器弄卡的,但实际情况往往是:机器慢是因为磁盘 IO 瓶颈,CPU 使用率虚高只是因为进程在不停重试或者忙着等待 IO 完成。真正要找的,是平均负载(Load Average)在短时间内出现明显拐点。
rea 的 CPU 模块会读取/proc/loadavg或uptime,把 1 分钟、5 分钟、15 分钟的负载值打印出来,并与 CPU 核数做对比。比如一台 8 核机器,如果 1 分钟负载是 12,而 15 分钟负载只有 3,说明刚刚有一波突发任务把系统拖住了;如果 15 分钟负载长期大于 8,说明这台机器持续超载,不是一瞬间的事。
然后它才会去展示进程 CPU 排名。这里有个更容易踩的坑:ps -eo pcpu显示的 CPU% 是进程生存周期内的平均值,而不是瞬时的瞬间值。对于刚启动的高消耗进程,这个百分比会被严重稀释;而对于一个已经运行了 7 天的常驻进程,哪怕它最近把 CPU 吃满了,平均出来也只有 5%。所以 rea 不是只靠ps拿一次快照,而是用top -b -n 1 -o %CPU这样的批处理模式拿到瞬时 CPU 占用,用瞬时值排序后再结合负载数值做判断。
# 获取瞬时 CPU 占用 Top 15 top -b -n 1 -o %CPU | sed -n '8,22p'为什么选top -b -n 1?“-b是 batch 模式,不在交互界面里卡住;“-n 1”只跑一轮,秒退;“-o %CPU”按 CPU 使用率排序。三个参数组合起来,就是脚本脚本化获取瞬时 CPU 消耗的标准答案。
3.4 容器模块:把镜像、容器、卷和构建缓存全都暴露出来
容器模块是这台机器能“起死回生”的关键。日常开发积累下来的 Docker 对象,体积膨胀速度远超过想象。rea 容器模块用docker system df作核心,再补上docker ps -a --size来展示每个容器的可写层大小。
docker system df是一个被低估的命令,单条命令就能看到镜像、容器、本地卷、构建缓存四类对象分别占了多少空间,以及有多少处于“可回收”状态。后面再配上-v(verbose)参数时,脚本才会跑docker images --format列出每个镜像的 ID、仓库名、创建时间和体积,按占用从大到小排序。
容器模块还有个“安全删除”设计:rea docker --clean会依次执行docker container prune -f先清理停止的容器,然后docker image prune -f清理无用镜像,最后docker builder prune -f清构建缓存。为什么是这个顺序?因为容器如果在运行,引用的镜像就不能删;先停容器,再删镜像,最后清构建缓存,逻辑顺序最顺。这里绝不默认加--volumes来清理卷,因为卷里经常埋着数据库数据,是本地开发的高危数据区。
docker_prune() { echo "[1/3] 清理停止容器..." docker container prune -f echo "[2/3] 清理悬空镜像..." docker image prune -f echo "[3/3] 清理构建缓存..." docker builder prune -f }运行完一套清理后,rea 会再次调用docker system df对比清理前后的空间差。这一步很重要,给人“做减法后的即时反馈感”。我看到过不少同事清理完 Docker 之后没有对比数据,无法量化成果,过两天又会忘记这回事。加一个前后对比的输出,养成定期清理的习惯就容易多了。
4. 实际运行效果与实际使用中容易忽视的细节
我拿自己那台 512G SSD 的开发机来举个可复现的例子。跑rea disk第一次扫描的结果是这样的:
挂载点 / 使用率 74% 剩余可用 98G /Docker 镜像仓库目录 138G WARN 超过 30G ~/.cache 42G WARN 超过 5G ~/Library/Caches 38G WARN 超过 5G看,问题一上来就暴露了。以前用df -h看到 74% 使用率时,我并没有很着急,以为一切正常,但其实 Docker 相关目录和缓存已经吃掉了 200G 空间。紧接着我执行了rea docker --clean,清理的动作很快,五分钟左右就完成,再去调用rea disk:
挂载点 / 使用率 51% 剩余可用 171G Docker 镜像仓库目录 44G OK 在阈值内 ~/.cache 22G OK 在阈值内 ~/Library/Caches 15G OK 在阈值内清理出整整 73G 空间。这对于没加磁盘的物理机来说,等于多出来一个空闲盘。
上面这个案例里有个细节是很多人会忽略的:~/Library/Caches和~/.cache里面的数据,可能是某些 IDE 索引、模拟器镜像或者浏览器缓存,真的不能无脑全删。rea 给出的阈值策略是:超过限值才建议清理,并且清理指定目录,不碰其它位置。执行rea cache --clean时,脚本也只对这个目录下可以安全重建的缓存下手,用户自己生成的配置类缓存被排除在删除列表之外。
4.1 进程扫描时被误判的“内存大户”
我第一次跑rea mem时,输出里前两名是虚拟机进程和一个 IDE 的索引服务,加起来消耗 6G 多。按直觉,这俩是怀疑对象。但脚本分析进程的启动时间和持续活跃度后发现,它们设定的是固定常驻内存兴奋,属于正常资源占用,并不能算泄漏。真正的问题反而是它们触发的一连串文件变更事件,让某个后台同步服务频繁重启和写日志,在 30 分钟时间里产生了 4G 的日志文件,这才是磁盘满的直接导火索。
所以我后来给 rea 额外加了一个journal扫描模块,专门扫/var/log和~/Library/Logs下最近 7 天有改动的日志文件。这个优化很实用。排查性能问题,不能只看资源占用最高的进程,还要看“谁让别的进程变高”。
4.2 在 macOS 上的兼容性问题
rea 在 macOS 和 Linux 上的输出结构一致,但脚本内部有不少条件分支。最典型的是du -sh在 macOS 默认输出的单位用的是 1024 进制,Linux 则可能用 1000 进制,如果拿数值直接和阈值比较,会造成误判。我的解法是在脚本开头定义一个BYTES变量,用du -sk拿到固定的 1024 字节粒度,再统一换算,不依赖不同系统默认单位的差异。
另一个差异点:macOS 上默认没有/proc/meminfo,vm_stat的页面大小不一定都是 4096,所以要动态获取。真实的脚本里,平台判断写在最前面,把记忆、磁盘、容器三个模块各自需要的基础参数全准备好,后面的逻辑只用统一变量,不用再反复判断系统。
case "$(uname -s)" in Linux) MEM_AVAILABLE=$(awk '/MemAvailable/ {print $2}' /proc/meminfo) ;; Darwin) PAGE_SIZE=$(sysctl hw.pagesize | awk '{print $2}') MEM_AVAILABLE=$(vm_stat | awk ...) ;; esac这就是跨平台脚本很土但很稳的写法。uname -s分支把差异在一开始就吸收掉,后面干干净净。
5. 自动化定时与脚本扩展:把“急修”变成“常备”
如果说前面四章是“工具能做什么”的功能部分,那这一章是更值钱的经验:怎么把一个只会在出问题时想起的工具,变成能主动发现苗头的“哨兵”。
rea 在设计时没有做成常驻守护进程,但提供了建议级的 cron 配置。原样照搬一个 crontab 条目是这样:
0 9 * * 1 /usr/local/bin/rea disk --cron >/tmp/rea_disk_report.txt 2>&1 0 */4 * * * /usr/local/bin/rea docker --check >/tmp/rea_docker_report.txt 2>&1周一早上 9 点跑一次磁盘扫描,每 4 小时检查一次 Docker 空间。这样时刻都在心里有数,而不是等到某天 CI 突然因为磁盘空间不足失败才去救火。
如果想把 rea 变成一个信息聚合的入口,还可以利用它的定位特性,把输出喂给 shell 别名或者你自己的监控脚本。比如我实际的做法,是配合 macOS 的shortcuts命令在每天午休时弹一个通知,报告磁盘剩余空间和 Docker 镜像总量。Linux 用户则可以做一个 systemd timer,把报告发送到内部的消息频道。这样就把“本地普查工具”变成了团队的信息源之一。
另外,rea 的输出格式我特意设计成了“人类可读 + 机器可解析”的双模式:默认输出带颜色和缩进,方便人看;加上--json参数时,每条结果都变成结构化的 JSON 输出,可以像 API 一样被后续脚本消费。这样一来,同事以后写一个跨机器的汇总面板时,不用重新发明轮子,直接把每台机器的 JSON 汇总起来就行。我认为这是 rea 比多数一次性脚本多走了一步的地方。
6. 用 rea 的体会与继续优化的方向
如果你问我,搞这个工具收获最大的是什么,我可能会说:把它从“一套救命脚本”慢慢完善成“一套方法论”的过程,比脚本本身更有意思。
回过头来复盘,rea 这类工具存在的一个普遍问题在于:默认安全策略与高频使用天然有冲突。因为它坚持“清理必须显式”,很多同事拿过来后用了一次,发现没有立刻看到空间数字下降,就不愿意再用了;他们预期是“我把命令跑一下,工具就该自动给我腾出空间”。这个矛盾我目前还没完全解决,解决方案是在输出里增加一行“当前可安全回收空间预估”,把清理收益前置展示出来,让人先看到甜头,再决定要不要真正动手。这个优化已经在做了。
另一条优化方向,是让磁盘扫描支持 exFAT 或者网络挂载卷。现在du在 NFS 上会异常慢,rea 在碰到这类挂载时就直接跳过了,但那些目录反而往往是团队共享文件的堆积地,体积可能不小。等有预算再尝试用find -mtime增量统计配合缓存结果,用上次的扫描结果做差分,极大缩短扫描耗时,这个思路目前还在设计阶段。
最后想对打算自己动手写类似工具的同行提一个建议:先从一个真实的、让你肉疼的问题出发,再把一个个小脚本合成一个统一接口。不要一开始就想着做完整平台。我以前用过的很多监控工具就是因为想做得太全,最后连维护成本都覆盖不了价值。像 rea 这样,二十来个核心函数加起来不过几百行脚本,但每一个函数都是被真实问题“逼”出来的,这才是它能长期用下去的根本原因。
如果你也有类似的本地环境管理痛点,照着这个思路去做。不需要用太重的框架,不需要引入一整套监控体系,从一条du命令开始,一点一点把场景补齐起来,你也能拥有一套属于你的“rea”。