ARTICLE DETAIL

资讯详情

深耕网站建设、视觉设计与SEO优化的一线实战洞察。

Linux删除文件后df空间不释放?inode与文件描述符深度解析

Linux删除文件后df空间不释放?inode与文件描述符深度解析 删了文件df -h却纹丝不动是 Linux 运维里最经典的“幽灵问题”之一。你用一个rm -f把 30G 的日志删掉了回头一刷分区还是 98%再刷还是 98%甚至怀疑自己是不是删错了目录。其实文件系统底层的 inode、文件描述符和 VFS 引用链在背后把这块空间死死摁住了。这篇文章我从现象复现开始一路拆到unlink(2)系统调用、VFS 缓存对象和引用计数再给你一套完整的排查和处理办法。不管是刚接触 CentOS 7 的新手还是被磁盘告警搞到头大的运维老手都能直接用上。1. 删了文件df -h 却纹丝不动先把“灵异现场”复现出来1.1 在 CentOS 7 上模拟一次“删了但没释放”很多文章喜欢直接讲理论但我建议你先亲手复现一遍因为只有见过现场后面所有机制你才会有体感。登录一台 CentOS 7 测试机最好是刚装完没重要业务的按下面步骤操作。# 造一个 2G 的文件 dd if/dev/zero of/tmp/bigfile bs1M count2048 # 在“当前 shell”里打开这个文件fd 3 指向它 exec 3/tmp/bigfile # 看一眼文件系统和目录统计 df -h / ls -lh /tmp/bigfile du -sh /tmp/bigfile # 删除文件 rm -f /tmp/bigfile # 此时你再查 df -h / ls -lh /tmp/bigfile du -sh /tmp/bigfile注意一个关键点exec 3/tmp/bigfile一定要在当前交互式 shell 里手动执行这样 fd 3 就挂在当前 bash 进程下。执行完rm -f之后你会看到ls已经找不到/tmp/bigfile了du -sh /tmp/bigfile也会报“没有那个文件”但df -h /显示的已用空间几乎一点没变。如果你在同一个 shell 里执行exec 3-也就是把 fd 3 关闭再执行df -h /会看到空间突然“回来了”。这个对比就是整篇文章的核心谜题目录项已经没了为什么数据块还占着为什么关闭文件描述符之后才释放1.2 df 和 du 查的根本不是同一份账出现这种现场第一个要搞清楚的就是df和du的统计口径完全不同这也是绝大多数误会的第一步。df -h直接读取文件系统超级块里的统计信息通过statfs(2)这类系统调用拿f_blocks、f_bfree、f_bavail它关心的是整个分区上“块”的分配情况。换句话说df 看的是文件系统这整块地的总账本谁占了多少块它就记多少块。du -sh则是从指定路径开始一层层遍历目录项把每个目录项所关联的文件的块数累加起来。它关心的是“从目录树还能看到哪些文件”。如果一个文件的目录项已经被删除du 自然就统计不到它哪怕它的数据块还实实在在占着磁盘。打个比方df 是物业在统计整个停车场的车位占用率du 是保安拿着门牌号挨个核对“这辆车对应哪个车位”。结果有辆车没有门牌了但车子还停在车位里没开走保安的登记表上找不到它可物业那边车位占用率依然是满的。1.3 文件系统层的“记账单位”super_block 与 statfs这里要稍微深入一点因为理解了“记账单位”才能理解为什么 df 会一直显示满。Linux 里每个文件系统实例无论 ext4、XFS都会在内存里维护一个super_block结构它相当于整个文件系统的全局控制块。df拿到的块总数、空闲块数都来自这里。文件数据最终是落在块设备上数据块要么被某个 inode 引用着要么在空闲链表里等待分配。当rm -f删掉文件时inode 里的块映射没有被回收超级块统计的“已分配块”自然就不会下降。所以别怪 df 不刷新它记录的本来就是文件系统底层块分配的真实状态。问题只在于那些数据块明明已经“没主”了为什么文件系统还不把它们标记成空闲2. 从 rm 到数据块释放中间隔着一整条 VFS 引用链2.1 dentry、inode、file文件对象的三层身份要把这个问题讲透得先认识 VFS虚拟文件系统层里的三个核心对象。我平时给团队讲的时候喜欢叫它们“三层身份”目录项、索引节点、打开文件描述。第一层是dentrydirectory entry目录项。它负责把路径和文件关联起来比如/tmp/bigfile这个路径在内核里就对应一个 dentry。dentry 缓存了路径到 inode 的映射关系目录树遍历、路径解析靠的都是它。第二层是inode。它是文件的“身份证”记录元数据文件大小、权限、时间戳、数据块位置还有一个关键字段i_nlink也就是硬链接数。一个文件可以有多个硬链接每个硬链接本质上就是多个 dentry 指向同一个 inode。第三层是file。进程每open(2)一次文件内核就创建一个文件对象它记录当前读写位置、打开模式、引用计数等信息并指向对应的 dentry 和 inode。进程的文件描述符表里的 fd 编号最终会映射到这个 file 结构上。它们的关系可以粗略理解成fd - file - dentry - inode - 磁盘数据块。任何一层还“拽着”下一层下一层的资源就不会被回收。2.2 unlink 只是“撕门牌”inode 还等着最后一个引用归零你执行rm -f /tmp/bigfile的时候实际调用的是unlink(2)系统调用。这个名字起得很准确它不是“销毁文件”而是“解除链接”。内核会把/tmp/bigfile对应的 dentry 从父目录里摘掉然后把 inode 的i_nlink减 1。如果i_nlink减完之后是 0说明已经没有任何目录项指向这个 inode 了这扇“门牌号”彻底被撕了。但注意只是“门牌被撕”不代表房子马上被拆。inode 还有另一个引用计数i_count它表示当前有多少内核对象正在使用这个 inode比如 file 结构、dentry 引用、page cache 引用等。回到我们刚才的现场exec 3/tmp/bigfile让当前 shell 进程持有 fd 3这个 fd 对应一个 file 结构file 结构又引用了 inode。所以rm执行完i_nlink归零但i_count依然大于 0inode 只能进入“已删除但暂未释放”的僵尸状态。2.3 关键判定条件i_nlink 和 i_count 同时归零才会真正释放那到底什么时候磁盘块才真正释放内核里的判定条件很明确当文件最后一次iput(2)时如果发现i_nlink 0 i_count 0才会真正销毁 inode释放它对应的全部数据块并更新超级块的空闲块统计。所以磁盘空间释放的完整链路是目录项删除i_nlink--→ 最后一个打开引用关闭i_count--→ 触发 inode 销毁 → 释放块映射 → 更新文件系统统计。只要i_count还大于 0系统就认为这个“僵尸 inode”还在被使用数据块必须继续保留。还有个常见变体容易让人懵硬链接。如果/tmp/bigfile还有一个硬链接/tmp/bigfile2那么 inode 的i_nlink是 2。你删掉其中一个i_nlink变成 1文件内容依然完整du 还能在另一个路径下看到它。只有当所有硬链接都被删除i_nlink归零且没有进程打开着它空间才会释放。这就是为什么有些人删了一个文件空间没变一查才发现还有别的硬链接。3. 定位“看不见的持有者”lsof、/proc 与 fuser 的实战排查3.1 lsof L1 一条命令看全部 deleted 文件既然问题出在进程还拿着已经删除文件的 fd那排查思路就很直接找出哪些进程持有已删除文件。CentOS 7 上最顺手的工具就是lsof。lsof L1L1的意思是列出 link count 小于 1 的文件也就是已删除但依然被进程打开的文件。如果系统没装 lsof先装一下yum install -y lsof执行结果大致长这样COMMAND PID USER FD TYPE DEVICE SIZE/OFF NODE NAME bash 2332 root 3w REG 253,1 2147483648 123456 /tmp/bigfile (deleted)这里面每一列都很关键COMMAND和PID告诉你哪个进程在持有文件FD是文件描述符编号3w表示 fd 3 以可写方式打开SIZE/OFF能看到这个被删文件当前的大小NAME末尾的(deleted)是内核给出的明确标记。实战中我经常这么组合lsof L1 | grep deleted | sort -k7 -rh | head -20按文件大小排个序优先处理占空间最大的几个。几十个 deleted 文件里往往就一个大文件贡献了绝大部分占用。3.2 没有 lsof 时直接翻 /proc/*/fd有些精简环境不让装额外包或者你想快速手工确认可以直接翻/proc伪文件系统。每个进程的/proc/PID/fd目录下列出了它所有打开的文件描述符符号链接指向对应文件路径。如果文件已删除链接内容会带上(deleted)后缀。ls -l /proc/*/fd 2/dev/null | grep deleted这条命令的原理是遍历所有进程的 fd 目录把指向已删除文件的符号链接筛出来。看到结果后再用ps -fp PID确认进程身份用这条命令查看具体路径readlink /proc/2332/fd/3输出会是/tmp/bigfile (deleted)确认无误。/proc方案的好处是零依赖基本所有 Linux 发行版都通用排查线上问题时更抗造。还有一种情况是文件本身没删干净还剩一个目录项可见但你需要确认谁在占用某个具体路径这时用fuserfuser -v /tmp/bigfile fuser -k /tmp/bigfile不过对于已经 deleted 的文件fuser通常派不上用场因为路径已经不在了。真正的主力还是lsof L1和/proc/*/fd。3.3 确认持有者身份后先判断进程能不能动找到进程只是第一步怎么处理才是关键。拿到 PID 之后我强烈建议先别急着kill而是看一眼这个进程是什么来头。ps -fp 2332如果只是你自己调试时随手开的tail、grep、测试脚本直接处理掉没任何心理负担。但如果是 Tomcat、Nginx、数据库进程甚至是一些没有名字的僵尸子进程就要谨慎了。尤其是通过FD列看到1w、2w说明它占的是标准输出和标准错误很可能是某个后台服务把日志输出重定向到了这个文件进程本身正在持续写入。这时候贸然kill -9业务可就断了。4. 空间释放的三种操作路径kill、截断、让进程自己放掉4.1 直接终止进程最快但不一定最优雅最直接的处理方式就是让进程退出进程一旦退出它持有的所有 fd 都会被内核自动关闭inode 的i_count归零空间立即释放。刚才那个测试场景里执行exec 3-关闭 fd或者直接退出那个 shell效果都一样。生产环境里推荐先用kill发 SIGTERM给进程一个优雅退出的机会。比如 Nginx、Tomcat 这类服务通常都能在退出时做收尾工作。如果等了几分钟空间还是不释放或者进程完全卡死再考虑kill -9。kill 2332 # 等待进程退出 ps -p 2332 # 如果还在 kill -9 2332但这里有个很隐蔽的坑有些进程是父子结构主进程死了子进程还在而且子进程恰好继承并持有了那个 fd。所以处理完一个 PID 后一定要再跑一遍lsof L1确认没有残留的进程继续持有 deleted 文件。我处理过不少“kill 完还占用”的情况最后查出来都是某个子进程一直攥着没放手。4.2 不重启进程通过 /proc/PID/fd/N 把文件截断如果持有文件的是核心业务进程不能随便重启还有一个偷懒但有效的办法直接通过/proc/PID/fd/N路径截断那个已删除的文件。: /proc/2332/fd/3或者用更明确的命令truncate -s 0 /proc/2332/fd/3这个操作的原理是/proc/PID/fd/N是那个 fd 对应文件的“通路”即使目录项已删除inode 还活着重定向写入它相当于对原文件做O_TRUNC文件大小被清零inode 的块映射被释放df 自然会降下来。但这个方案有前提也有副作用。前提是 fd 当初必须是以可写方式打开的如果原 fd 是r只读截断操作会失败。副作用更值得注意进程当前文件读写位置offset并不会因为 truncate 而重置。假设进程已经写到了第 100MB 的位置你把它截断成 0 后进程再继续写日志会从原来的 100MB 偏移量开始写文件会瞬间变成一个带空洞的稀疏文件du看到的大小甚至可能比原来还大。所以截断后如果空间又涨回来并不意外。这个方案更适合临时救急最理想的处理还是让进程自己重新打开日志文件或者干脆重启服务。4.3 日志轮转与重开日志的正确姿势大多数“删除文件不释放空间”的现场本质都是日志文件被运行中的进程长时间占着。与其出了问题手忙脚乱不如从源头把日志轮转做好。CentOS 7 上标准方案是logrotate。以 Nginx 为例比较稳妥的配置是这样/var/log/nginx/*.log { daily rotate 7 missingok compress delaycompress notifempty create 0640 nginx adm sharedscripts postrotate [ -f /var/run/nginx.pid ] kill -USR1 cat /var/run/nginx.pid endscript }关键点在postrotate里那条kill -USR1。像 Nginx、rsyslog 这类程序收到特定信号后会主动关闭旧日志文件并重新打开新文件这样进程持有的 fd 就从旧文件切到了新文件旧 inode 的i_count归零空间立刻释放。如果你的业务进程不支持信号重开日志logrotate 还有个copytruncate选项先复制一份当前日志再把原文件截断为 0。这个方案不需要进程配合适合 Java 这类不监听日志重开信号的服务。缺点是复制和截断之间存在极小的时间窗口可能丢失几百条日志但如果业务能接受这点损耗它比每天重启服务靠谱得多。/var/log/myapp/app.log { daily rotate 14 copytruncate compress missingok notifempty }反正我自己的原则是能用信号重开日志的进程坚决不用 copytruncate不能用信号的才退而求其次。逐个排查不要无脑重启。4.4 特殊场景清空 docker 容器日志如果你在跑 Docker这类“空间不释放”还有一个很常见的变种/var/lib/docker/containers/容器ID/容器ID-json.log这个文件被 dockerd 主进程持有。你直接在宿主机上rm -f它空间并不会释放因为 dockerd 还开着这个文件的 fd。标准处理姿势是截断而不是删除truncate -s 0 /var/lib/docker/containers/*/*-json.log更彻底的办法是给 docker daemon 配置日志大小上限在/etc/docker/daemon.json里加{ log-driver: json-file, log-opts: { max-size: 50m, max-file: 3 } }这样单个容器日志超过 50MB 就会自动轮转保留最近 3 个文件。配好之后重启 docker 生效之后基本不用再手工清日志了。5. 容易被误判成“没释放”的几个场景inode 耗尽、数据库与虚拟磁盘5.1 inode 耗尽空间没满但文件根本建不出来删除文件不释放空间还有一种“非典型”表现不是空间不释放而是 inode 全部耗光你连新建文件都报 “No space left on device”但df -h一看明明还有几个 GB。这种时候要看df -idf -i /如果 IUse% 已经到 100%说明分区里塞满了海量小文件每个文件消耗一个 inode。常见来源是邮件队列、PHP session、临时缓存目录、监控系统的小文件写爆等。清理思路是大规模删除小文件但要注意删除速度。比如find /var/spool/xxx -type f -mtime 7 -delete如果文件量达到几十万上百万也可以写个脚本分批删避免一次find卡死 I/O。还有一种隐蔽情况被进程打开着但没有释放的 deleted 文件同样会占用 inode。也就是说即使你处理的是“inode 耗尽”也有必要跑一遍lsof L1看看是不是有大量已删除文件被某个进程攥着。5.2 应用层“逻辑删除”数据库表空间不收缩还有一种误判来自数据库。业务方说“我把表里数据删了几百 GB磁盘怎么没降”这时候千万别直接怀疑文件系统先搞清楚数据库是不是把空间还给操作系统了。以 MySQL InnoDB 为例默认情况下一个表的数据存在.ibd文件里。执行DELETE FROM table后InnoDB 只是把这些行标记为已删除并把它们所在的页标记为可复用文件本身不会缩小。这是数据库自己的空间管理机制跟 VFS 引用计数没有任何关系。要让文件系统层面的空间真正降下来通常要重建表OPTIMIZE TABLE your_table;或者用ALTER TABLE ... ENGINEInnoDB的方式重建。PostgreSQL 对应的操作是VACUUM FULL。这类问题容易和“deleted open file”问题表面撞车但排查路径完全不同一个是看数据库内部统计一个是看操作系统lsof别搞混。5.3 ext4/XFS 的预留块、延迟分配与虚拟磁盘不缩容除了数据库还有几类系统层面的“假性不释放”也值得知道。ext4 默认会预留 5% 的块给 root 用户这是为了避免分区一满系统连紧急操作都做不了。你可以用tune2fs -m查看和调整tune2fs -l /dev/mapper/centos-root | grep Reserved block count如果磁盘本来就逼近极限这 5% 预留会让普通用户感觉“明明删了文件空间怎么还是满的”。另外 XFS 这类文件系统可能存在延迟分配和后台回收机制删除大量文件后df 不会立刻弹出空间有时过几分钟才会看到变化。处理完问题之后不要马上截图下结论等一会儿再确认。最后是虚拟化场景VMware 的 vmdk 或 KVM 的 qcow2 磁盘镜像客户机里删文件只意味着客户机文件系统释放空间宿主机上的镜像文件并不会自动变瘦。只有在客户机里执行fstrim并启用虚拟磁盘收缩功能宿主机镜像才可能缩小。这不是 Linux 文件系统的锅但它是“删了文件空间不释放”这个话题下最常被拉出来讨论的兄弟问题。6. 最后补充一条我的日常排查习惯我在生产环境里处理过很多次磁盘告警经验沉淀下来其实就一套固定动作。磁盘一告警先df -h看哪个分区满然后立刻跑lsof L1 | grep deleted优先处理占空间最大的 deleted 文件。确认完进程身份再决定是 kill、截断还是让进程重开日志。处理完后再跑一遍df -h和lsof L1确认没有残留再向业务方反馈。还有个小技巧分享给你对于常见日志路径我会提前写好一个轮转配置并且每周用 cron 巡检一次 deleted 文件数量。比如这样一行超过 50 个就告警lsof L1 2/dev/null | grep -c deleted空间不是删了就会立刻回来要等 inode 和 fd 放行。把这条底层逻辑刻在脑子里遇到多少“删除不释放”的问题都能一眼定位。
返回列表