ARTICLE DETAIL

资讯详情

深耕郑州网站建设与运营推广的一线实战洞察。

df与du磁盘空间不一致:Linux排查与句柄释放指南

df与du磁盘空间不一致:Linux排查与句柄释放指南 1. 先把两个命令的“视角”掰开说清楚磁盘排查时敲下df -h和du -h --max-depth1结果一个说 80G 满了一个说各个目录加起来才 40G这种“对不上账”的情况几乎每个运维人都遇到过。先把结论放前面这两个命令从设计之初就不是为了给对方做交叉验证的它们读取的数据源、统计单位、遍历规则完全不同天然允许存在差异。真正要做的不是纠结“哪个准”而是看懂差值方向和差值大小顺着线索找到空间到底被谁占着。这篇文章会从内核接口、文件系统结构、到具体复现命令全部讲一遍适合刚接手服务器的新手也适合被“磁盘告警但 du 查不出大头”折磨过的老手。df的数据来自statvfs系统调用它直接把文件系统超级块里维护的全局计数读出来总块数、空闲块数、可用块数。这个数字由内核在分配和释放块时实时更新反映的是文件系统层面的真实水位。du走的是另一条路它从指定目录开始递归遍历对每一个目录项调用lstat取出st_blocks字段再乘以 512 字节最后按目录累加。也就是说du是“数出来的”df是“查出来的”。数出来的过程会受到目录项是否可见、是否跨设备、是否重复计数等一堆因素影响所以两者不等是常态完全相等反而说明环境特别干净。1.1 df 看的是文件系统du 看的是目录树理解差异的第一把钥匙是统计粒度。df -h的每一行对应一个挂载点也就是一个已挂载的文件系统实例它统计的范围就是那块设备或那个逻辑卷、那个网络存储上所有已分配的块。du -h --max-depth1的每一行对应一个具体目录它关心的是这个目录及其子目录里能“看得见”的文件。两者的边界不一样一个以文件系统为界一个以目录路径为界。举个最常见的例子。根分区/是一个文件系统/data是另外挂的一块盘。执行df -h /只统计根分区而du -h --max-depth1 /在默认行为下会顺着目录树一路走进/data把那块盘的占用也算进/data这一行里。结果是du的各项之和大于df根分区的总量看着像“数据凭空多出来”。解决办法很简单给du加-x参数只统计与起始目录同一文件系统的内容两者就回到同一比较维度了。更隐蔽的一种情况正好反过来某个目录原本在根分区上占了很多空间后来运维在它上面挂了一块新盘原来的数据就被“盖住”了。挂载点遮蔽之后新盘上的内容会顶替原目录显示被遮蔽的老数据在du遍历时完全看不到但它的块依然被根分区占着df照实统计。这时df比du大出一截而du无论怎么翻目录都找不到那部分空间。1.2 块、保留块与 apparent size三个不同的“大小”很多人以为“文件大小”是一个值其实在 Linux 里至少有三种口径。第一种是表观大小apparent size就是ls -l列出来的字节数代表如果文件是连续无空洞存储时需要多少字节。第二种是实际占用块数即st_blocks * 512反映文件真正吃掉多少磁盘。第三种是du默认采用的st_blocks口径而du --apparent-size才切换到表观大小口径。df统计的永远是实际分配的块du默认也是实际块所以正常情况下两者在根目录层面应当接近。但一旦用了du --apparent-size或者脚本里混用了不同口径差值就可能来自稀疏文件。稀疏文件内部有大片“空洞”st_blocks很小ls -l看着却有几十 G。数据库预分配文件、虚拟机镜像、容器镜像层经常是这种形态用错口径会让人误判空间去向。还有一个经常被忽略的角色保留块。ext4 默认给 root 保留 5% 的块空间df的总量和已用计算把保留块算作“已分配但普通用户不可用”du则只统计被文件真正占用的部分。一块 1TB 的 ext4光保留块就有约 50G。用tune2fs -l /dev/sdX能看到Reserved block count这就是df与du之间一个天然存在的常量化差值来源。它不是异常是文件系统的有意设计方便 root 在磁盘写满时还能执行关键维护操作。1.3 为什么差值方向能直接指出病因排查时我形成一个习惯动作先看谁比谁大。df明显大于du问题基本锁定在“空间被占用但du看不见”——已删除未释放的文件句柄、挂载遮蔽、隐藏在其他命名空间里的文件。du明显大于df方向相反问题多半出在重复计数或口径错误——跨文件系统遍历、硬链接被多次统计、reflink 共享块被当成独占块、--apparent-size口径混用。方向确定了再往下查就是按图索骥不用一股脑把所有排查命令都跑一遍。差值的量级同样能提供线索。差值恰好等于某块盘的总容量那基本是挂载混淆。差值是一个稳定的小比例比如 5% 左右那很可能是保留块。差值是随业务波动的动态值且删文件后不下降就该怀疑进程句柄泄漏。差值是一个很大的整数 G 且变化不大考虑稀疏文件或预分配文件。提示任何比对之前先确认两个命令统计的是同一层。df -h /对du -h --max-depth1 / -x而不是df -h对du -h --max-depth1 /。2. 五种典型差异场景的原理拆解原理讲完进入具体场景。这些场景我在不同规模的环境里反复遇到覆盖了绝大多数“对不上账”的案例。每个场景都会说明成因、表现、验证方法和处理手段可以当成排查手册直接对着用。2.1 文件删了但进程还攥着句柄这是生产环境里最经典、杀伤力也最大的一种。运维做了日志清理或删除了一个大文件ls看不到du也统计不到可df的已用空间纹丝不动。原因在于 Linux 的文件语义目录项被删除后如果还有进程持有这个文件的打开句柄内核不会立即回收数据块必须等到最后一个引用释放才真正释放。这段时间里df通过超级块仍然看到这些块被占用而du遍历目录树时已经找不到这个文件了。验证方法很直接lsof L1会列出所有 link count 小于 1 的已删除文件find /proc/*/fd -ls 2/dev/null | grep (deleted)也能捞出被进程 pin 住的句柄。输出里会标注进程 PID 和文件描述符号/proc/pid/fd/fd后面跟着(deleted)。找到之后能重启的业务进程直接重启空间立刻释放不能停的老进程可以做一次“原地清空”用: /proc/pid/fd/fd或truncate -s 0把内容抹掉句柄还留着但块被释放服务不中断。注意这一步对某些以追加模式写日志的程序会出现日志空洞重启前评估清楚。要特别警惕一种变体文件是被unlink掉的进程句柄还在但进程本身处于D状态或者僵死/proc里能看到却没法通过kill清掉。这种通常要连内核态一起处理属于更深的排障范畴日常遇到的比例不高但确实存在。2.2 挂载点遮蔽与跨文件系统遍历挂载点遮蔽的机制前面提过这里补一个更细的坑。假设/var/lib/docker原本在根盘上某天给/var/lib/docker挂了一块数据盘。挂载之后新盘的内容盖住了老目录df会显示根盘已用空间没有下降因为老数据还在根盘的块里只是“路径不可达”。du /也不会统计这部分因为它遍历时看到的是新盘内容而根盘上那些块对应的路径已经被遮蔽。验证有个小技巧用findmnt -T /var/lib/docker看当前目录挂的是哪块设备再在维护窗口把新盘umount掉du --max-depth1统计遮蔽目录老数据就露出来了。也可以借助debugfs直接读块设备对 ext4 用debugfs -R ls -l / /dev/sdX绕过挂载点看到真实目录内容这招在排查“空间去哪了”时很有用。跨文件系统遍历造成的du偏大则更常见。du -h --max-depth1 /不加-x时会钻进/proc、/sys、/dev、其他挂载点。其中/proc、/sys、/dev是伪文件系统里面很多文件的st_blocks为 0 或者统计口径特殊可能造成误导。我处理的规范做法是统计根盘一定带-x同时用--exclude排除伪文件系统和已知的大挂载点把口径锁定得死死的。2.3 硬链接与 reflink 的重复计数硬链接的本质是多个目录项指向同一个 inode。GNUdu在一次遍历中会记住已经统计过的 inode 编号遇到重复就跳过所以单次调用不会把硬链接算两遍。问题出在“分次调用再相加”的场景。运维常常分别对/data/a、/data/b跑du再把结果加起来如果这两个目录里存在跨目录硬链接同一个 inode 就会被统计两次汇总值比df大。这是很多人做容量报表时踩的坑看着数据膨胀还找不到原因。df从超级块拿数字一个 inode 的块只算一次天然不会重复。所以这类差异的特征是单独看每个目录的du很正常加起来就偏大。解法是改用一次遍历比如du -h --max-depth1 /data -x让du自己去重或者用find -inum排查重复 inode。reflink写时复制是另一个维度的重复计数。btrfs、XFS、部分企业级存储支持cp --reflinkauto复制出来的文件与原文件共享底层 extent物理上只存一份。可du是逐文件看st_blocks的两个共享块的文件会被各算一次导致du之和远大于df的真实占用。btrfs 上还有 subvolume 快照共享的情况。这种差异无法靠du消除需要文件系统专用工具比如btrfs filesystem du -s、compsize它们会按共享 extent 去重统计。2.4 稀疏文件与 --apparent-size 的干扰稀疏文件的表现和很多人直觉相反du显示得很小ls -l显示得很大df的数字和du一致都按实际块。所以单纯的稀疏文件不会让df与du产生冲突反而是排查时容易把人带偏——看到ls -l有 100G 就以为空间被这块吃了实际df根本没扣这么多。数据库的预分配表空间、QEMU 镜像、容器镜像层都可能存在稀疏区域。真正的坑在脚本里。有人为了让du输出和ls -l一致习惯性加--apparent-size结果du汇总出的总量比df大很多因为表观大小把空洞也算进去了。排查磁盘水位时du不要加--apparent-size除非你明确想核对表观大小。想单独确认稀疏程度可以用du -h file与du -h --apparent-size file对比两者差距越大说明空洞越多。2.5 文件系统自身开销与保留块除了保留块文件系统还有一批“非用户可见”的固定开销。inode 表在mkfs时就已经分配好占用固定块数ext4 的日志journal默认 128M 到 1G 不等目录本身也从 4096 字节起步目录项越多元数据块越多块位图、inode 位图也会占空间。这些开销df全部算作“已用”而du统计目录时只能看到目录占块看不到位图和日志所以df天然比du大一点点。在极小的文件系统上这个差距特别明显比如一个 100M 的测试分区光日志加元数据就吃掉十几个百分点。验证办法是tune2fs -l /dev/sdX看Block count、Reserved block count、Free blocks用这些数字手算一遍。XFS 用xfs_infobtrfs 用btrfs filesystem usage。我见过有人拿df的Used减去所有目录du之和得出“有 8G 神秘空间”最后查出来是保留块加日志加元数据白折腾一晚上。把固定开销算清楚能省下很多无效排查。3. 手把手复现与验证搭一个最小实验环境光讲原理容易记不牢动手复现一遍印象最深。下面的实验都在测试机或虚拟机上做不要在生产环境练手。我们用一块 loop 设备造一个可控的小文件系统方便观察每一次空间变化。3.1 用 loop 设备造一块可控的小文件系统先建一个 200M 的镜像文件格式化 ext4 并挂载dd if/dev/zero of/tmp/testfs.img bs1M count200 mkfs.ext4 /tmp/testfs.img mkdir -p /mnt/testfs mount -o loop /tmp/testfs.img /mnt/testfs df -h /mnt/testfs挂载后立刻对比df -h /mnt/testfs和du -sh /mnt/testfs。你会看到df的已用大约是几十 M 而不是 0du几乎是 0。这就是文件系统自身开销加保留块的直观体现。用tune2fs -l /tmp/testfs.img看保留块和元数据明细把df的已用减去du的占用差值应该能和这些固定开销对上。这一步做完你对“天然差值”就有量化概念了。3.2 复现“删除后未释放”写一个占位文件并用一个进程一直打开它然后删除dd if/dev/zero of/mnt/testfs/hold.bin bs1M count50 # 用一个后台进程持续持有句柄 tail -f /mnt/testfs/hold.bin /dev/null PID$! rm /mnt/testfs/hold.bin df -h /mnt/testfs # 已用仍然包含这 50M du -sh /mnt/testfs # 几乎为 0 lsof L1 | grep holdlsof会列出那个(deleted)的句柄。此时执行: /proc/$PID/fd/3文件描述符按lsof实际输出替换或者直接kill -9 $PID再看df空间立刻回收。这套流程在生产上对应“删日志文件后盘不释放”的标准处理。记住一个细节不是所有程序都能安全地被 truncate比如某些数据库的数据文件被清空后可能导致数据损坏操作前务必确认文件类型。3.3 复现挂载遮蔽在测试文件系统里建目录写数据再在它上面叠加一个小文件系统mkdir -p /mnt/testfs/covered dd if/dev/zero of/mnt/testfs/covered/big.bin bs1M count40 mkdir -p /tmp/other mount -t tmpfs -o size10M tmpfs /mnt/testfs/covered df -h /mnt/testfs # 根分区仍占着那 40M du -sh /mnt/testfs/covered # 只看到 tmpfs 里的内容 umount /mnt/testfs/covered du -sh /mnt/testfs/covered # 老数据重新出现umount之后老数据立刻现身这个现象能最直观地说明遮蔽问题。生产上遇到“df 高但找不到大头”一定要检查关键目录是否被新挂载点盖住findmnt -T逐个过一遍核心路径。3.4 复现硬链接重复计数在同一个文件系统里建硬链接分两次统计dd if/dev/zero of/mnt/testfs/a.bin bs1M count30 mkdir -p /mnt/testfs/dirA /mnt/testfs/dirB ln /mnt/testfs/a.bin /mnt/testfs/dirA/linkA ln /mnt/testfs/a.bin /mnt/testfs/dirB/linkB du -sh /mnt/testfs/dirA # 30M du -sh /mnt/testfs/dirB # 30M两个目录各有 30M加起来 60M但物理上只占 30M。df只认 30M。把这个实验和du -h --max-depth1 /mnt/testfs对比单次遍历的输出就是正确的 30M。这个例子解释了大量“报表加起来超了”的疑惑做容量统计时切记用单次遍历。4. 排查流程与命令速查实验做完回到生产排障。真正现场往往时间紧、压力大一套固定的决策路径能省下大量试错时间。下面把我常用的顺序整理出来。4.1 从差值方向出发的决策路径第一步对齐口径。用df -h 挂载点和du -h -x --max-depth1 挂载点做同层对比先排除跨设备遍历和伪文件系统干扰。第二步判断方向。df大就查句柄、查遮蔽、查保留块du大就查硬链接、查 reflink、查口径。第三步量化差值。把df已用减去du之和看看这个差值是不是恒定比例保留块与元数据是不是等于某块盘容量挂载混淆是不是随写入波动句柄泄漏。# 对齐口径 df -h /data du -h -x --max-depth1 /data | sort -hr | head -20 # 查已删除句柄 lsof L1 2/dev/null | awk {print $1,$2,$7,$9} find /proc/*/fd -ls 2/dev/null | grep deleted # 查保留块与元数据 tune2fs -l /dev/sdX | grep -Ei block count|reserved|inode count # 查挂载关系 findmnt -T /data -o TARGET,SOURCE,FSTYPE,SIZE,USED,AVAIL这套命令基本能覆盖八成场景。剩下的两成里还有容器 overlayfs 分层、网络存储快照、LVM 精简池未回收等更隐蔽的原因。容器场景要区分镜像层和可写层docker system df -v能看到镜像、容器、卷的分项占用网络存储要看是否开启了压缩去重块设备上报的容量和实际写入量可能不一致。4.2 必背命令组合目的命令说明同层对比du -h -x --max-depth1 /path加-x限制同一文件系统排序找大头du -h -x --max-depth2 /path | sort -hr | head两级深度更快定位找已删除句柄lsof L1或lsof | grep deleted需权限注意输出可能很长查 inode 水位df -iinode 满也会报“磁盘满”查保留块tune2fs -l /dev/sdXext4 专用查挂载树findmnt -T /path快速确认某个目录下面挂着什么交互式排查ncdu -x /path直观适合现场演示ncdu值得单独说一句。它是终端里的交互式磁盘分析工具-x限制同文件系统用方向键逐层钻取比反复敲du效率高得多。在客户现场解释空间分布时ncdu的可视化效果比一屏屏命令行输出好太多。安装简单主流发行版仓库里都有。4.3 常见问题速查表现象最可能原因验证方法处理df 大 du 小差值稳定小比例保留块元数据tune2fs -l对比正常无需处理df 大 du 小删文件不释放进程持有句柄lsof L1重启进程或 truncatedf 大 du 小差值等于某盘容量挂载点遮蔽findmnt -T 卸载迁移被遮蔽数据du 大 df 小报表相加才异常跨目录硬链接find / -inum改用单次遍历du 大 df 小btrfs/XFS 环境reflink 共享块compsize用文件系统专用工具du 大 df 小脚本加了 apparent口径混用去掉--apparent-size统一切到块口径df 满但 du 找不到大目录inode 耗尽df -i清理小文件调整分区后 df 不变未重扫或未扩容文件系统resize2fs、xfs_growfs扩容文件系统层最后一行值得展开。用工具把分区调大之后如果只扩了分区表或逻辑卷没有在文件系统层执行扩容df看到的容量不会变。ext4 需要resize2fsXFS 需要xfs_growfsbtrfs 用btrfs filesystem resize。扩容前先df -h记下原值扩容后再对比能避免“扩容了怎么没效果”的困惑。有些环境还需要partprobe让内核重读分区表或者对整盘设备做blockdev --getsize64确认底层容量真的变了。5. 生产环境避坑心得前面讲的都是方法论最后一部分分享几条用代价换来的经验。这些东西文档里不会写但现场真能救命。5.1 几条救命的注意事项永远不要在业务高峰期直接清空被进程持有的数据文件。用: file清空一个字节偏移已经很大的文件很多程序会按原偏移继续写中间形成巨大的空洞文件表观大小不变但实际占用可能不降反升还会导致读取异常。正确做法是先确认程序对文件句柄的处理逻辑能重启就重启不能重启就排查是否有官方提供的日志轮转机制。统计脚本统一口径。把df、du的调用封装成固定函数du一律带-x不带--apparent-sizedf一律指定挂载点而不是裸跑。这样出来的报表在各机器之间可比不会因为某台机器的目录结构特殊就出现离群值。我们在容量平台上就因为口径不统一出现过一个机器“凭空多出 200G”的假告警查了半天是脚本作者加了--apparent-size。保留块比例可以调但别随便清零。数据盘如果确实想要更多可用空间可以用tune2fs -m 1降到 1%但降到 0 会让 root 在磁盘写满时无法进行必要的文件操作风险很高。日志盘和纯数据盘可以适度降低系统盘保持默认。inode 和空间是两条独立水位线。大量小文件会把 inode 吃光此时df -h显示还有空间但写入照样报No space left on device。df -i必须纳入日常巡检。邮件队列、缓存目录、临时文件目录是小文件重灾区给这些目录单独挂载点并设置 inode 上限能有效隔离风险。注意lsof L1在文件数量极多时可能跑很久加超时控制避免排查命令本身把机器拖慢。5.2 监控与预防要减少“对不上账”的告警最好的办法是让监控同时采集两类指标并做交叉校验。空间维度采集df的使用率、可用量、保留块比例容量维度定期跑du -x --max-depth1形成趋势。当两者趋势出现持续背离比如df使用率上涨而du总量平稳就提前预警句柄泄漏或遮蔽问题而不是等到写满才被动救火。另一个实用做法是给关键目录建立基线。把每个挂载点下各一级目录的du值记录下来形成快照。日常巡检对比快照能第一时间发现“某个目录突然涨了 30G”这种异常比事后从零排查高效得多。这套基线配合ncdu交互使用日常维护基本不会漏。句柄类问题还能通过进程监控预防。对会产生大文件的常驻进程比如日志采集、消息队列、数据库监控其打开文件描述符数量和已删除句柄数一旦出现异常增长就介入。很多线上事故的根因不是磁盘不够而是该释放的块没释放监控到位就能把隐患消灭在告警之前。
返回列表