ARTICLE DETAIL

资讯详情

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

Linux磁盘空间诡异消失?从df与du差异到lsof定位已删除未释放文件

Linux磁盘空间诡异消失?从df与du差异到lsof定位已删除未释放文件 有段时间没折腾 Linux 服务器了最近帮朋友排查一个“诡异”的线上问题磁盘明明显示满了但翻遍系统里所有文件都找不到占空间的大文件df 和 du 的结果差了十几个 G。当时第一反应就是——文件删了但空间没释放。这个坑在 Linux 运维里太经典了几乎所有老手都遇过新手第一次碰上往往会愣住半天。今天把这类问题的底层原理、完整排查链和实际解法整理出来希望对正在被“幽灵空间”折磨的人有帮助。这个问题说大不大说小不小不处理的话可能引发 MySQL、Nginx、日志系统写不进去服务直接挂掉处理起来如果不懂原理也很容易误删文件、误杀进程把线上环境搞得更乱。这篇文章的目标很清楚让你在 10 分钟内定位到“谁占住了已删除的文件”并安全地释放空间。同时我会讲清楚 inode、文件描述符、硬链接这些概念这样后面即使换一种场景你也能举一反三。1. “磁盘满了但啥也没删掉”——先确认你遇到的真是这个坑1.1 现象复盘df 和 du 结果的巨大落差先说判断问题的标准场景。在 Linux 上查看磁盘占用大家一般会用两个命令df -h看的是文件系统层面的容量和已用空间也就是整个分区到底还剩多少。du -sh看的是目录或文件实际占用的块数是按目录树去遍历统计出来的。正常情况下df显示的 used 总量应该大约等于各目录du加起来的和不算保留块、元数据之类的小额差值。但如果遇到文件被删除却仍被进程占用的场景就会出现一个非常明显的特征$ df -h /data Filesystem Size Used Avail Use% Mounted on /dev/vdb1 50G 48G 2.0G 96% /data $ du -sh /data/* 2/dev/null | sort -rh | head -5 4.0G /data/mysql 3.2G /data/logs 1.5G /data/www ...把列出来的全加起来大概只有 15G 左右但df显示已经用了 48G。这个差值就是传说中的“被删除但仍被占用的空间”。我第一次遇到时也是一脸懵那么大一块地方凭空消失了还有一种更隐蔽的现象du单独看某个小目录时很正常但整个分区df已经 100% 了写不进去任何新文件。这种时候不要急着去翻目录先怀疑是不是有进程在“口含”一个已删掉的大文件。1.2 为什么文件删了空间却没归还核心原因一句话就能说清在 Linux 中文件的删除unlink和空间的真正释放是两回事。当你执行rm删除一个文件时系统做的其实是两步动作将该文件的硬链接计数减 1如果链接计数降到 0从目录结构中移除该文件名。但这里有个关键前提如果此时有进程已经通过 open() 打开了这个文件文件系统不会立刻把它的数据块标记为“空闲”。进程手里的文件描述符仍然指向那个 inode而 inode 还带着这些数据块直到该进程关闭这个 fd 或进程本身退出内核才会完成真正的空间回收。用一个生活化的类比这就像你在一家餐厅订了一桌菜菜已经端上来了但服务单上先划掉了这道菜又因为你这桌还在吃后厨就不能把盘子撤掉重新使用。rm就是“菜单划掉”进程退出或关闭 fd 才是“撤盘子洗碗”。知道这个原理之后前面的现象就完全不神秘了df看到的“已用”是把那些还没被回收的数据块算进去的而du是按文件名去遍历的文件名字都没了自然统计不到。于是两边就产生了巨大的“空间差额”。1.3 怎么快速确认差额确实存在不要光靠感觉可以用两条命令把差额量化出来$ df -B1 /data | awk NR2 {print $3} 51539607552 $ du -sB1 /data 2/dev/null | awk {print $1} 16106127360要是df的“已用”比du在/data下统计出来的大得出奇比如差了好几个 G那基本上就锁定“已删除文件仍被占用”这个方向了。当然也要先排除挂载点覆盖、子挂载这种情况比如/data/mysql其实是一个独立分区再下结论。排除方法很简单df -h里看看这个目录是不是一个独立的设备。2. inode 与硬链接把“文件已删但空间不走”背后的机制吃透2.1 搞清楚 inode 是什么才能理解空间的真实归属很多人在排查这个问题时卡住不是因为不会敲命令而是对文件系统的结构没有概念。简单拆解一下一个在 Linux 上的“文件”至少由两部分组成——文件名dentry目录项和 inode索引节点。inode 里保存的是文件的元信息大小、权限、属主、最后修改时间以及指向实际数据块的指针。数据块才是真正占用磁盘空间的地方。当你open()一个文件时实际获得的是一个“引用”inode 的文件描述符。通过/proc/pid/fd/可以查到这个 fd 指向哪个 inode。而当你rm文件时删除的是目录项同时把 inode 的链接计数i_nlink减一。但这个 inode 本身只要还有 fd 指着它就不会被彻底回收。磁盘空间跟着 inode 走目录项半小时删掉一百个也不影响实际块回收只要 inode 没自由块就一直被拴着。2.2 文件描述的“一删一留”链接计数和 fd 的关系再补一个细节为什么进程一直开着不关空间就永远释放不了因为进程的 fd 表里保存着一个指向 inode 的指针只要 fd 不关闭内核就认为这个文件还是“活”的。即使目录里已经看不到这个名字通过/proc/pid/fd/fd号仍然可以访问它的全部内容——这也是我们后续“无进程重启强行清空文件”的解法的理论依据。这一步还引出一个点如果一个文件有多个硬链接比如你ln file1 file2创建了硬链接那么rm file1并不会清空文件内容因为 inode 的链接计数从 2 变成了 1数据块仍在目录树里有“主人”。只有在所有硬链接都被删除、且没有任何进程持有 fd 的情况下内核才会真正释放 inode 和数据块。所以排查空间不释放时不能只盯着“被删除的文件”还得想想有没有硬链接在背后“续命”。2.3 常见误解纠偏删不掉和删了不释放是两回事新手经常把两个问题混在一起一个是“删除操作本身报错”比如Text file busy、权限不足、只读文件系统、文件在 NFS 上被锁定等另一个是“删除成功但空间不变”就是本文说的这个典型坑。前者是删除动作没成功后者是删除动作成功了但空间回收被延后。两者排查工具也不一样前者多用lslocks、fuser、mount查看状态后者直接用lsof找占用的进程。这里特别提醒如果在项目里看到df满了先看清楚是哪一种不然可能忙活半天检查权限问题根本没在点上。3. 完整排查链路两行 lsof 找“罪犯”然后顺藤摸瓜锁进程3.1 核心命令lsof L1 以及 /proc 目录下的 fd 线索经过前两节的铺垫现在可以进入实战了。定位“谁占住了已删除文件”最核心的是这条命令$ lsof L1 /data COMMAND PID USER FD TYPE DEVICE SIZE/OFF NODE NAME mysqld 1234 mysql 4u REG 253,17 1073741824 456789 /data/mysql/ibdata1 (deleted)其中L1表示“列出 link count 1即已删除但仍被打开的文件”。输出里的SIZE/OFF就是当前文件的实际大小通常也就是你丢失的空间NODE是 inode 号NAME末尾的(deleted)是明确的标记。如果线上环境没有lsof别慌可以用/proc手动找。系统把每个进程打开的文件描述符都暴露在/proc/pid/fd/下配合一次全盘扫就能定位$ for pid in /proc/[0-9]*; do p${pid#/proc/} for fd in $pid/fd/*; do if readlink $fd | grep -q (deleted); then echo PID $p FD ${fd##*/}: $(readlink $fd) fi done done这里readlink会显示 fd 指向的真实路径删除后的文件会名校注(deleted)。这个办法不用装额外工具在精简系统上也能跑只是输出格式不太友好但能救急。3.2 看到结果之后怎么确认这一单具体情况lsof L1往往会把所有分区的情况都列出来不只有/data。想更聚焦某个文件系统可以先用df确认设备名再过滤$ df -h /data | tail -1 /dev/vdb1 ... $ lsof L1 | grep /dev/vdb1或者直接 D 指定目录做深度遍历注意 D 会递归整个目录线上大目录要小心性能开销$ lsof D /data 2/dev/null | grep deleted还有一种常见情况lsof L1没有任何输出但空间确实没释放。那多半不是普通文件被删而是下面这些衍生场景文件被映射到内存里比如 mmap 的共享库lsof仍能看见但L1有时会因为权限原因漏掉——记得用sudo是 socket 或临时文件被删除后仍被持有NAME 列可能不写(deleted)而是其他特殊状态是某些容器、systemd 服务在MountNamespace里持有已删除的挂载点内容。先用 sudo 跑一遍lsof L1是最省事的。没结果再往下查/proc/*/mountinfo看有没有已删除的挂载点残留grep -i deleted /proc/*/mountinfo。3.3 从 PID 反查业务归属别看到进程名就动手当我们定位到占用方是一个进程后不要一上来就 kill。先说一个真实教训我碰到过一次lsof显示/data/log/access.log (deleted)被一个叫filebeat的进程占着但当时怀疑是误报反复核对 PID 才发现那个 filebeat 是在另一个容器里跑的杀死它比重启业务更安全处理方式完全不同。所以拿到 PID 后至少再补几条确认链$ ps -fp 1234 UID PID PPID C STIME TTY TIME CMD mysql 1234 1 0 Jun10 ? 00:12:34 /usr/sbin/mysqld --basedir/usr $ ls -l /proc/1234/cwd /proc/1234/exe确认进程的启动时间、父进程是谁、当前工作目录在哪。如果 PID 对应的进程是nginx、php-fpm、mysqld、docker-containerd-shim这种对应的处置方案差别会很大有的可以安全重启有的是核心业务不能随便动。这里有一个极强的操作建议先把 pid、文件大小、路径、进程启动命令行记下来存到一个临时文件里再想“杀不杀”。因为后续无论你采取哪种释放方式都需要回到这些信息而且排查操作一旦被打断凭大脑记忆很容易遗漏多个占用进程。4. 释放空间的三种正解重启进程、清空 fd 指向的内容、接管后写入4.1 最常规的做法重启占用进程对大多数场景来说最简单的办法就是重启那个还“含”着已删除文件的进程。比如 Nginx 的 access.log 被rm但 master/worker 还没释放执行$ nginx -s reload # 或者 $ systemctl restart nginx重启之后进程重新干净地打开日志文件旧空间立刻被回收。验证方法很简单$ df -h /data使用率会明显降下来。这种方法对 MySQL、PostgreSQL、Redis 都适用但前提是业务允许短暂中断。能交接的尽量先交接再重启比如 MySQL 可以先用FLUSH TABLES或转主从切换。注意有些服务通过reload或HUP信号并不代表所有 worker 都会重新打开文件比如旧 worker 在慢请求期间仍可能保留旧 fd。最稳妥的是先发 reload等一会儿再扫一遍lsof L1看还有没有残留还有残留就再确认一次旧进程是否退干净。4.2 不想重启进程用 truncate 把“看不见的文件”抠出来很多场景不能随便重启比如线上一个跑了很久的 Java 应用、一个连接数很多的 MySQL 实例或者其他不能中断的服务。这时候有个更温柔的做法不杀进程先把那个已经删除但还被占用的文件内容“压扁”成 0 字节让内核恢复它占用的数据块同时进程继续对 fd 做正常写入。具体命令$ ls -l /proc/1234/fd/4 lrwx------ 1 mysql mysql 64 Jun10 09:30 4 - /data/mysql/ibdata1 (deleted) $ truncate -s 0 /proc/1234/fd/4 # 或 $ cat /dev/null /proc/1234/fd/4执行后进程手上的 fd 仍然有效但它指向的文件大小变成了 0原来占用的块会被回收。配合df -h立刻就能看到可用空间涨回来。这里有个细节要注意对于 MySQL 的 InnoDB 表空间文件直接对 fd 执行 truncateibdata1有可能导致数据库异常操作前一定要确认你是不是“误删了仅此一份的表空间文件且 MySQL 还开着”这种极端情况普通的日志文件没问题数据库文件则要先做备份或评估。为什么不建议用 /proc/1234/fd/4这种 shell 重定向来“清空”呢因为有些进程需要精确的偏移量写入重定向会改变文件的 offset可能引发意外。truncate -s 0是直接设置大小不碰 offset对很多只追加写入的守护进程更友好。4.3 logrotate 场景的延伸下次别再犯同样的节奏错误很多“空间不释放”的事故都来源于 logrotate 配置。logrotate 默认会在轮转时把旧日志改名再向主进程发送信号让它重开文件。但如果你把配置写成copytruncate它不发送信号只是在轮转后清空原文件反之如果配了create但进程不及时响应 HUP就可能出现“旧文件已被 rename 但仍被进程持有”于是旧日志的空间一直被占住越来越大直到磁盘打满。遇到这种情况要处理的不只是这一轮而是检查 logrotate 配置/etc/logrotate.d/nginx /var/log/nginx/*.log { daily rotate 7 compress delaycompress missingok notifempty create 640 nginx adm sharedscripts postrotate [ -f /var/run/nginx.pid ] kill -USR1 $(cat /var/run/nginx.pid) endscript }如果进程没响应USR1那它仍在写旧 fd。这就是为什么很多团队的规程里logrotate 之后必须人工扫一遍lsof L1。养成这个习惯能省去大半“空间神秘消失”的排查工作。5. 为什么“清理完了”过几天又满了我踩过的三个衍生坑5.1 系统保留空间与预留块的干扰新手在释放空间后习惯用df -h里Use%来判断“是否清理干净”这本身没错但 EXT4 默认会预留 5% 的块给 root 用户做应急mke2fs -m可以调整。所以你会发现清理后df显示Use%永远是 96% 甚至 100%而实际还能写。这不是 bug也不是又有文件占着$ tune2fs -l /dev/vdb1 | grep Reserved block count看到预留比例后心里就有数了。如果你确认不需要那么多预留也可以临时调低或关闭但不建议在生产环境把预留设成 0否则磁盘满的时候普通用户直接写不进文件root 也没有“最后一根救命稻草”。5.2 未挂载的分区、inode 耗尽与文件系统“回收延迟”“释放完仍然显示满”的第二个隐蔽场景是inode 耗尽。很多人只盯块空间忽略了文件系统还有另一项资源就是 inode 数量。如果文件系统里满是 0 字节小文件就可能出现df -h显示还有空间但df -i显示IUse%100%此时任何新文件都创建不了包括临时文件进程报错“No space left on device”。排查命令是$ df -i /data另外还有一个场景是挂载点阴影。假设你先在/data写了若干文件之后又把另一块盘挂载到/data/mysql那么原来/data/mysql下的文件虽然不再出现在du的统计里但它们仍在第一块盘的 inode 上占用空间。这种“幽灵目录”既不是文件被删除也不是进程占用只是被挂载点挡住了排查时容易绕圈。解法是把该目录卸载或用mount --bind访问底层。最后是trim/discard的问题。如果你用的是 SSD 和某些文件系统如 f2fs、ext4 的discard选项没开删除后块迟迟不标记为可用df层面通常会先回收但底层闪存回收是异步的。极端情况下要主动执行$ fstrim -v /data这个属于比较少见的衍生场景但碰到“数据明明清了、SSD 性能一直掉”的故障时值得查一下。5.3 删除的是“目录树”还是把整个挂载点底下的内容毁了还有一种事故级别的误操作就是删文件时路径写错。比如你在某个目录下执行rm -rf *但当前目录其实是挂载点本身删掉的不是某个子目录而是整个分区内容。这种时候抢救空间反而成了次要问题——数据先丢了一大批。所以每次做高危删除建议先执行$ pwd $ ls -la $ df -h .确认自己在哪个分区、哪个目录再动手。这句废话在事后复盘里往往是最值钱的一条经验。6. 一个被低估的预防方案先“软删除”观察期过了再 rm6.1 用 mv 代替 rm为排查留出后门上面讲的都是事后处理。真正有经验的人会在一开始就避免让“已删除文件被占用”成为难题用mv而不是rm。把要清理的大文件先移动到磁盘外的临时目录或加上.trash后缀$ mv /data/logs/bigfile.log /data/logs/.trash/bigfile.log.20240101这样即使进程还持有 fd也不会立刻造成“目录项消失但空间仍被占用”的尴尬——你随时能在目录里看到这个文件到底还存不存在、有多大。离开观察期后再执行rm此时先扫一遍lsof L1如果进程确实没再持有则安全删除。这种“软删除”策略也可以在脚本里做成规范所有删除动作都先进回收站目录保留 7 天自动清理。6.2 预防性巡检把 lsof L1 变成定时任务不需要等出事了再手忙脚乱完全可以把这个检查放进 cron 或者监控系统$ crontab -e */30 * * * * /usr/bin/lsof L1 /var/log/lsof_deleted.log 21配合一个简单的告警脚本只要输出行数超过阈值就发钉钉或邮件。这样“磁盘满了但还没放出来”的问题通常能在占用量超过 50G 之前就被发现而不是等到业务写不进去。#!/bin/bash count$(wc -l /var/log/lsof_deleted.log) if [ $count -gt 10 ]; then mail -s Deleted-but-open files detected opsexample.com /var/log/lsof_deleted.log fi这个方案对中小团队非常实用零依赖、不引入额外监控组件逻辑一眼就能看懂。6.3 团队协作里最容易漏掉的一个点排查记录最后补一个“软经验”。踩这个坑的人经常互相问“怎么查”但很少有人会问“你之前是在哪一步查到那个进程的”。我建议每个团队沉淀一份这样的排查记录把命令、现象、处置方式写成几行注释放在知识库里。这样下次谁再遇到不需要重新做完整的lsof教学直接查库就能落地。毕竟诊断思路这种东西不是每次都能靠“灵感”想起来的能复用的记录远比临时发挥可靠。按我现在的习惯服务器上任何磁盘相关操作前都会跑一遍df -h清理大文件时先lsof L1确认没有进程持有再动手。如果你现在还处于“空间没了但找不到原因”的阶段直接按上面的链路走一遍基本能收敛在十分钟内。这个本事不难但关键时刻真能救命。
返回列表