Linux 生产环境磁盘 100% 故障排查与根治实录

一台 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

三、三个真凶总结

#根因大小原因本质问题
1NLog 错误日志54GDebug 进程跑 16 天,MQTT 重连死循环写 Warn日志无单文件大小限制
2Docker 容器日志339GMySQL 2 个月 stdout 无限累积Docker 默认无日志轮转
3git 重复副本34Grelease/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 每天兜底

七、经验教训

  1. dudf 对不上的时候,一定是僵尸文件——找 lsof +L1 查看被进程持有但已删除的文件
  2. Docker 的 docker system df 不会显示容器日志大小——必须直接看 /var/lib/docker/containers/
  3. NLog / Serilog 一定要配文件大小上限——光按天切割不够,遇到死循环日志一天就能写爆
  4. Debug 版服务不要放后台跑着不管——一个 MQTT 重连循环两周能写出上百 GB
  5. 清理是治标,限制是治本——杀掉进程、截断日志只是暂时,配好日志轮转才能一劳永逸
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值