磁盘告警排查指南:df/du/inode与文件句柄的7大深坑

磁盘告警排查指南:df/du/inode与文件句柄的7大深坑 半夜两点被磁盘告警消息吵醒钉钉群里一张截图某个数据分区 Use% 超过90%要求立即处理。这种告警我这一两年见过不下几十次从一开始慌慌张张执行du -sh /*从根目录一层层往下翻到后来十分钟内锁定根因中间踩的坑确实不少。最让人崩溃的是这几种情况df 显示磁盘满了du 统计出来的目录加起来却差着好几个GB或者明明把大文件删了空间却一点没回来再或者 df -h 看着还有余量应用却直接报 No space left on device。如果搞不清楚 du、df、inode 以及文件句柄之间的微妙关系磁盘告警能让你白折腾一整晚。这里先把结论放前面磁盘空间告警不等于“删文件”这么简单我整理了 7 类最容易让运维翻车的高频深坑每一类都会讲清楚现象、原理、排查命令和处置建议。文中的命令我都在自己的测试机和生产环境验证过你可以放心照着抄。1. 磁盘告警后的第一件事先分清是哪一种“满”1.1 先跑两条命令df -h 和 df -i很多人收到磁盘告警第一反应就是df -h看一下使用率然后直接开始找大目录。但这里我建议把df -i也一起跑掉两条命令输出的信息合在一起才能判断问题到底是块空间不足还是 inode 耗尽。df -h df -i解释一下这两者的区别。df -h看的是文件系统数据块的分配情况它反映的是“容量还剩多少”而df -i看的是 inode 的使用情况inode 可以理解为文件的“户口本”每个文件或目录都要占用一个。正常情况下 inode 的数量远大于文件数量不会成为瓶颈但一旦某个目录下面堆积了海量小文件就会出现数据块还没用完、inode 已经先耗尽的状况。这时候df -h可能显示还有几十GB但应用写文件仍然报错错误信息同样是 No space left on device非常迷惑。如果只看容量不看 inode你会在“磁盘明明没满”的假象里绕很久或者反过来在“明明删了一堆文件、容量却不释放”的困惑里白费力气。1.2 四种告警类型的快速判断把两条命令的输出拼起来看基本能判断出问题属于哪一类这里我列一个自己常用的对照表现象可能原因下一步方向df -h 满df -i 正常块空间不足或有大文件被删未释放du 逐层定位或 lsof 查已删文件df -h 正常df -i 满inode 耗尽小文件数量爆炸find 统计文件数量定位小文件目录df -h 满df -i 也满双重压力通常 by 小文件 大文件同时堆积先处理 inode再处理容量df -h 正常df -i 正常仍写不进只读挂载、文件系统错误、配额限制mount、dmesg、quota 检查很多监控系统只会画出“磁盘使用率”这一条曲线不会区分块空间和 inode。所以我处理告警的第一件事永远是把这两条命令的输出记录下来和监控数据做一次比对。顺序对了后面才不会跑偏。2. 深坑一df 满了 du 却没满到底谁在说谎2.1 为什么 df 和 du 会差出几个 GB这是磁盘告警里最经典也最容易误导人的一个坑。df 显示一个分区使用了 95%你用du -sh /或du -x -h --max-depth1 /把整个目录树统计一遍发现加起来只有 60%怎么都对不上。问题出在统计口径上。df 读取的是文件系统层面的块分配信息它只关心哪些块已经被标记为“已分配”du 则是从目录项出发沿着文件树逐个计算文件大小。正常情况下两者应该接近差出几个GB也不奇怪但如果差距达到了几十GB大概率是有文件已经被 unlink从目录项里删除却仍然被某个进程以文件句柄的方式持有。这个场景在 Linux 里非常常见应用程序打开了一个很大的日志文件运维或者清理脚本执行了rm /var/log/app.log但是 app 进程一直没有重启日志文件句柄还开着。从文件系统的角度看这个 inode 仍然“活着”它占用的数据块没有释放所以 df 认为空间就是被占着但从目录项的角度看文件已经不存在了du 从根目录遍历时根本看不到它自然也不会统计。最终结果就是 df 满、du 不满你删什么都不见效果。2.2 用 lsof 找出“已经删除但还开着”的文件定位这种问题的标准命令是 lsof重点是列出 link count 小于 1 的文件也就是已经被删除但仍然被进程打开的文件lsof L1这个输出在文件数量很大的服务器上可能会非常多尤其是一些长时间运行的 Java 应用或数据库进程可能同时持有几十上百个已删除文件句柄。我一般会加一层过滤只关心超过 1MB 的已删文件并按大小排个序lsof L1 2/dev/null | awk $7 1048576 {print $1, $2, $7, $NF} | sort -k3 -n这里$7是文件大小$NF是文件名通常会显示为path (deleted)。执行完就能看到是哪个进程、哪个文件在占空间。多数情况下罪魁祸首是应用日志、临时文件或者数据库的 binlog 之类。拿到进程 PID 后再用下面的命令确认一下这个进程还在干什么ps -p PID -o pid,ppid,etime,cmd确认进程身份之后再做下一步动作不要急着 kill。2.3 让空间真正释放的操作顺序如果确认占空间的进程是日志服务、应用服务这类可以重启的进程最直接的办法是重启它重启后文件句柄自动释放空间就回来了systemctl restart app如果进程不能随便重启比如数据库、消息队列重启代价很大并且你确认那个已删除的文件是可以丢弃的日志还有一种相对温和的清理方式。先找到它在/proc/PID/fd/下的描述符编号ls -l /proc/PID/fd/ | grep deleted假设日志文件的 fd 编号是 12可以这样把它直接清空: /proc/PID/fd/12注意执行: /proc/PID/fd/12之前一定要确认这个 fd 指向的是日志或临时文件。如果是数据库正在写的数据文件、索引文件或者其他业务关键数据清空操作等于把数据直接干掉后果比磁盘告警严重得多。除非你百分百确认文件可以重建否则不要走这条路。处理完不要立刻下结论等一两分钟再执行df -h确认空间确实回到预期值。如果释放完空间仍然不够用再继续下一类排查。3. 深坑二inode 耗尽df -h 正常却写不进文件3.1 inode 到底是什么先讲一个生活类比。你可以把文件系统想象成一个停车场数据块就是停车位而 inode 是每个车位的“登记牌”。当系统存储了大量小文件每个文件都要占一个 inode哪怕文件内容只有 1 字节。一旦登记牌发完了后面的车再有空位也停不进去。反映到应用层面就是明明磁盘还有空闲容量但任何新建文件、新建目录的操作都会失败。inode 中保存的是文件的元数据包括文件类型、权限、属主、时间戳、硬链接计数以及指向数据块位置的指针。ls -l看到的大部分属性都来自 inode而不是文件内容本身。文件系统在格式化时会根据容量和块大小预先分配一批 inodeext4 通常每 16KB 空间分配一个 inode但如果实际使用场景以海量小文件为主这个默认比例很容易被打穿。3.2 快速定位 inode 消耗大户先确认 inode 确实满了df -i如果看到某个挂载点的 IUse% 达到 100%接下来要找出是哪个目录“长”出了海量小文件。我常用的方法是按顶层目录统计 inode 数量find / -xdev -type f | cut -d/ -f2 | sort | uniq -c | sort -rn-xdev很重要它让 find 只在当前文件系统内统计避免跨挂载点把其他分区的文件算进来。输出的第一列是文件数量第二列是顶层目录名。看到数量异常的那个再往下钻取find /var -xdev -type d -size 100M -exec ls -ld {} \; 2/dev/null这里的思路是通过目录大小定位小文件密集的目录不过更实用的还是直接按目录递归统计文件数量比如for d in /var /tmp /home /usr /data; do echo $d: $(find $d -xdev -type f 2/dev/null | wc -l) files; done实战中 inode 耗尽的高发场景很固定/tmp下垃圾会话文件堆积、邮件队列/var/spool/clientmqueue堵塞、PHP 或 Java 应用的 session 临时文件没清理、容器内频繁产生的小缓存文件。顺着这些目录优先查命中率很高。3.3 清理策略和预防手段找到小文件密集目录后清理命令按时间条件走避免一口气全删find /var/spool/something -type f -mtime 7 -delete find /tmp -type f -mtime 3 -delete如果要删除的文件数量是百万级的find -delete可能比较慢可以先统计数量再批量删也可以用rsync的--delete配合空目录来做“镜像删除”但后者更考验细节普通场景不建议。预防 inode 耗尽比清理更重要的几点一是给日志和临时目录配置轮转logrotate 或定时任务二是尽量把大量小文件落到 xfs 文件系统而不是 ext4 上xfs 的 inode 是动态分配的默认不容易出现“inode 满了但空间还有”的窘境三是对业务产生的临时文件设置定期清理任务不要依赖手工。注意ext4 的 inode 数量在格式化时就已经确定在线扩容文件系统大小时也不会自动等比增加 inode。如果在 ext4 上已经频繁遇到 inode 耗尽要么删除大量小文件要么在迁移数据后重新用mkfs.ext4 -i的合适参数格式化。4. 深坑三已删文件不释放kill 进程不是唯一解法4.1 文件删了空间为什么不回来这个坑在很多新人那里一踩一个准看到磁盘满了执行rm -rf删掉一个大文件再df -h发现空间居然一点没变。紧跟着就会得出“rm 无效”的错误判断。真实情况不是 rm 无效而是删除动作只做了“拆目录项”这一步。Linux 的 unlink 机制是这样的一个文件在磁盘上同时存在两个层面的引用一个是目录项dentry一个是 inode 引用计数。rm会把目录项移除inode 引用计数减一。如果这个引用计数还有进程通过文件描述符持有那么 inode 本身不会被回收数据块也继续处于分配状态。只有引用计数归零文件占用的空间才会被真正放回空闲块池。这也是为什么“已删文件不释放”几乎总是和“进程没有重启”绑定在一起。删文件本身没错错的是文件被某个活着的进程一直咬着不放。4.2 杀进程还是重启服务先算代价找到占用已删文件句柄的进程之后常见的做法是kill -9但我的建议是别急着动手。先把代价算清楚进程类型推荐操作风险说明普通无状态应用systemctl restart app风险低日志服务、打印服务重启或清空 fd有短暂中断窗口数据库MySQL/PostgreSQL禁止直接 kill优先清空 fd 或规划维护窗口误操作会丢数据消息队列、任务调度评估重启窗口尽量停机处理可能中断消费对于数据库这类进程如果被删的文件实际上是数据库数据文件或 WAL/binlog直接 kill 或者清空 fd 都可能造成实例不可用甚至数据丢失。正确的做法是先确认这个文件确实没用或者马上执行恢复操作再考虑清理。磁盘告警再紧急也不能拿数据完整性去赌。4.3 生产环境里的优雅处理方式如果是一个可以丢弃的日志文件被应用的 fd 持有而且应用不能重启我通常用下面这套流程第一步找到 PID 和被删文件的 fd 号lsof L1 | grep deleted ls -l /proc/PID/fd/ | grep deleted第二步确认 fd 指向的文件可以清空后执行: /proc/PID/fd/文件描述符编号执行后 fd 仍然有效进程可以继续向文件写入但文件大小已经变成 0原来占用的数据块全部释放。这种方式的优点是不需要中断业务缺点是清空后那个文件就真的“没历史”了如果后面需要排查日志内容只能从执行时间点之后重新积累。清空之后等一会儿再看df -h。如果空间还没回来检查一下是不是多个进程同时持有了同一个已删文件的句柄这种情况在 fork 出来的子进程里很常见必须把所有持有句柄的进程都处理掉。注意不要在未确认文件用途时对所有 deleted 文件统一执行清空操作。有的程序会把 pid 文件、socket 文件临时 unlink 后重建这类 fd 并不占多少空间清空反而可能干扰业务逻辑。只处理明显是日志或大文件的那几个。5. 深坑四挂载点掩盖让你在错误的方向上白忙5.1 挂载点掩盖是怎么发生的还有一种情况df 显示某个分区使用率很高你在对应目录下跑du -sh /*时进入一个子目录后看到的容量却来自另一个文件系统。这个现象叫挂载点掩盖。展开说就是Linux 允许把一个分区挂载到任意一个空目录上这个目录就会成为挂载点。假设你的根分区是/容量 50G使用率 90%同时/var/lib/docker独立挂载了一块 500G 的数据盘使用率只有 1%。如果你执行du -sh /var/lib/docker它统计的是 500G 数据盘上的文件不是根分区上的文件数字可能很小但如果你对/var做du -sh /var而/var/lib/docker这个挂载点下面数据很多它也会被一并统计进来导致你以为问题出在/var折腾半天才发现数据根本不在根分区。反过来还有一种更隐蔽的情况一个目录本来存放了重要数据后来因为某个误操作把一个新分区挂载到了这个目录上旧文件并没有消失只是被“盖住”了df 和 du 都看不到它们。这种场景常见于容器挂载卷、临时挂载盘等处理时一定要谨慎避免误格式化。5.2 用 findmnt 看真实的挂载布局排查磁盘容量前我建议先看挂载树而不是直接照着目录名往下找。findmnt比mount输出更清晰findmnt -R /它会以树形格式展示所有挂载点之间的父子关系一眼就能看出哪个目录是独立分区。如果只想确认某一个目录findmnt /var/lib/docker输出会显示这个目录挂载的是哪个设备挂载参数是什么。配合df -hT 目录路径可以快速知道这个目录消耗的是哪个分区的容量。5.3 排查时千万别被目录名带偏把挂载布局理清之后再使用 du 时建议养成加-x参数的习惯让统计范围锁定在当前文件系统du -x -h --max-depth1 / 2/dev/null | sort -rh | head -20-x参数的含义是跳过其他文件系统的子目录这样即使/data/mysql下面挂了一块很大的独立盘也不会干扰你对根分区的判断。出现告警时我自己的排查顺序是先df -h确认是哪个挂载点再findmnt看清它下面有没有子挂载最后才用 du 带着-x去定位目录。这个过程看起来多了一步却能省掉后面几次无用功。6. 深坑五稀疏文件和保留块也在偷走容量6.1 稀疏文件ls 和 du 的“数字幻觉”稀疏文件不是一个故障但它会制造一种数字上的矛盾让磁盘排查变得很拧巴。所谓稀疏文件是指文件逻辑上很大但实际只占用了少量数据块的文件。最典型的就是虚拟机的虚拟磁盘镜像qcow2、数据库预分配但尚未填充的文件、BT 下载的占位文件等。判断方法很简单把同一个文件的逻辑大小和实际占用大小对比一下du -h --apparent-size 文件路径 du -h 文件路径第一行是逻辑大小apparent size类似ls -l看到的文件长度第二行是实际占领磁盘的块大小。如果两个数字差着几个数量级就是稀疏文件。稀疏文件对排查的干扰在于如果你用du去扫描目录看到超大文件会以为找到了元凶但df的使用率却一直没怎么涨说明这个文件实际上没有吃掉多少空间相反如果盲目复制或压缩稀疏文件复制出来的目标文件可能变成全量数据瞬间把磁盘占满。处理稀疏文件要么直接用工具如cp --sparsealways保留稀疏属性要么明确判断之后不做处理。6.2 ext4 保留块默认被预占的那 5%ext4 文件系统默认会预留 5% 的数据块给 root 用户这是文件系统层面的一个保护机制。目的是当普通用户写满磁盘时系统管理员和系统进程仍然有足够的空间写入关键日志、执行恢复操作避免系统因为“连一条日志都写不进去”而彻底瘫痪。不过 5% 这个比例对现代大容量磁盘来说非常“贵”。一块 2TB 的数据盘5% 就是 100GB。你用df -h看一块 ext4 数据盘时Size 和 Used、Avail 三个数字加起来往往不等于总容量中间差出的那一截就是这个保留块。如果你创建的是专门存大文件的数据分区5% 的保留确实有点浪费。查看保留块实际大小可以用tune2fs -l /dev/sdX | grep -E Block count|Reserved block count|Block size用“保留块数量”乘以“块大小”就是文件系统被预占的空间。比如 Reserved block count 是 300000Block size 是 4096那保留空间就是300000 * 4096 / 1024 / 1024 1171MB左右。6.3 调低保留块比例的正确姿势如果确认某个分区是数据盘、日志盘不需要那么高的保留比例可以用 tune2fs 调整tune2fs -m 1 /dev/sdX-m后面的数字是百分比-m 1表示把保留比例从 5% 降到 1%。要注意的是这个操作只对 ext2/ext3/ext4 文件系统有效xfs 没有完全对等的“保留块”概念。系统盘和根分区不建议把保留比例调得太低尤其当/var/log也位于根分区时保留空间是关键时刻的最后一道缓冲调低之后你就要为“日志写满导致系统登录不了”的局面负责。注意tune2fs -m调整保留块不需要卸载文件系统但为了保险我还是建议先在非生产环境验证。调整过程中如果出现断电等异常理论上文件系统仍有变挂的风险数据盘无事系统盘建议谨慎。7. 深坑六日志、临时文件与 Docker 层堆积7.1 日志和临时文件的隐藏大户磁盘使用率在没有任何大文件活动的情况下缓慢爬升这种“温水煮青蛙”式的告警通常是日志或临时文件堆积。我见过最多的几个位置/var/log/journaljournald 默认限制是文件系统容量的 10%根分区 100G 的情况下它可以合法占掉 10G。/var/log/下各种应用日志如果没有配置 logrotate单条日志文件膨胀到几十 GB 很常见。/tmp和/var/tmp很多程序生成的临时文件不会自行删除时间一长数量惊人。core dump 文件程序崩溃产生的核心转储单个文件可能等于进程内存大小几个 GB 到几十 GB 都有可能。/var/spool/mqueue系统邮件队列堵塞时这里会堆积大量小文件同时消耗 inode 和块空间。7.2 Docker 的 overlay2 目录为什么那么占空间如果你机器上跑了 Docker磁盘告警时千万记得去看/var/lib/docker。在 overlay2 存储驱动下镜像层、容器可写层、容器日志都堆在这里。先用 Docker 自带的分析命令docker system df它会显示镜像、容器、本地卷、构建缓存分别占了多少空间。如果镜像和卷都不是大头再用du看具体位置du -sh /var/lib/docker/* du -sh /var/lib/docker/overlay2/*容器日志的位置在/var/lib/docker/containers/容器ID/下日志文件名一般是容器ID-json.log。很多默认配置下容器日志没有大小上限一个长期运行的容器日志涨到几十 GB 太常见了。解决办法是在启动容器时加日志轮转参数docker run -d --log-opt max-size50m --log-opt max-file3 ...更彻底的做法是修改 Docker 守护进程配置在/etc/docker/daemon.json中加入{ log-driver: json-file, log-opts: { max-size: 50m, max-file: 3 } }然后重启 Docker 让配置生效。这个配置能阻止以后继续产生巨型日志但已经存在的巨型日志文件还是要手动清理的。7.3 日志轮转和定期清理的建议日志文件的规范管理我认为比“出了问题再清理”重要得多。给日志目录配置 logrotate 是最基本的操作下面是一个 nginx 日志的配置示例保存到/etc/logrotate.d/nginx/var/log/nginx/*.log { daily rotate 7 missingok notifempty compress delaycompress dateext copytruncate }journald 的占用也要在配置里限一下修改/etc/systemd/journald.confSystemMaxUse500M然后重启 journaldsystemctl restart systemd-journald注意执行docker system prune时它会删除所有未被使用的镜像、停止的容器、悬空构建缓存。如果你的同事有依赖旧镜像回滚的习惯清理前最好先确认一下。更安全的方式是docker system df先看再单独执行docker image prune -f清理悬空镜像避免误删。8. 深坑七快照和备份造成的“假告警”8.1 LVM 快照在 df 里看不到却真实占用空间我处理过一起非常迷惑的告警根分区 df 显示使用率从 50% 一路涨到 90%但进去查看目录总量一切都很正常。后来才发现问题根本不在文件系统内部而是 LVM 快照把卷组的空间吃掉了。LVM 快照的工作原理是当宿主逻辑卷LV有数据变更时快照会把变更前的原始数据复制到快照预留区保证快照里保留的是历史状态。这意味着快照本身会随着数据的持续写入不断增长最终占满卷组的空闲空间。而 df 看的是文件系统块根本不会把这个增长反映出来你只会看到卷组 VFree 越来越少甚至宿主 LV 将来扩容时无空间可用。排查命令vgs lvs重点看快照 LV 的 Data% 是否接近 100%。确认快照没用了就直接删掉lvremove 卷组名/快照名删除快照可以快速释放一大块卷组空间但这个操作无法回滚删除前一定要确认这个快照不是恢复数据的最后希望。8.2 备份工具的隐蔽缓存和挂载点和快照类似的还有备份工具的“临时文件”。不少备份软件会把数据先写入文件系统上的一个隐藏目录或挂载点备份完成后再上传到远端如果中途失败或进程被杀临时文件就一直留在原地。这些隐藏目录一般不显眼名字可能是.snapshot、.backup-cache、.Trash-1000之类。du -sh /时不会默认把这些目录列进*通配里需要显式检查ls -la / du -sh .snapshot 2/dev/null du -sh .Trash-* 2/dev/null还有一种情况是备份系统直接挂载了一个外部存储在某个目录下挂载后该目录的旧数据被掩盖备份数据持续增长时你以为占的是当前分区实际上全部算到了挂载源的存储上。8.3 快照清理的实战复盘我之前有个 2TB 数据库卷组某天监控显示某个逻辑卷的使用率没有明显变化但整机 df 开始告警排查进程、删除临时文件都没用。最后是执行vgs才发现卷组 VFree 从 800GB 掉到了不到 50GB而lvs里一个昨天做备份前自动创建的快照已经膨胀到近 700GB。删掉快照的瞬间卷组空间就回来了。从那之后我给自己定了一个规矩磁盘告警如果常规排查五分钟没有结论就顺手跑一遍vgs lvs快照占用必须作为一个固定检查项写进排查清单。这比在错误目录里翻半天文件高效得多。9. 把排查做成习惯命令组合与速查表9.1 我的日常排查三件套以前我收到告警会敲一堆命令慢慢试。现在基本固定成一套命令组合五分钟内能完成第一轮定位。第一步看容量和 inode确定是大块空间问题还是小文件问题df -hT df -i第二步从根开始按文件系统边界统计目录大小排除挂载点干扰du -x -h --max-depth1 / 2/dev/null | sort -rh | head -20第三步查已删除但还被进程占用的文件lsof L1 2/dev/null | awk $7 1048576 {print $1, $2, $7, $NF} | sort -k3 -n这套组合跑完大部分告警都能定位到根因。如果结果依然模糊再检查vgs/lvs快照、journald 日志、Docker 目录。9.2 磁盘告警排查速查表把整个排查过程压缩成一张表几乎可以用来“抄作业”现象可能原因第一排查命令df -h 使用率高du 目录总和小已删文件被进程持有lsof L1df -h 正常写入仍报 No spaceinode 耗尽df -i目录 du 很大但 df 使用率低该目录有独立挂载点findmnt 目录单个文件 ls 和 du 差异巨大稀疏文件du -h --apparent-size 文件使用率缓慢爬升日志、容器日志、core dumpjournalctl --disk-usage所有目录正常df 仍告警LVM 快照或备份缓存占用vgs lvs删除文件后空间不释放进程句柄未释放lsof L1 | grep deleted这张表只是快速入口真实环境里可能多个因素叠加存在先按表定位其中一个处理完以后重新统计再决定是否继续。9.3 最后说一条个人经验磁盘告警处理多了最深的体会有两条。一是平时要积累基线数据。我给自己负责的机器准备了一个简单的巡检脚本每周记录一次各分区 df -h、df -i、top 10 目录、journald 占用这些指标的基线。告警出现时对照基线哪个数字异常得特别明显根因就藏在哪里排查效率完全不一样。二是每次处理完告警我都会把“操作了什么、空间回来多少、根因是什么”记到故障复盘文档里。刚开始觉得麻烦后来发现不同机器的告警原因翻来覆去就是那几类有了一份自己的案例库下次再遇到相似问题几分钟就能给出结论。希望这篇内容能让你少走几步弯路。磁盘告警不可怕可怕的是方向错了还一直往前冲。