磁盘空间告警后的7个深坑:du和df对不上、inode耗尽、已删文件不释放怎么办
磁盘空间告警这种事,每个运维和开发都遇到过。告警邮件弹出来的时候,你手头可能正在开会、写代码、或者半夜被电话叫醒。赶紧登上服务器敲一遍df -h,看起来空间确实快满了;但等你顺着目录一路执行du -h --max-depth=1准备找出元凶的时候,三层目录都没看完,数据却对不上——明明du统计出来的总量远小于df展示的已用空间。这种“怎么算都少了一块”的感觉,真的很折磨人。
更麻烦的是,空间满了之后还不一定马上能定位到问题。inode耗尽、被进程占用的已删文件、日志轮转失效、分区预留空间……这些坑不会同时出现,但每次总有一两个会冒出来。我这些年处理过的磁盘告警,少说也有几十次,踩过的坑基本都齐了。今天就把七个最典型、最容易让人卡壳的问题一次性讲透,顺便把每个问题的排查思路和常用命令整理出来,希望能帮你下次少走弯路。
1. 深坑一:du和df对不上,到底该信谁
du和df是最常用的两个磁盘统计命令,但很多人在告警后第一反应就是拿它们互相验证。第一个坑往往就是这么出现的——两个数字相差巨大,甚至一个显示空间告急,另一个看起来还挺宽敞,于是整个人都懵了。
1.1 统计维度完全不同
df统计的是文件系统的块设备使用情况,它直接读取文件系统的元数据,统计的是“文件系统视角”的块占用。也就是说,一个文件只要分配了块,不管它是普通文件、目录、还是已经被删除但依然被进程占用的残留块,df都会把它算进去。
du则是从目录树的文件项开始遍历,逐个文件统计字节数,再把目录下的所有文件加起来。它统计的是“文件视角”的实际数据大小。
用生活类比一下:df等于你在小区门口数车位,只要画了线、占了位置就算数;du等于你挨家挨户数每一辆车实际长度。如果一个车位里停着一辆货车但只按引擎盖大小收费,两边数字自然就对不上。同理,文件系统里有大量块被分配但又不属于任何文件时,du是数不到的,df则会照实统计。
1.2 排查思路和实操
遇到du和df结果不一致,先别急着怀疑工具坏了。按我习惯的顺序来:
首先,确认是不是文件系统不一致导致的统计滞后。如果用的是 ext4 这类日志文件系统,系统崩溃或者异常断电后,逻辑块组可能处于不一致状态,df读取时会参考超级块里的计数器,这个数字可能还没来得及恢复准确。可以先执行sync,再试一次df。
其次,重点排查是否存在已经删除但仍然被进程占用的文件。这是最常见的元凶。
# 找到被删除但仍有进程打开的文件 lsof | grep -i deleted或者用更高效的方式:
find /proc/*/fd -lname '* (deleted)' 2>/dev/null每出现一行结果,就代表有一个已经被删除但还在占用磁盘块的文件。
还有一个容易忽略的点:df默认显示的“已用”包含了文件系统元数据(inode表、块位图、日志等)的占用,而du完全不统计这些。如果一个文件系统上有大量小文件,inode表本身可能就占掉几百MB甚至几个GB,两边的差额会非常明显。
所以,我的通用判断是:df代表真实容量消耗,du代表可见文件大小。真正占空间但又看不见的部分,才是你排查的重点方向。
2. 深坑二:inode耗尽,明明还有空间却写不进去文件
很多刚接触服务器运维的人第一次遇到磁盘满,第一反应就是删文件。删完一看df -h,可用空间确实还有几十GB,但写新文件的时候照样报No space left on device(设备上没有空间)。这种“明明有空间却什么都写不了”的状态,十有八九就是 inode 耗尽了。
2.1 inode是什么,为什么重要
inode 是文件系统里保存文件元数据的数据结构,文件名、文件大小、权限、时间戳、数据块位置等信息都记录在 inode 里。每个文件或目录至少占用一个 inode。文件系统在格式化的时候,就会按比例分配一部分空间给 inode 表。
对比一下:
| 当前状态 | df -h 显示 | df -i 显示 | 能否写入新文件 |
|---|---|---|---|
| 空间满 | 使用率100% | 使用率正常 | 不能 |
| inode满 | 使用率正常 | 使用率100% | 不能 |
| 双满 | 使用率100% | 使用率100% | 不能 |
排查 inode 是否耗尽,命令很简单:
df -i看/dev/vda1这类挂载点的 IUse% 那一列,如果接近 100%,基本就坐实了。
2.2 快速定位和清理方案
inode 耗尽的本质是有海量小文件堆积。这些文件可能分布在邮件队列、临时目录、缓存目录、或者是某个程序异常写入的日志目录里。
我最常用的定位命令是:
# 统计每个一级目录下的文件数量,按数量排序 for dir in /home /var /tmp /usr /opt; do echo "== $dir ==" find $dir -xdev -type f 2>/dev/null | wc -l done更精准的单目录定位方式:
find / -xdev -type f 2>/dev/null | awk -F '/' '{print $2}' | sort | uniq -c | sort -rn这个命令按根目录下的二级目录统计文件数量,哪类目录的文件特别多,一眼就能看出来。
定位到具体目录后,清理思路分两步:
- 如果能确认文件无用,直接删除。删除时要记得先确认没有进程占用,否则可能踩到下一个坑。
- 如果文件不能删,考虑把数据转移到大容量的独立分区,或者启用文件系统压缩特性(ext4 可以开启 inline_data,把小块数据放进 inode 里)。
这里得特别提醒:inode 耗尽之后,连删除文件这个操作本身都可能失败。因为删除文件也需要在目录里创建/修改条目,如果目录所在的文件系统 inode 已经满了,某些操作会受限。遇到这种情况,最稳妥的办法是先腾出少量 inode,比如删除一个包含大量小文件的临时目录,或者清空某个缓存文件夹,再继续后续清理。
3. 深坑三:删了文件空间却没释放,罪魁祸首是占用进程
这个坑我刚开始排查磁盘问题时也踩过。明明用rm删掉了一个几十GB的大日志文件,再看df -h,可用空间字节都没变化。那一刻真有点怀疑人生。
3.1 删除不等于释放
Linux 和 Windows 的文件删除逻辑不同。在 Linux 下,rm只是把文件从目录项中移除,但如果某个进程仍然持有该文件的文件描述符(file descriptor),这个文件占用的数据块就不会被真正释放。数据块要等所有引用它的描述符都关闭后,才会归还给文件系统。
最常见的是 tail、服务进程的重定向输出、或者某个崩溃后遗留的进程,仍然持有已删除日志文件或临时文件的句柄。
你删掉的只是文件名,真正占空间的还是那个隐藏在进程里的"僵尸文件"。
用个通俗的比喻:某间办公室的电话号码被注销了,但里面的设备还在被一个联系不上的员工占着,物理空间腾不出来。
3.2 查找并处理占用进程
排查命令前面提过了,这里再说细一点。
# 列出所有删除但仍被占用的文件及对应的进程PID lsof +L1这个命令的输出中,SIZE字段会显示文件占用的实际字节数,PID就是占用进程的进程号。如果文件已经被删除,NAME一栏通常会带(deleted)标记。
如果没有 lsof,可以用 proc 文件系统:
ls -la /proc/[0-9]*/fd/* 2>/dev/null | grep deleted这个命令会列出所有进程打开的、但已经删除的文件描述符。结合ls -lia看 inode 号,可以判断具体是哪个文件。
找到进程后怎么处理,取决于业务性质:
- 如果是临时任务产生的进程,直接
kill进程,空间会立刻释放。 - 如果是核心服务,不能随便重启,可以先确认这个进程是否还有存在的必要。比如有些服务处于空转状态,但持有的句柄却不释放,这种情况可以咨询业务方确认后重启服务。
我常见的一种场景是:tail -f某个日志,日志轮转后把旧文件删了,tail进程还开着旧句柄;或者 Java 服务把日志输出重定向到文件,而日志文件被 logrotate 移走后,Java 进程的旧句柄还占着。这类问题重启进程通常立刻见效。
4. 深坑四:排查效率太低,从告警到定位花了几个钟头
其实前面三个坑虽然经典,但如果排查命令用得熟练,每个都只需几分钟就能定位。真正消耗时间的是排查思路混乱,东敲一下命令、西翻一个目录,最后探测半天还停留在猜测阶段。
4.1 先看df再跑du,但别一头扎进全盘扫描
我见过不少同行的第一反应是du -sh /*这种全盘扫描。遇到大目录多的服务器,这个命令跑个十几分钟都正常,磁盘告警本来就紧急,时间根本耗不起。
正确的顺序应该是:
# 第一步:先确认到底哪块文件系统满了 df -h # 第二步:直接进入挂载点根目录,按目录深度逐层下钻 cd /var du -h --max-depth=1 | sort -hdu的参数里,--max-depth=1表示只看当前目录下一级子目录的占用,sort -h按人类可读的大小排序,直接把最大的目录顶到最上面。比起一层一层地ls,效率要高一个量级。
如果文件系统的挂载点在某个目录下,比如/data是单独分出来的分区,那/var下统计出来的数据就不会包含/data里的内容。这时候要特别注意:du默认不会跨越文件系统边界(如果不加-x参数,它还是会统计进去),加了-x后则会忽略其他文件系统挂载点。要区分来看。
4.2 高效定位目录的命令组合
下面这组命令我几乎是刻在肌肉记忆里的:
# 查看当前目录下所有子孙目录(一级)的大小排名 du -h --max-depth=1 /某个挂载点 2>/dev/null | sort -hr | head -20 # 只看当前目录下的文件大小排名 ls -lhS | head -20 # 一步到位,找单个超过N大小的文件 find /某个挂载点 -xdev -type f -size +1G 2>/dev/null -exec ls -lh {} \; | sort -k5 -h最后一条find命令的威力在于:不用一层层点进去翻,直接筛选出大于1GB甚至10GB的大文件,往往几秒钟就能定位到罪魁祸首。
为了高效运维,顺手给磁盘使用率设置个监控告警阈值也很有必要。比如空间超过80%告警一次,超过90%告警一次,超过95%直接升级到紧急事件。inode 使用率超过80%同样要告警,这样能提前处理,而不是等满了再头痛。
5. 深坑五:日志和临时文件无限膨胀,轮转策略形同虚设
大文件的来源,十次有八次是日志和临时文件。尤其是没人打扫的日志目录,一旦积累到GB级别,磁盘告警几乎是必然的。但很多人处理完之后,过两天又开始膨胀,这说明问题的根源不在某个文件,而在日志轮转机制上。
5.1 常见的日志膨胀场景
第一种是应用进程把标准输出写进同一个文件,但 logrotate 只轮换了日志,应用进程依然持有旧日志的句柄(前面那类问题)。这种情况轮转后就白白占用了,旧文件即使被重命名也无法释放。
第二种是应用自己生成了大量调试日志,轮转规则没有针对这些文件名做配置。比如某些 Java 中间件会生成gc.log、dump.log,它们可能按日期生成新的文件,但旧文件没有定期清理。
第三种是临时文件目录里堆积了大量会话文件、上传文件、缓存文件,比如 PHP 的 session 目录、Java 的临时目录、Nginx 的 proxy_temp 目录。一旦程序异常退出,旧的临时文件就成了无主垃圾,永远留在磁盘上。
5.2 如何设计日志策略
一套基本合理的日志轮转配置,以 logrotate 为例,长这样:
/var/log/myapp/*.log { daily rotate 14 compress delaycompress missingok notifempty create 644 app app sharedscripts postrotate /bin/kill -HUP $(cat /var/run/myapp.pid) endscript }几个参数都值得解释一下:
rotate 14:保留14份日志,按天轮转,也就是留有14天的历史。compress+delaycompress:压缩上上个周期的日志,而不是最新那份,这样最新日志还能被程序继续写入。postrotate里的kill -HUP:通知进程重新打开日志文件,这一步是解决"句柄占用不释放"的关键。没有这一步,轮转后旧文件可能还继续增长。sharedscripts:多个日志文件共享一个postrotate脚本,减少重复执行。
对于应用自己生成的日志,比如 Java 程序,可以写一个独立的 logback 或 log4j2 配置,按文件大小和日期双重切割。线上系统最怕日志文件无规律膨胀,所以我的原则是:所有会产生日志或临时文件的目录,都应该有一条明确的清理规则,要么归 logrotate 管,要么归应用内部的滚动机制管。
另外,监控上可以加一条,对日志目录的最高文件数和最新文件年龄做告警,防止"临时文件堆积但没人发现"的情况。
6. 深坑六:分区预留空间和快照悄悄吃掉容量
排查磁盘空间时,还有一种隐蔽情况:df显示文件系统已经使用了 100%,可你顺着目录算了半天,怎么也找不到对应大小的文件。这种时候十有八九是"不可见空间"被占用了。
6.1 分区预留空间
ext 系列文件系统在格式化的时候,会默认预留 5% 的块给 root 用户使用。这个预留空间用于防止文件系统碎片化、以及 root 在磁盘剩满时仍能登录系统进行维护。对大多数分区来说,5% 不算多,但如果分区特别大,比如 100TB 的数据盘,5% 就是 5TB,这个数字相当可观。
查询预留空间大小:
tune2fs -l /dev/vda1 | grep -i reserved如果确认业务盘不需要预留这么多空间,可以调小:
tune2fs -m 1 /dev/vda1这个命令把预留比例从 5% 改成 1%,对容量紧张的数据盘来说是立竿见影的。注意不要设置为 0,否则一旦写满,root 可能都无法登录执行排查。
6.2 快照和逻辑卷备份
另一个吞空间的隐形大户是 LVM 快照。LVM 快照的实现方式是 CoW(写时复制):快照创建时几乎不占空间,但随着原卷数据被修改,快照为了保留旧数据,会逐渐占用新空间。如果运维忘记了某个快照的存在,等快照存储池满的时候,原卷的写入也会被阻塞。
查看逻辑卷和快照:
lvs看LV列,凡是从某个卷衍生出来的快照卷,名字通常带snap字样,而且Data%会持续增长。如果快照已经完成了它的使命(比如备份已经做完),直接移除:
lvremove /dev/vgprod/proddata_snap这里要特别小心:移除快照是不可逆操作,务必确认快照内容不再需要。
有时候不是 LVM 快照,而是云平台的云盘快照,这些快照容量一般不体现在服务器df里,但会体现在账单和云监控里。这种我建议直接在云控制台排查一下,有没有创建时间久远但一直保留的自动快照策略。
7. 深坑七:视觉盲区,挂在“眼前”的容量盲区
有时候磁盘分区本身没满,但你明明看到一个文件系统满了,怎么排查都找不到大文件。这多半是挂载点层级混乱造成的错觉。
7.1 挂载点边界的容量误判
比如你执行df -h,看到/根分区满了,于是到/下面去找。如果此时某个子目录比如/data是一个独立分区,那么/data下的文件是不会计入根分区使用率的。如果你执行du -sh /的时候带了-x参数,它就会自动跳过其他文件系统挂载点,但如果你没有加-x,它会把不同文件系统里的文件全统计进去,导致数字看起来和df不一致。
反过来的误判更常见:某个挂载点下面的内容特别多,但df显示的是挂载点所属分区的用量,你去看挂载点目录时可能只看到本分区的几个文件,却忽略了下面还套着其他挂载点。
可以用mount或者findmnt查看完整挂载树:
findmnt -R /这个命令把根分区及所有子挂载点以树状形式列出来,一眼就能看出哪些目录是独立的文件系统边界。
7.2 快速梳理挂载点容量
结合df -hT和du,我通常按下面的逻辑排查:
- 先用
df -hT看所有文件系统的类型和挂载点。 - 对每个接近满的文件系统,执行
du -h --max-depth=1 -x 挂载点,-x参数确保只统计本文件系统内部的数据。 - 对定位到的大目录,继续逐层下钻。
另外一个替代工具ncdu也值得推荐。它是du的交互式 TUI 版本,可以快速浏览目录占用量、按大小排序、甚至直接删除光标所在文件。没有的话先安装:
apt install ncdu # Debian/Ubuntu yum install ncdu # CentOS/RHEL第一次跑的时候它会扫描整个目录树,之后的操作就像浏览文件管理器一样顺畅。对于大目录探底,效率比纯命令行高不少。
8. 故障处理完成之后,还有一件收尾的事
空间清理完,磁盘告警解除,不代表这件事就到此结束。如果只是"删文件—看 df—确认没满",大概率过一段时间又会重复告警。我习惯再把每个坑对应的职责梳理一遍,把运维变成可维护的流程。
以软件项目作类比:磁盘故障就像"只改 bug 不改设计",问题当然会复现。能彻底解决问题的方式,是把"容量管理"和"日志策略管理"作为系统日常的一部分,而不仅是出告警时的被动反应。
具体可以做三件事:
- 把磁盘使用率、inode 使用率、分区剩余 inode 数、日志目录大小做成常规监控项,配合告警阈值,提前发现潜在风险。
- 为常用目录建立 crontab 定时清理任务。比如临时目录超过7天就清空,日志目录超过30天就压缩并删除旧档。
- 定期检查测试环境或非核心业务的挂载快照、预留空间、无用挂载点,尽早发现异常消耗。
对于真的没法通过清理短时间释放容量的路径,比如数据持续增长的数据盘,需要提前规划扩容方案。扩容方式取决于存储架构——传统分区支持在线扩容,LVM 可以加 PV 再扩展 LV,云盘通常也会支持快照扩容。关键是要在告警之前把扩容流程跑通,否则真到容量打满的时候,业务中断时间就变成不可控的了。
我在实际处理中最大的体会是:磁盘告警的难点从来都不在"删除文件"这一个动作上,而是在于如何快速判断"谁在占空间、为什么占、怎么预防它再占"。把上面这七个坑在心里过一遍,遇到告警就不慌,思路顺下来二三十分钟肯定能定位到根因。下次再看到 du 和 df 打架,记住第一条——先检查被删除但未释放的文件,这一条就帮你在 80% 的场景里少走很多弯路。