Linux磁盘“幽灵空间”:df和du不一致的排查与实战

Linux磁盘“幽灵空间”:df和du不一致的排查与实战 先讲个真实场景半夜收到服务器磁盘告警爬起来敲了一句df -h看到/data使用率已经 98%心凉了半截。再敲一句du -sh /data/结果只显示用了 40%。磁盘上凭空多出的那 58% “幽灵空间”相信每个 Linux 运维都经历过。df说满了du说没满到底谁在说谎这篇博客就聊聊我排查这类问题的完整思路和几条硬经验。内容不深适合刚接手服务器的新人也适合经常被磁盘告警折腾的同行参考。1. 为什么 df 和 du 会对不上账1.1 先搞清楚 df 和 du 到底在看什么很多新手一上来就喜欢分个对错觉得必然是其中一个统计错了。实际上两个工具都没错只是它们看问题的视角完全不同。df看的是文件系统层面它直接读取文件系统的超级块信息统计的是整个文件系统上所有的块分配情况。可以理解为一份“卷级账本”记录的是这张磁盘上总共有多少块、已分配多少块、还剩多少块。凡是文件系统认为已经占用掉的块都会被算进去。du看的是目录树层面它从指定的路径开始递归遍历所有可见文件和目录把每个文件实际占用的块数累加在一起。可以理解为一个“文件级量尺”只关心通过目录树能看到的文件。这两个工具统计口径不同结果自然可能出现偏差。我整理了一张对比表平时给团队新人讲这个问题时也常用工具统计对象数据来源能否看到被删除但仍被占用的文件统计速度df整个文件系统的块分配情况文件系统超级块superblock能只要文件系统还没释放这些块就算“已用”秒级du指定目录下的可见文件逐个调用 stat() 累加不能它根本看不到已经被删除的目录项取决于文件数量可能很慢理解了这个底层差异后面所有排查就有了方向当df和du出现明显偏差时大概率是存在“文件系统已记账但目录树已不可见”的空间。1.2 最常见的元凶已被删除但仍被进程占用的文件这是我在生产环境遇到最多的情况也是“幽灵空间”的头号来源。Linux 删除文件的操作本质上是删除这个文件的目录项dentry并减少 inode 上的链接计数。如果这时候有进程已经打开了这个文件inode 并不会立刻被回收文件数据块也不会真正释放回文件系统。只有持有该文件描述符fd的进程关闭它或者退出内核才会完成最后的释放。也就是说你rm掉文件之后文件系统的账本上记录的还是“这些块已被占用”而目录树里已经找不到这个文件了。这时候df依旧显示已满du却怎么都统计不到这部分空间两者之间就出现了差值。定位方法很简单lsof -nP | grep deleted-n表示不解析主机名-P表示不解析端口名这两个选项能明显加快输出速度。grep deleted会在输出里筛出状态为“已删除”的文件。输出结果里COMMAND列是进程名PID列是进程号NAME列是文件的原始路径。看到这些信息你就能判断是哪个进程在占用已经删掉的文件。注意看到这类文件后不要条件反射地直接kill进程。如果是数据库、业务应用等关键进程强行结束可能造成更严重的事故。正确做法是先确认进程职责再选择合适的窗口用重启服务、kill -HUP或平滑重载等方式释放。1.3 不止文件句柄预留块和元数据也会造成偏差除了“已删除但仍被占用”的文件还有两个日常容易忽略的统计口径差异。第一个是文件系统的预留块。ext4和ext3文件系统在创建时会默认预留一部分空间给 root 用户避免文件系统满了之后管理员连修复的余地都没有。默认预留比例是 5%但部分发行版或定制环境可能不同。用下面的命令可以查看tune2fs -l /dev/sda1 | grep -i reserved你会看到Reserved block count和Reserved block percent两个字段。这部分空间在内核的记账逻辑里属于“已占用”但du永远统计不到因为它是文件系统元数据层面的开销。第二个是文件系统自身的元数据开销包括 inode 表、块位图、inode 位图、日志journal区域等。df显示的使用量中已经包含这些而du只统计文件内容。尤其是创建了大量小文件的场景inode 表可能吃掉不少空间这时候du和df的对不上就更加明显。2. 还有哪些“看不见”的空间被悄悄吃掉2.1 挂载点叠加同一个路径下的两副面孔除了文件句柄挂载点叠加也是制造幽灵空间的老手只是它出现的频率没有前者高。举一个我实际遇到的问题。某台机器挂载了一个独立的/var/log分区但/var/log目录下面又被某个程序创建了一个nginx子目录里面写满了日志。这时候从根分区来看du -sh /var/log/nginx统计不到因为/var/log是一个独立的挂载点du默认会进入这个挂载点遍历如果用了-x参数则会直接跳过。更隐蔽的情况是“掩埋挂载点”。假设/opt/app下面挂载了磁盘 A但系统启动后又有人把磁盘 B 挂载到了同一个/opt/app路径上原来的磁盘 A 就被“遮住”了。此时du遍历/opt/app看到的只是磁盘 B 的内容磁盘 A 上的数据从目录树里完全访问不到但df仍然能看到磁盘 A 的挂载信息和使用率。排查这类问题可以用findmnt -R /查看整个挂载树或者用mount | grep -E /(opt|var|home)按目录巡检。养成习惯当磁盘告警时先确认告警分区上有没有嵌套挂载点。2.2 稀疏文件和预分配文件稀疏文件是另一个容易让人误判“幽灵空间”的源头尤其是跑虚拟化和数据库的机器上。稀疏文件sparse file的特点是文件逻辑大小很大但实际占用的磁盘块很少。你ls -lh看到的文件大小和du -sh看到的实际占用可能差很多。这本身不算问题但如果某天磁盘满了你顺着du找出了一个逻辑大小几十 GB 的文件想当然地删掉却发现df只释放了一点点空间就会觉得“空间没被还回来”。再有一种是预分配文件。很多数据库或日志系统会提前用fallocate或posix_fallocate把磁盘空间一次性分配好比如fallocate -l 10G /data/redo.log。这种操作会立刻让df的使用率上升但du统计到的同样只有实际写入的块。这个文件在文件系统看来是“已占用”的删除之后空间自然会被释放但如果不删它就一直占着。区分这两类文件有个小技巧ls -lh看逻辑大小du -h看实际占用两者差值越大越可能是稀疏文件或未完全写入的预分配文件。2.3 文件系统快照与延迟记账如果你用了 LVM 快照、ZFS 快照或 btrfs 快照那么df和du的账会变得更加复杂。以 LVM 快照为例快照本身会占用空间而且随着源卷数据不断变化快照占用的空间会持续增长。但du遍历源卷目录时只会看到当前活跃的数据快照里保存的旧数据块根本不体现在目录树中。如果快照接近容量上限系统可能会报“卷空间不足”之类的错误而du显示的数据量其实不大。ZFS 和 btrfs 的快照机制也类似它们都是通过写时复制COW实现的旧数据块因为被快照引用而无法释放。你在清理数据时df的使用率下降速度比预期的慢很可能就是有快照在“扣留”旧数据块。排查方向比较明确检查是否有残留快照比如lvdisplay查看 LVM 快照状态或者zfs list -t snapshot、btrfs subvolume list查看对应系统的快照列表确认是否需要保留。2.4 inode 耗尽磁盘没满但写不进文件有一种“假满”的情况和df、du的经典偏差不太一样但排查思路上高度关联。当文件系统里的小文件数量过多把 inode 表占满时就会出现“磁盘明明还有空间但创建不了新文件”的现象。这时候df -h显示还有空闲du -sh统计的数据量也不算大但touch一个新文件都会报No space left on device。需要查看另一个指标df -i-i参数让df输出 inode 的使用情况而不是块的使用情况。如果IUse%已经接近 100%那么问题就不是磁盘空间而是 inode 耗尽。这种问题多发生在邮件队列、消息队列、缓存目录这类每分钟可能产生大量小文件的路径。解决思路是清理历史小文件或者把这些路径迁移到文件数量更具扩展性的文件系统上。2.5 容器 overlay2 与宿主机的视角差容器场景里df和du对不上也是一个高频问题特别是 Docker 默认的 overlay2 存储驱动下。容器内的df -h看到的通常是一个融合了镜像层和容器可写层的虚拟文件系统它的统计口径和宿主机目录的du并不一致。镜像中删除或者覆盖文件时原文件可能还残留在镜像下层中只有所有引用该镜像的容器都被停止并清理后对应的空间才会真正释放。常有同事在容器里执行rm删掉了大文件然后跑出来说“我删了但是宿主机磁盘没变化”。这里面有个陷阱rm只影响当前容器的可写层底层镜像层中的文件还在。如果多个容器共享同一个镜像你删掉的只是自己这一层的引用。排查这类问题比较有效的命令是docker system df docker image ls先看清楚镜像和容器占用的空间层级做好容器清理和镜像清理宿主机空间才会真正回来。3. 定位“幽灵空间”的三板斧实操3.1 第一板斧找已经删除但还占着空间的进程排查幽灵空间我永远是先执行lsof相关命令因为“已删除但仍被占用”的概率实在太高。完整命令推荐这样写sudo lsof -nP L1 | awk $7 1048576 {print $1, $2, $7, $10}L1表示只列出 link count 小于 1 的文件也就是已经被删除但仍有进程引用的文件。awk里的$7是文件大小单位是字节我习惯只过滤出超过 1 MB 的文件这样可以快速排除掉一堆小体积的临时文件干扰。如果 lsof 输出为空别急着下结论也可能是权限不够记得加sudo。找到进程后根据进程类型选择释放方式。如果是 Nginx 这类可以热重载的服务执行nginx -s reload就能安全关闭旧的日志文件句柄如果是 Java 应用、数据库这类重型服务需要结合业务窗口重启或优雅滚动重启。3.2 第二板斧用 du 分层定位大目录lsof没找到大文件句柄或者找到了但释放后空间仍不够那就进入du的分层排查阶段。我的习惯命令du -x -h --max-depth1 / 2/dev/null | sort -rh | head -20这里有几个参数值得说明一下。-x表示不要跨文件系统统计这是避免把其他挂载点内容重复计入的关键--max-depth1只显示一层目录的结果方便一层层收缩范围2/dev/null把权限报错丢弃减少干扰输出。如果你du排查根目录本身没发现问题但df依然显示空间紧张那就要回到 2.1 节提到的挂载点叠加问题用findmnt -R /检查是否存在多个挂载点再逐个挂载点单独统计。3.3 第三板斧检查文件系统预留与隐藏挂载如果前两板斧都没查出明显问题就需要看文件系统本身的“保留”项了。检查 ext4 预留空间tune2fs -l /dev/mapper/your-disk | grep -E Block count|Reserved block count|Free blocks如果根分区因为某些原因被设置了很高的预留比例可以临时调整tune2fs -m 1 /dev/mapper/your-disk-m 1把预留比例改为 1%这是应急操作不建议长期执行因为一旦文件系统完全写满普通用户连最基本的修复工具都跑不起来。检查隐藏挂载可以用findmnt -R / lsblk -flsblk -f能一屏看出所有块设备及其挂载点、文件系统类型、剩余空间很多对不上的账拉出这张表基本就清楚了。3.4 不同文件系统的额外校验不同文件系统的记账细节不太一样排查时也得区别对待。xfs没有 ext 系列那种默认 5% 的 root 预留空间但它的元数据设计和块分配策略会让df和du在某些时刻出现短时偏差比如延迟日志delayed logging带来的日志区域占用。用xfs_info /挂载点可以查看基本信息重点看data段的bsize和blocks。btrfs则更复杂它的df输出分为Data、Metadata、System三部分三者都可能单独满。如果有人改了 btrfs 的 chunk 分配策略会出现数据没多少但 data 空间被占满的情况。这种场景下直接看btrfs filesystem df /挂载点比看df -h更准确。4. 实战复盘一次数据库服务器的幽灵空间排查4.1 现场信息有一年我处理过一台 MySQL 数据库服务器的告警现象很典型。值班监控显示/data分区使用率在凌晨达到 91%按惯例应该发文确认清理方案。可当我登录机器执行du -sh /data时计数器只显示了 42%。数据目录里的 file、binlog、slow log 加起来也就 42%那多余的 49% 去哪了磁盘上肯定有什么东西“看不见”了。4.2 排查过程我做的第一件事不是继续用du翻目录而是直接执行sudo lsof -nP L1 | grep /data | head -50输出里很快就浮现了十几条异常记录全是 binlog 文件状态显示deleted但COMMAND列是mysqld。当时的情况是 DBA 手动清理了一部分过期 binlog但 MySQL 主进程还在频繁写入和读取旧文件删除动作并没有立刻生效这些 binlog 的数据块仍然被进程掐在手里。确认了占用进程是数据库本身我立刻联系 DBA 确认是否可以滚动重启实例。最终在业务低峰期执行了平滑重启实例重启完成的那一瞬间监视器上磁盘使用率直接从 91% 掉到了 45%。整个释放过程不到两分钟磁盘上那块“幽灵空间”瞬间消失。4.3 当时的反思与改进这次排查结束之后我复盘了三个值得改进的点。第一当时的监控只关注df没有把du的统计纳入对比导致磁盘告警无法反映出“空间被文件句柄占用”这一维度。后来我们在监控里加了一个脚本定期对比df和du的差值连续两次差值超过 20% 就触发告警。第二清理 binlog 这类会被服务持续引用的文件应该优先考虑官方工具或至少先跟业务确认重启窗口而不是直接rm。直接删除后如果进程不释放句柄空间释放周期完全不可控。第三对数据库这类长期不重启的服务要建立日志轮转和定期重启机制。不一定要每天重启但至少要有预案知道“如果 binlog 删除后空间没释放该在什么时间窗口重启服务”。5. 日常预防让“幽灵空间”不再反复出现5.1 监控层面df、du、inode 三个指标一起盯很多磁盘告警之所以闹到半夜是因为监控只看了df -h这一项。我建议监控至少增加三个维度监控项目命令建议阈值文件系统空间使用率df -h高于 80% 告警inode 使用率df -i高于 80% 告警df 与 du 差值df -h / du -x -h差值超过 20% 告警第三个指标特别有用它可以在“幽灵空间”刚刚出现时就给出提示而不是等到磁盘快要撑爆才报警。5.2 操作层面两个必须要养成的习惯第一个习惯是“删除大文件前先查句柄”。任何一个超过 1GB 的文件在rm之前先执行lsof /path/to/file如果输出为空说明没有进程引用删除后空间立即释放。如果有进程引用就要想清楚释放窗口。第二个习惯是替日志做好轮转。很多删除操作本来可以通过 logrotate 避免比如使用copytruncate选项它会先复制日志内容再清空原文件这样进程持有的文件句柄不会作废服务也不需要重启/path/to/log/*.log { daily rotate 7 copytruncate compress missingok notifempty }这样配置之后日志轮转不再依赖服务重启也大大降低了“删了文件但不释放”的风险。5.3 命令速查表最后整理一张速查表排查时对着操作就行场景命令说明查找已删除但仍被占用的文件sudo lsof -nP L1只看 link count 小于 1按大小过滤已删除文件sudo lsof -nP L1 | awk $7 1048576过滤出 1MB 以上的文件查看 inode 使用率df -i检查是否 inode 耗尽层次化定位大目录du -x -h --max-depth1 / 2/dev/null | sort -rh | head -20不跨文件系统查看预留块比例tune2fs -l /dev/xxx | grep -i reservedext 系列专用查看整个挂载树findmnt -R /找出掩埋挂载点查看块设备对应关系lsblk -f一屏看清设备和挂载XFS 文件系统信息xfs_info /挂载点查看 XFS 的块信息btrfs 文件系统空间btrfs filesystem df /挂载点按 Data/Metadata/System 分维度看我个人的习惯是磁盘空间告警之后第一件事永远先跑一遍lsof -nP L1再做其他排查。因为处理过太多“删文件删到怀疑人生结果发现是句柄没释放”的案例之后你会明白很多对不上的账底层原因其实很简单。区别只在于你是被表象牵着走还是一眼就看到了问题的核心。