ARTICLE DETAIL

资讯详情

深耕网站视觉设计与运营推广的一线实战洞察。

Linux磁盘幽灵空间排查:df满du没满的真相与处理

Linux磁盘幽灵空间排查:df满du没满的真相与处理 凌晨两点四十监控平台的告警把值班手机震醒了。登录服务器一看根分区或者说某个数据分区使用率已经冲到95%以上df -h 红得刺眼。但等我跑了一遍 du整个人都愣住了系统里所有能看到的文件加起来离 df 报的已用空间还差一百多G。空间哪去了文件系统上明明没有那么多文件。这不是我遇到过唯一一次也不会是最后一次。磁盘空间这个事df 和 du 天生就带着两套口径同一块盘、同一个文件系统它们算出来的“已用”差出一大截是完全正常的事。真正的问题在于当 df 说满了、du 说没满这个“幽灵空间”到底被什么东西占着怎么在不重启、不误删的情况下把空间找回来这篇文章把我这些年踩过的坑、验证过的方法完整整理出来适合所有背过运维值班、或者自己管服务器的小团队参考。1. 先认清现象df 报满、du 没满不是玄学是两套账本这类故障最迷惑人的地方在于它不是磁盘真的坏了也不是监控误报而是你手里两个最常用的命令统计的根本不是同一个东西。我之前遇到的一个典型现场是这样的。某台跑着容器服务的节点/var 分区 500G监控告警“磁盘使用率 95%”。我登上去先执行df -hT /var输出Filesystem Type Size Used Avail Use% Mounted on /dev/sda3 ext4 493G 462G 6.2G 99% /var接着执行du -xsh /var输出296G /var一个说用了 462G一个说目录下所有文件加起来才 296G中间差了 166G。这 166G 就是所谓的“幽灵空间”。你要是在这种时候按照常规思路去找大文件用du -sh /var/* | sort -rh把目录翻个底朝天大概率什么也发现不了——因为问题根本不在“目录树里有名字的文件”上。这种情况下系统已经开始出现连带症状。比如服务写临时文件失败、数据库连接被拒绝、Java 进程尝试写堆转储时报 “No space left on device”。有些程序更脆弱写入一旦失败直接崩掉看起来像应用故障其实是底层磁盘空间已经把它逼到墙角。所以在动手之前先建立一个基本认知磁盘空间被占用不等于能看到“占用它的文件”。df 看到的是文件系统的账本du 看到的是目录树里的实体。两本账对不上才需要继续往下查。另外还有一个容易混淆的坑df 显示的 Avail 并不等同于“还有多少空间可以写”。ext4 为代表的文件系统默认会为 root 预留一部分块这部分既不在 Used 里也不允许普通用户用。所以有时候 df 明明显示 Avail 还剩几百兆但普通用户写入照样报磁盘满。这个细节后面会展开说。2. 统计口径差异df 看的是“块”du 数的是“文件”要搞懂幽灵空间必须先搞清楚这两个命令统计路径的本质差异。这不是“一个准一个不准”的问题而是它们观察的对象完全不同。2.1 df 的本质读文件系统的超级块账本df 的全称是 disk free它不是一个一个文件去数空间而是直接读取文件系统超级块superblock里的元数据。文件系统在格式化的时候会把整块盘划分成大量固定大小的块block并维护一套账本记录总共有多少块、已经分配出去多少块、还剩多少空闲块。df 报告的 Used翻译过来就是“文件系统已经分配出去的块”的总大小。这里的“已经分配”范围很宽有名字的普通文件占用的块被删除但仍被进程打开、尚未释放的块inode 表、块位图、inode 位图这类文件系统元数据日志区比如 ext4 的 journal占用的块特殊文件、稀疏文件实际分配的物理块注意最后一类很容易被忽略文件系统内部结构本身也要占空间。inode 表不是无限大的如果你存了成百上千万个小文件光 inode 表就能吃掉好几个 G。这部分空间没有任何一个“文件”对应du 永远看不见。打个比方df 相当于物业公司的总户数登记表。不管房间里住没住人、住的是业主还是租客只要房间分配出去了表上就算“已入住”。至于房间里是否真有人敲门应声那是另一回事。2.2 du 的本质沿着目录树挨个数文件du 的全称是 disk usage它从你指定的路径出发沿着目录结构逐个 stat 文件把每个文件占用块的数量累加起来。它能看到的东西必须满足一个前提在目录树里存在一条路径能引用到这个文件。也就是说文件必须“有名字”。du 天然看不见以下几类空间已经 unlink 但还被进程持有句柄的文件目录项没了但块还没释放文件系统内部元数据inode 表、位图、日志预留块reserved blocks挂载点下层被遮挡住的旧文件和旧文件系统已分配但失去引用的孤儿块文件系统异常后的残留du -sh /var那 296G就是“能看到名字的文件的实际块占用”。它和 df 的 Used 之间的差值全部由上面这几类空间构成。用生活类比du 相当于挨家挨户敲门数人头。门敲得开、有人应声的才算数。但有些人房间锁着文件被删了但进程还没撒手有些人住的是物业配套房元数据这些人 du 一个都数不到。2.3 其他会放大差异的边界情况除了上面说的无名字占用du 本身还有一些统计偏差叠加在一起会让差异更明显硬链接同一个 inode 被多个目录项引用时du 默认只统计一次这是合理的。但你如果拿它和 df 对账会因为这个特性感觉到微小差异。稀疏文件文件逻辑大小可能几十 G但实际分配了 1G 的块。du 统计的是实际分配的块所以和 ls -l 看到的逻辑大小也完全两码事。这种文件在某些应用场景下到处都是比如数据库预分配的数据文件、虚拟机磁盘镜像。跨文件系统边界不加-x参数的 du 会走进挂在目录树下的其他文件系统把 NFS、其他分区里的文件也算进去导致 du 的结果反而比 df 对应分区大。这也是排查时容易产生误导的地方。后面讲实操我会强调du -x的用法。搞清楚这些差异之后“幽灵空间”的本质就清楚了凡是占用在文件系统账本上、却不存在可见目录项的空间df 看得到du 看不到。3. 幽灵空间的几个真正来源按命中率排序根据我处理过的实际故障df 和 du 差距巨大时最可能的来源就那么几类。排查的时候按下面这个优先级往下走能少走很多弯路。排查顺序来源命中率关键命令1被删除但仍被进程打开的文件极高lsof L12文件系统保留块、元数据、日志中tune2fs -l3挂载点下方被遮挡的旧文件中低findmnt、lsblk4日志轮转失效、容器日志膨胀中journalctl --disk-usage5inode 耗尽导致的伪空间不足中df -i6LVM 快照、文件系统快照、reflink中低lvdisplay、btrfs subvolume list3.1 被删除但进程仍打开的文件这是最常见、最经典的原因几乎占了这类故障的八成以上。Linux 的文件删除机制是这样的执行rm操作实际上只是把目录项从父目录中移除简称 unlink。如果此时没有任何进程打开这个文件inode 的引用计数归零系统会立刻回收数据块空间马上释放。但如果有一个进程正以读写模式持有这个文件的句柄情况就变了目录项虽然没了inode 仍然有一个引用计数来自那个进程数据块不会被回收。更棘手的是进程还可以继续往这个“已删除”的文件里写入数据。也就是说你在文件系统上看不到这个文件但它的体量可能还在持续增大。典型的产生场景程序写日志时直接 open 文件之后不关闭句柄。运维做日志清理时图省事rm了日志文件但服务进程还一直持有旧句柄继续写入。日志轮转做了等于没做磁盘空间越吃越多。临时文件用完忘记关闭或者某个常驻进程的临时文件被外部脚本删除但进程还在继续写。数据库预分配的临时文件、undo 日志被外部误删但数据库进程还握着句柄。排查命令很直接lsof L1L1的含义是列出 link count 小于 1 的文件也就是所有“已被删除但仍被打开”的文件。输出里 NAME 一列通常会标(deleted)。如果只想看大小和路径可以这样lsof -nP L1 2/dev/null | awk NR1 || $4(deleted)注意不同发行版 lsof 输出的列位置可能有差异建议先不加 awk 直接看一遍全量输出确认 SIZE/OFF 列的位置再处理。这个命令极其关键大多数幽灵空间在这一步就会现出原形。3.2 文件系统“自留地”保留块、元数据和日志如果 lsof 没查出问题或者查出的 deleted 文件大小不足以解释所有差异就要看文件系统自身的开销了。ext4 默认在格式化时预留 5% 的块给 root 用户使用。这个设计本意很好当磁盘被普通用户写满时root 还能登录进来清理文件、修复系统。但对于一块大容量数据盘来说5% 可能是几十个 G相当可观。这部分块在 df 里不算 Used但它会从 Avail 中扣除并且导致 Use% 显示偏高。换句话说df 显示“满了”但实际 du 统计的文件占用和 df 的 Used 之间的差距里并不包含保留块保留块更多是让 Avail 变小、让百分比看着吓人。真正的元数据开销在很多情况下才是 du 和 df Used 差距的主要来源。inode 表、块位图、inode 位图、ext4 的 journal默认大约 128MB这些对 TB 级大文件系统来说占比不大但如果你存的全是几百 KB 的小文件几千万个 inode 的元数据开销可能达到几十 G。这就是为什么有时候“文件总大小”只有几百 Gdf 却显示用了五百多 G。查看具体参数tune2fs -l /dev/sda3 | grep -E Block count|Reserved block count|Free blocks计算一下如果差异在几个 G 到几十个 G 量级、且没有删除大量小文件或大量目录基本可以判定是元数据开销。这种情况通常不需要处理属于文件系统正常运行成本。当然如果你觉得保留块比例不合理可以用下面的命令调整后面第 5 节详细讲。3.3 挂载点下方的“前任文件”这是一个非常隐蔽、但真实发生过的坑你在一个有数据的目录上直接挂载了一个新文件系统原来目录里的文件被“遮住”了。举个例子。某台服务器上/data目录原本存了 20G 数据后来同事往/data上挂了一块新磁盘。挂载成功后从目录树看/data是空的du 算出来也只有新的文件系统用量。但底层文件系统上的那 20G 旧文件并没有消失它们占用的块还是记在底层文件系统的账本上。df 能看到这 20G 已经被使用du 却完全看不到因为它进的是上层文件系统的边界。排查方法是看挂载关系findmnt -R / lsblk -f cat /proc/mounts重点关注有没有一个目录既被挂载、底层又有非空数据。如果确实存在这种情况需要先卸载上层文件系统把旧文件迁走或者清理掉再重新挂载。这种问题常见于临时挂载、紧急加盘、或者把整块盘挂到已有数据的目录上的搬迁操作。等你想起来旧数据没处理可能已经过去几个月了。3.4 日志轮转失效与容器日志膨胀系统日志、应用日志、容器日志是另一大块容易被忽略的“有名但看不见”的占用。这里的“看不见”不是指文件系统层面看不见而是指你在排查时没往那个目录想。systemd 日志默认存储在/var/log/journal用 journald 接管了很多服务的输出。journald 默认有大小限制SystemMaxUse通常设为文件系统大小的 10%但如果你配置不当或者容器场景下日志被某个进程持续打开就可能出现异常膨胀。Docker 的 json-file 日志驱动也有类似问题。默认情况下一个容器如果疯狂打日志json 日志文件会一直增长而且 containerd/docker 进程一直持有它的句柄。你从容器外部用du看/var/lib/docker/containers能看到文件大小但如果你直接rm了它空间不会释放因为句柄还在。这类问题的本质又回到了第 3.1 类的“deleted but open”。检查命令journalctl --disk-usage du -sh /var/lib/docker/containers/*如果确实膨胀日志轮转配置要重新设计而不能只靠手动删。手动删日志删完空间不释放还会让你误以为“清理没生效”。3.5 inode 耗尽一个长得像“没有空间”的兄弟问题有时候 df 和 du 对得上账空间明明还有但系统就是写不进新文件。这种时候十有八九是 inode 耗尽了。inode 是文件系统里用来记录文件元数据的索引节点每个文件和目录都要占用一个 inode。inode 的数量在格式化时就定了不能像空间一样动态扩容。当你创建了大量小文件——比如 cron 邮件堆积在/var/spool/mail下、squid 缓存目录里塞满了碎片文件、某个程序疯狂创建临时文件——inode 会先于空间被耗尽。检查命令df -i如果 IUse% 接近 100%即使 df -h 显示 Avail 还有很多任何创建文件的操作都会报错。处理办法和老生常谈的一样删除无用的海量小文件或者重建文件系统时把 inode 数量调大-i参数调整 bytes-per-inode。这个问题和“幽灵空间”的关联在于很多人在排查“df 满、du 没满”时会顺手查一下df -i。两者可能同时发生也可能一个是另一个的诱因。先排除 inode 问题再聚焦空间差异排查链路才完整。3.6 LVM 快照、文件系统快照与 reflink最后这一类比较偏冷门但在特定环境下命中率不低。LVM 快照的原理是 COW写时复制创建快照时并不复制全部数据只在原卷被修改时把将要被覆盖的数据复制到快照预留空间里。如果原卷变化量很大快照空间会被慢慢吃满此时原卷和快照都无法正常写入。lvdisplay里能看到快照的实际分配量。文件系统层的快照也是类似逻辑ext4 没有原生快照但 btrfs/zfs 有。另外 reflink比如cp --reflinkauto创建的“镜像文件”在逻辑上是大文件物理上却和原文件共享数据块。du 如果按逻辑大小统计反而会算出比 df Used 更大的数字。这种反过来“du 比 df 大”的情况也经常让人一头雾水。判断方法lvdisplay # 看 LVM 快照的 Allocated to snapshot btrfs subvolume list /path4. 一次完整排查实操lsof 出场空间立刻“现身”前面讲了原理和可能来源这一节用一个真实案例把完整排查链路走一遍。案例背景K8s 节点/var 分区 500G告警阈值 95%。我登录后没有马上执行任何清理命令而是先建立一个“最小命令组合”用来确认差异来自哪个层面。4.1 第一步用最小命令集锁定方向df -hT /var df -i /var du -xsh /var输出分别对应/var 文件系统类型、总大小、已用、可用、使用率/var 的 inode 使用率目录树视角下 /var 的总占用这里我特意用了du -x让它不要跨文件系统。如果挂了别的分区在 /var 下面du -x能避免把它们算进来。当时的输出df -hT /var Filesystem Type Size Used Avail Use% Mounted on /dev/sda3 ext4 493G 462G 6.2G 99% /var df -i /var Filesystem Inodes IUsed IFree IUse% Mounted on /dev/sda3 2.7M 1.2M 1.5M 44% /var du -xsh /var 296G /varinode 只用了 44%排除 inode 耗尽。df 的 Used 462G 与 du 的 296G 之间差了 166G方向非常明确存在大量“无名字”的空间占用。下一步直接查 deleted 文件。4.2 第二步lsof L1 锁定真正的占用者在 500G 规模的磁盘上lsof 全量扫描可能要几秒钟。生产环境建议加-nP跳过主机名和端口解析加快速度lsof -nP L1 2/dev/null输出结果里出现了几条这样的记录COMMAND PID USER FD TYPE DEVICE SIZE/OFF NODE NAME dockerd 1234 root 26w REG 8,3 168933011456 123456 /var/lib/docker/overlay2/abcdef/.../app.log (deleted) nginx 5678 www-data 11w REG 8,3 2147483648 234567 /var/log/nginx/access.log (deleted)第一行瞬间解释了 166G 中的大头/var/lib/docker/overlay2/.../app.log (deleted)SIZE/OFF 列显示约 157G。这是一个被删除但仍被 dockerd 进程持有的容器日志文件。这 157G 已经不在目录树里du当然看不到。但 dockerd 进程一直握着文件句柄文件系统也不敢释放这些块。4.3 第三步处理前先判断影响面不要手一抖就 kill看到 deleted 文件后很多人第一反应是直接kill -9对应 PID把句柄强制回收。但这是有风险的操作。那个进程如果是数据库或关键业务服务直接杀掉可能导致长时间不可用、数据丢失或集群重新调度。合理的选择按优先级排列看进程管理方式如果是 Docker 容器产生的日志直接重启容器或systemctl restart docker句柄会被释放。但要确认容器有副本或能自动拉起别把整个业务拉挂。如果是 nginx、apache 这类服务执行优雅 reload 即可新日志会重新打开新文件旧句柄随之释放。比如nginx -s reload。只有确认进程可以安全重启时才考虑 kill。我自己当时的处理是找到容器 ID优雅重启了对应容器。重启后再次执行lsof -nP L1 2/dev/null输出为空说明 deleted 文件句柄已经全部释放。再看 dfdf -hT /var Filesystem Type Size Used Avail Use% Mounted on /dev/sda3 ext4 493G 296G 172G 64% /var空间回来了 166G和 du 的统计对上了。4.4 如果 lsof 没结果继续往下追查lsof 查不到 deleted 文件时差异来源通常在另外几种。按照这个顺序继续findmnt -R /var # 确认子目录里有没有额外的挂载点遮蔽底层文件 tune2fs -l /dev/sda3 | grep -E Block count|Reserved block count lsblk -f如果差异在几个 G 到十几个 G且文件系统里小文件极多金属数据开销的可能性最大。这种情况下只要服务正常、监控有余量不必强行清理。真正的“幽灵空间”是找不回来的因为它根本不是文件占用的而是文件系统结构本身在占。如果差异非常大、同时有断电或异常关机历史考虑文件系统异常导致已分配块失去引用。这种情况需要卸载文件系统后执行 fsck 检查。生产服务器务必先评估停机窗口并先做好备份。fsck 是修复工具不是查询工具乱跑一样有风险。5. 收尾不是删完就完文件系统配置、日志轮转与残留句柄把空间找回来只算完成了一半。如果不对产生问题的根源做处理过几周大概率会再次告警而且原因可能一模一样。5.1 能降保留块的场景把 tune2fs 用起来对于非根分区、纯数据盘ext4 默认 5% 的保留块确实偏保守。一块 1T 的数据盘5% 就是 50G多少有点浪费。如果你的数据盘上有独立监控、每天有备份、不担心 root 无法登录清理的问题可以调低保留比例tune2fs -m 1 /dev/sda3-m 1表示保留 1%也可以直接设成 0。但根分区不建议设 0因为根分区一旦写满连清理文件的操作都可能无法执行你需要能写入临时文件系统稳定性会大打折扣。数据盘也一样建议至少留 1%别把最后一点退路堵死。执行完 tune2fs 后再用df -h看Avail 会比之前多出来约 4% 的空间。如果文件系统正在挂载使用这个调整是即时的不需要重启。5.2 日志轮转别再用 rm 直接删日志这次故障的直接诱因是容器日志无限增长。Docker 默认的 json-file 日志驱动不会自动切割日志文件可以一直涨到占满磁盘。处理方式有两种第一种给现有容器重启时加入运行时限制或者在/etc/docker/daemon.json里配置{ log-driver: json-file, log-opts: { max-size: 100m, max-file: 5 } }配置完需要重启 docker 服务和容器才生效。注意旧容器如果不重建配置不会自动套用。第二种业务侧做日志轮转。系统级的 logrotate 配置要特别注意不要简单地把轮转后的旧文件删掉而要确保应用进程在日志被轮转后能重新打开新文件。常见的做法是copytruncate它先把当前日志文件复制一份然后立即截断原文件这样进程持有的句柄没有变化也无需重启服务。代价是复制和截断之间存在极小的时间窗口可能丢一点点日志但对于多数应用完全无所谓。我自己更推荐的做法是create加reloadlogrotate 先将旧日志重命名然后创建一个新的同名日志文件再执行 postrotate 脚本让应用重新打开日志比如nginx -s reopen。这样不丢日志也不影响业务。5.3 容器场景日志目录做容量上限在 K8s 或 Docker 环境里我还建议对容器日志目录单独做监控或配额。一个很实用的做法是du -sh /var/lib/docker/containers/*定期巡检或者借助 logging driver 直接把容器日志投递到集中式日志平台本地只保留小体积文件。如果出现“删了容器日志文件但空间没释放”的情况先确认句柄持有者再决定是重启容器还是等待日志驱动自行切割。最忌讳的是删一个跑一个看着磁盘空间没变化心态先崩了。5.4 文件系统检查什么时候需要 fsck如果故障发生前有过断电、强制关机、云主机强制停止等异常文件系统可能出现“已分配但任何目录项都不引用”的孤儿块。这种情况不能用 lsof 或 du 查出来只有 fsck 才能扫描并修复。fsck 前必须注意生产环境先评估停机窗口尽量在维护时间做文件系统最好是卸载状态执行不得已也要以只读方式重新挂载数据盘先做快照或备份fsck 本质是修改底层元数据有风险对 ext4 文件系统umount /var fsck.ext4 -f /dev/sda3从头到尾跑一遍之后用df -hT /var重新确认如果 Used 明显下降、Avail 增加说明确实有孤儿块被回收。6. 别把警报只挂在 df 上监控指标设计的反向思考处理完一次幽灵空间故障后比删掉多少 G 更有价值的事情是重新思考你的监控告警体系。传统的“磁盘使用率超过 85% 就告警”只考虑了 df 视角它在很多场景下既会漏报也会误报。6.1 df 和 du 该各看各的各管一摊我的建议是监控里同时放三组指标df 使用率用于判断“还能不能写”。这个指标最直接告警阈值可以根据磁盘容量调比如小容量盘 80% 就要 warn大容量盘可以放到 90%。df -i inode 使用率用于判断“还能不能创建文件”。很多公司根本不监控 inode直到业务突然创建不了文件才追悔莫及。inode 告警建议比空间更早触发因为 inode 耗尽后清理工作量大得多。deleted 文件总大小这是本次故障最能提前发现问题的指标。大量 deleted 文件积压说明日志轮转、进程句柄管理出了异常。写一个简单的统计命令挂到 nagios 或 zabbix 之类的监控系统里#!/bin/bash # 统计当前系统所有 deleted 文件的总大小单位字节 lsof -nP L1 2/dev/null | awk NR1 {sum $7} END {printf %.0f\n, sum}这个值如果持续增长就应该告警。它比单纯盯 df 更早暴露隐患也更容易定位问题主体。6.2 开发侧判断磁盘空间时别拿不同口径的数据比写代码确认磁盘空间时也常踩类似的坑。比如 Java 的File.getUsableSpace()对应的是系统调用 statvfs 的 f_bavail也就是 df 里 Avail 那列它是“当前用户实际可用空间”不等于文件系统总容量减已用。C# 里的GetDiskFreeSpaceEx返回的lpFreeBytesAvailableToCaller也是类似语义它受卷配额、权限限制和保留空间影响。如果程序一边通过 du 统计了日志目录里所有文件大小一边通过 df 查询“剩余空间”然后判断“空间够不够删”这种对比本身就是错的。目录文件总大小和分区可用空间不是同一个账本中间隔着元数据开销、其他文件占用和 deleted 文件。正确的做法是统一口径要么都用文件系统层的数据做容量判断要么都用目录树统计做清理判断不要混着用。6.3 这个问题在 Windows 上同样存在Windows 用户也会遇到“明明删了很多文件C 盘空间没回来”的诡异现象只是背后的主角换成了卷影副本、系统还原点、页面文件和休眠文件。资源管理器里看不到它们但在“磁盘清理”工具中选择“清理系统文件”或者用管理员权限执行vssadmin list shadowstorage就能看到卷影副本到底吃了多少空间。这和 Linux 下“目录树看不到但文件系统在占用”的逻辑是同构的空间占用并不总以你能看见的文件形式存在。还有 U 盘、移动硬盘在 Windows 下显示“写保护”“无法格式化”但容量却正常的情况很多时候也和文件句柄占用有关。要么是杀毒软件正在扫描、要么是某个进程还握着卷的句柄。先关闭所有窗口、结束相关进程再重新插拔问题往往就解决了。6.4 最后分享一点个人经验这套排查流程走过几遍之后我现在接手一台“诡异磁盘告警”的服务器顺序已经固化成肌肉记忆先df -hT和df -i锁定边界再du -xsh估算差距量级接着lsof -nP L1找 deleted 文件。这个组合在三分钟内就能回答“空间去哪了”这个核心问题剩下的只是选一个安全的释放方式。真正让我觉得值得写下来的是“不要被命令输出吓住”这个心态。df 说满了、du 说没满不代表系统坏了也不代表监控误报。它只是文件系统在用两种不同的语言告诉你同一件事空间确实被占了但占用它的东西不在常规视线里。理解两套统计口径的差异顺着差异的方向一层层往下查幽灵空间自然无处遁形。
返回列表