一台 502G 的开发服务器反复被占满,从 98% 到 0 字节可用,经过三轮排查揪出三个真凶,最终释放 380G 并建立四层永久防护体系。
一、故障现象
某天日常开发中,Claude Code 突然报错无法执行命令,df -h 一看:
文件系统 大小 已用 可用 已用% 挂载点
/dev/sda2 502G 464G 13G 98% /
只剩 13G 了。当时初步清理了几个大目录(release 源码副本、Docker 测试卷),空间恢复到 118G(25%)。然而没过几小时,磁盘再度被打满到 100%,0 字节可用。意识到这不是简单的"空间不够",而是有什么东西在持续疯狂写盘。
二、排查过程
第一轮:常规排查——发现但未根除
先用 du 扫一遍目录大小:
du -sh /*/ 2>/dev/null | sort -rh | head -10
结果:
63G /home/
11G /snap/
11G /var/
2.8G /lib/
2.0G /usr/
初步清理了 /home/linbot/release/source/(34G 的 git 重复副本)和 Docker 测试数据卷,空间恢复到 93%,以为问题解决了。
然而半天之后,磁盘又满了。 这次 du 只统计到 136G,但 df 显示 491G——差了 355G 失踪了!
第二轮:定位"幽灵文件"——被进程持有的已删除文件
du 看不到但 df 满了,这是经典症状:文件被删了但进程还持有文件描述符,磁盘空间不会释放。
使用 lsof 查找这类"僵尸"文件:
sudo lsof +L1 2>/dev/null | grep -v memfd | grep deleted | head -20
输出中看到大量类似:
Mysafe.We 2516572 linbot 1w REG 8,2 55134.6MB (deleted)
Mysafe.We 2516572 linbot 2w REG 8,2 55134.6MB (deleted)
dotnet 2512263 linbot 1w REG 8,2 55134.6MB (deleted)
PID 2516572 (Mysafe.WebUI) 和 PID 2512263 (dotnet) 各持有一个 55GB 的已删除日志文件!
真相:用户手动 dev-run.sh start misweb 启动了 Debug 版本的 WebUI,进程在后台跑了16 天(391 小时),MQTT 重连失败导致死循环写 Warn 日志,文件涨到 54GB。用户看到磁盘满了就删了那个日志文件——但进程没停,文件句柄还在,空间不释放。
# 强制杀进程,110GB 瞬间回来
kill 2516572 2512263
第三轮:Docker 日志黑洞——339GB 藏着没被发现
杀掉 dotnet 进程后,磁盘从 100% 只恢复到 95%,还有三百多 GB 不明去向。
用 du -x 只扫描根文件系统(不穿透挂载点):
sudo du -x --max-depth=1 / 2>/dev/null | sort -rh | head -10
结果令人震惊:
472658380 / # 总共 451G(块大小计)
370586432 /var # 353G 在 /var!
60516348 /home # home 只有 58G
之前的 du -sh /var/ 显示才 7G,现在暴涨到 353G!继续深挖:
sudo du -sh /var/lib/*/ 2>/dev/null | sort -rh | head -5
# 348G /var/lib/docker/
sudo du -sh /var/lib/docker/*/ 2>/dev/null | sort -rh | head -5
# 337G /var/lib/docker/containers/
337G 在 Docker 容器目录里!最终定位到:
sudo du -sh /var/lib/docker/containers/*/
# 339G b325f0...(cca-dev-mysql)
# 52K 1be31d...(cca-dev-redis)
cca-dev-mysql 容器从创建以来(2个多月),MySQL 的所有 stdout/stderr 被 Docker 的 json-file 日志驱动无限累积,最终膨胀到 339GB 的单个 JSON 日志文件!
# 确认文件
sudo ls -lh /var/lib/docker/containers/b325.../b325...-json.log
# -rw-r----- 1 root root 344G 8月3日 14:46 b325...-json.log
三、三个真凶总结
| # | 根因 | 大小 | 原因 | 本质问题 |
|---|---|---|---|---|
| 1 | NLog 错误日志 | 54G | Debug 进程跑 16 天,MQTT 重连死循环写 Warn | 日志无单文件大小限制 |
| 2 | Docker 容器日志 | 339G | MySQL 2 个月 stdout 无限累积 | Docker 默认无日志轮转 |
| 3 | git 重复副本 | 34G | release/source 和 mysafe 完全重复 | 历史遗留 |
三者叠加 ≈ 427G,加上其他零散文件,正好把 502G 打满。
四、解决方案
紧急止血
# 1. 杀 dotnet 进程(释放已删除日志的句柄)
kill 2516572 2512263
# 2. 截断 Docker 容器日志
sudo truncate -s 0 /var/lib/docker/containers/<CONTAINER_ID>/<CONTAINER_ID>-json.log
# 3. 重启容器释放句柄
docker restart cca-dev-mysql
永久防护:四层防御体系
第一层:NLog 文件大小限制
修改 NLog.config,给所有 File Target 加上 archiveAboveSize:
<!-- 错误日志:单文件上限 100MB,按天或按大小自动归档 -->
<target name="error_file" xsi:type="File" encoding="utf-8"
maxArchiveFiles="30"
archiveEvery="Day"
archiveAboveSize="104857600"
fileName="${basedir}/Logs/${shortdate}_error.log"
.../>
<!-- 业务日志同样加上 -->
<target name="business_file" xsi:type="File" encoding="utf-8"
maxArchiveFiles="7"
archiveEvery="Day"
archiveAboveSize="104857600"
.../>
效果:单日志文件不超过 100MB,配合
maxArchiveFiles上限,错误日志最多占 30 × 100MB = 3GB。
第二层:Docker 容器日志轮转(核心)
重建容器时加上 --log-opt 参数:
docker run -d --name cca-dev-mysql \
--log-opt max-size=100m --log-opt max-file=3 \
-p 3308:3306 \
-v <volume>:/var/lib/mysql \
-e MYSQL_ROOT_PASSWORD=xxx \
mysql:8.0
效果:每个容器日志上限 100MB × 3 个文件 = 300MB,永远不可能再膨胀到 339G。
注意:
/etc/docker/daemon.json的全局配置只对新创建的容器生效,已存在的容器需要重建才能应用。
第三层:Cron 定时清理
# 每2小时清理 Claude Code 旧会话临时文件
0 */2 * * * find /tmp/claude-1000 -mindepth 1 -maxdepth 1 -type d -mmin +120 -exec rm -rf {} \; 2>/dev/null
# 每天凌晨3点清理 /tmp 旧文件(超过12小时)
0 3 * * * find /tmp -type f -mmin +720 -delete 2>/dev/null
# 每天凌晨4点截断超过100MB的应用日志(兜底)
0 4 * * * find /home/linbot/new-cca-mis /home/linbot/mysafe -path '*/Logs/*.log' -size +100M -exec truncate -s 0 {} \; 2>/dev/null
# 每天凌晨4点 Docker 日志兜底(超过500MB就截断)
0 4 * * * find /var/lib/docker/containers -name '*-json.log' -size +500M -exec truncate -s 0 {} \; 2>/dev/null
第四层:开发习惯
dev-run.sh start启动的服务,用完记得dev-run.sh stop- Debug 版本的 dotnet 进程不要放任跑几周不管
- 定期
docker system df检查容器资源使用
五、关键排查命令速查表
| 场景 | 命令 |
|---|---|
| 磁盘概况 | df -h / |
| 目录大小排行 | du -sh /*/ 2>/dev/null \| sort -rh |
| 不穿透挂载点 | sudo du -x --max-depth=1 / \| sort -rh |
| 找大文件 | sudo find / -xdev -type f -size +1G -exec ls -lh {} \; |
| 找僵尸文件(已删除但进程持有) | sudo lsof +L1 2>/dev/null \| grep deleted \| head -20 |
| 按大小过滤僵尸文件 | sudo lsof +L1 \| awk '$7 ~ /^[0-9]+$/ && $7>10000000' |
| Docker 磁盘 | docker system df -v |
| 容器日志文件大小 | sudo ls -lh /var/lib/docker/containers/*/ |
| 截断容器日志 | sudo truncate -s 0 /var/lib/docker/containers/<ID>/<ID>-json.log |
| 截断后释放空间 | docker restart <container>(或 kill 持有句柄的进程) |
六、最终效果
清理前:502G / 502G 100% 🔴
清理后:122G / 502G 26% 🟢
释放空间:380G
防护前:随时可能被单个日志打满
防护后:Docker 每容器 ≤300MB、NLog 单文件 ≤100MB、Cron 每天兜底
七、经验教训
du和df对不上的时候,一定是僵尸文件——找lsof +L1查看被进程持有但已删除的文件- Docker 的
docker system df不会显示容器日志大小——必须直接看/var/lib/docker/containers/ - NLog / Serilog 一定要配文件大小上限——光按天切割不够,遇到死循环日志一天就能写爆
- Debug 版服务不要放后台跑着不管——一个 MQTT 重连循环两周能写出上百 GB
- 清理是治标,限制是治本——杀掉进程、截断日志只是暂时,配好日志轮转才能一劳永逸

57

被折叠的 条评论
为什么被折叠?



