/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */
#content_views .toc,
/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */
#content_views.markdown_views > p:empty:has(+ .toc),
#content_views.markdown_views > .toc + p:empty,
/* 富文本旧版目录标记 */
#content_views.htmledit_views #main-toc,
#content_views.htmledit_views #hr-toc,
#content_views.htmledit_views p[id*=\”-toc\”] {
display: none !important;
}
/* 目录去掉后,紧跟的首个标题不再多出一块上边距 */
#content_views.markdown_views > .toc + h1,
#content_views.markdown_views > .toc + h2,
#content_views.markdown_views > .toc + h3,
#content_views.markdown_views > .toc + h4,
#content_views.markdown_views > .toc + p:empty + h1,
#content_views.markdown_views > .toc + p:empty + h2,
#content_views.markdown_views > .toc + p:empty + h3,
#content_views.markdown_views > .toc + p:empty + h4 {
margin-top: 0 !important;
}
最让人困惑的一种服务器磁盘空间告警,不是"满了",而是满了却找不到是谁占的。
df 说这个分区用了 95%,你用 du 把目录挨个加起来,却发现所有文件加起来才 40%。剩下的空间去哪了?
这种情况我遇到过不止一次。它通常不是误报,而是你漏看了一个维度——这两个命令统计的根本不是同一件事。
下面按真实排查顺序走一遍,从"怎么判断是哪一类满"到"删了文件为什么空间不释放"。
一、先分清是哪一类"满"
看到 No space left on device 或磁盘告警,第一件事不是去找大文件,而是判断满的是哪种资源。
日常在 linux 查看服务器磁盘空间,多数人只敲一个 df -h 就去找大文件了,结果经常白忙一场。正确的做法是至少同时看两个数:
df -hT
df -i
df -hT 看容量和文件系统类型,df -i 看 inode。
如果 Use% 接近 100%:这是真的空间不够,继续往下看第二节。
如果 Use% 还行,但 IUse% 到了 100%:这是 inode 耗尽。每个文件(含目录、硬链接)都要占一个 inode,文件数把 inode 用光了,哪怕还剩几十 GB 空间,创建新文件同样会报 No space left on device。
这两者报错完全一样,解法完全不同。inode 耗尽的常见来源是几百万个小文件:PHP session、邮件队列、CI 构建产物、某种"每次请求写一个文件"的应用 bug。
定位 inode 大户:
sudo find / -xdev -printf '%h\\n' 2>/dev/null | sort | uniq -c | sort -nr | head -20
-xdev 很关键——不让 find 跨文件系统跑,否则会扫到 /proc、/sys 这些虚拟文件系统,结果全是噪音。
还有一类容易漏的:/dev/shm、/tmp 这类 tmpfs 是内存文件系统,它们满了报的就是内存不够,不是磁盘不够。用 df -h /tmp 单独看一下就能区分。
二、找占用:du 的正确用法
确认是容量问题后,就该查清楚服务器磁盘空间具体被哪些目录吃掉了。查看linux服务器磁盘剩余的分布,用 du 定位。注意这两个参数:
du -xh / –max-depth=1 2>/dev/null | sort -rh | head -20
- -x:不跨文件系统。/ 下面往往挂着别的分区甚至网络存储,不加 -x 会把所有挂载点的空间重复算一遍,出来的数字完全对不上。
- –max-depth:控制层级。-x 配合 –max-depth=1 先定位到一级目录,然后对着最大的那个目录再往下钻,比一次性 du -sh * 铺满屏幕有效率。
想交互式的,装 ncdu:
ncdu -x /
它会把目录按大小排好,可以直接在界面里进目录、删文件。生产环境排查时这个比反复敲 du 快得多。
三、df 和 du 差很多:三个真实原因
这是本文标题想解决的问题。先说结论:这俩命令统计的口径本来就不一样,差值不可能严格等于零。真正需要处理的,是差值大到几十 GB 的情况。
原因一:文件被删了,但进程还拿着它(最常见)
Linux 下删除文件,本质是在目录里去掉一个链接。只要还有进程持有这个文件的句柄,inode 和数据块就不会真正释放。
于是:
- du 遍历目录树,看不到这个已经从目录里消失的文件 → 不统计
- df 读文件系统超级块,那块空间确实还被占着 → 统计
差值就是这么来的。
查证很简单:
lsof | grep deleted
# 或
lsof +L1
lsof +L1 列出链接数小于 1 的文件,也就是"已从目录删除但仍被打开"的那些。输出里能看到进程名、PID、占用大小。如果这里有大文件,df 和 du 的差就有了完整解释。
关于 lsof 的完整字段与选项,可查阅 lsof(8) 手册页。
原因二:挂载点把原来的文件盖住了
做 linux服务器磁盘挂载 时有个容易忽略的顺序问题:如果先往 /data 写了 50GB 数据,之后又把一块盘挂载到 /data,原来的 50GB 就被"盖住"了——du 遍历不到(被新挂载遮蔽),但空间实实在在被占着。
区分方法:临时 umount 掉该挂载点,再看原目录的大小。生产环境做这个操作前务必确认没有服务正在读写。
原因三:口径差异本身
这一条不是故障,是必须知道的常识:
- df 统计的是文件系统的块使用情况,包含元数据和保留块
- du 是逐个文件累加,对硬链接(只算一次)、稀疏文件(按实际占用而非表观大小)、无权限读取的目录(会被跳过)表现都不同
所以几 GB 的差异是正常的。只有当差值达到几十 GB,并且能在 lsof +L1 里找到对应的 deleted 文件,证据链才算完整。不要看到几 GB 差就急着重启服务。
四、空间不释放:用 truncate,不要用 rm
这是实操里最常犯的错。
日志涨到 30GB,很多人第一反应是:
rm /var/log/myapp/app.log # 错误示范
删完之后 df 一点没变——因为写日志的进程还持有这个文件的句柄,空间不会释放,而且你还失去了对这个文件的控制(没法再截断它了)。
正确做法是清空而不是删除,inode 保持不变,进程继续往里写,空间立刻释放:
# 方式一
truncate -s 0 /var/log/myapp/app.log
# 方式二(效果相同)
> /var/log/myapp/app.log
如果文件已经被 rm 掉了,还能补救——通过 /proc 找到那个还在被持有的文件描述符,直接截断它:
# PID 和 FD 从 lsof +L1 的输出里取
truncate -s 0 /proc/$PID/fd/$FD
这样不需要重启。只有当拿不到 PID、或者想彻底恢复干净状态时,才去 systemctl restart 对应服务——注意是重启服务,不是重启整机。
顺带说预防:配好 logrotate。copytruncate 是一个选项,更规范的做法是让程序在收到信号后重新打开日志文件(reload hook)。无论哪种,核心是不要让"删除"成为清理手段。
五、三个大户:journal、Docker、包缓存
清理要按风险从低到高来。先动这些确定无害的:
systemd journal
journalctl –disk-usage # 先看占了多少
journalctl –vacuum-size=500M # 保留 500MB
journalctl –vacuum-time=7d # 或保留最近 7 天
治本的做法是在 journald.conf 里设 SystemMaxUse=,否则它会周期性涨回来。
Docker
docker system df # 看镜像/容器/卷/构建缓存各占多少
docker system prune -f # 清理停止的容器、无用网络、悬空镜像、构建缓存
docker image prune -a -f # 清理未被任何容器使用的镜像
⚠️ docker system prune –volumes 会删数据,不只是删层。如果有匿名卷装着数据库文件,执行前必须确认。生产环境我只在明确知道卷可弃时才加这个参数。
容器日志也是隐形大户,可以在 Docker 配置里限死:
{
"log-driver": "json-file",
"log-opts": { "max-size": "50m", "max-file": "3" }
}
包管理缓存
apt clean # Debian/Ubuntu
dnf clean all # RHEL 系
六、还有空间却写不进去:ext4 的保留块
有种情况很迷惑:df 显示还剩 5%,但应用就是报 No space left on device。
这是 ext4 默认保留了 5% 的块给 root,目的是磁盘写满时管理员还能登录清理。非 root 进程在用到 95% 时就写不进去了——明明有空间,但那部分不是给你的。
查看保留量:
sudo tune2fs -l /dev/sda1 | grep -i 'reserved block'
数据盘上可以调低:
sudo tune2fs -m 1 /dev/sdb1
⚠️ 三条限制必须知道:
七、一个可以直接照做的清理顺序
| 1 | df -hT + df -i 判定是容量还是 inode | 无 |
| 2 | du -xh –max-depth 定位大目录 | 无 |
| 3 | lsof +L1 查幽灵文件 | 无 |
| 4 | journalctl –vacuum-size | 低 |
| 5 | truncate 清空大日志(不要 rm) | 低 |
| 6 | apt/dnf clean、清理旧内核 | 低 |
| 7 | docker system prune -f | 中(别带 –volumes) |
| 8 | 动应用数据 | 高,必须有备份或快照 |
| 9 | tune2fs -m 调保留块 | 中(仅限 ext4 数据盘) |
第 8 步之前,先问一句:这些数据删了能不能恢复?生产环境没有快照就删应用数据,是把一次磁盘告警变成一次数据事故。
八、什么时候该扩容,而不是清理
如果做完上面这些,空间在很短时间内又涨回去,说明这不是一次清理能解决的问题,而是容量规划问题。
判断依据很简单:把清理掉的量和增长速率做个除法。清了 30GB、一周后又满了,那扩容比反复清理划算——运维的时间成本也是成本。
两种扩容方向:
- 纵向扩:直接把现有云盘扩到更大,多数云平台支持在线扩容(不停机扩盘,之后再扩分区与文件系统)。具体步骤和限制参见云服务器在线扩容云盘文档。
- 横向拆:把 /var、容器数据、数据库拆到独立的数据盘上。好处是某一类数据暴涨不会连带把根分区塞满导致系统登不进去——前面第 1 步里"根分区满了"是最难处理的一种,因为连排查工具都可能写不了临时文件。
容量告警阈值也建议提前设成 75% / 85% 两级,而不是等到 98%。等 98% 才告警,意味着你没有时间从容排查,只能在压力下做决策——而这恰恰是最容易误删的时刻。
关于本文命令的说明:文中的命令与参数基于主流 Linux 发行版的常见行为,du/lsof/journalctl 等工具在不同版本间选项基本一致,但涉及文件系统调整的操作(tune2fs -m、扩容后扩分区)会直接改动磁盘元数据,执行前请确认文件系统类型、设备路径,并在有快照的前提下进行。保留块比例、journal 上限这类数值给的是常用参考,具体应按你的磁盘规模和业务特性调整。
回到开头那个问题:服务器磁盘空间告警的时候,真正要花时间的不是删文件,而是判断满的是容量、inode 还是句柄。判断对了,后面就是几条命令的事;判断错了,删再多文件也不动地方。
网硕互联帮助中心






评论前必须登录!
注册