ARTICLE DETAIL

资讯详情

深耕编程入门与网站建设的一线实战洞察。

Linux磁盘空间未释放?用lsof揪出已删除但被占用的文件

Linux磁盘空间未释放?用lsof揪出已删除但被占用的文件 登录服务器准备清理磁盘习惯性敲了一句rm -rf /data/logs/xxx.log看文件确实没了再跑df -h一看根分区还是红到发紫的 98%。这种场景干运维的应该都不陌生。更让人血压升高的还在后头——你反复检查确认文件真的删干净了空间纹丝不动群里同事还追问“要不要重启一下服务器”。先给个明确结论八成不用重启而且不建议一上来就重启。重启确实能解决这个问题但属于典型的“用大炮打蚊子”还会把线上服务的可用性赔进去。这篇文章我把自己多年排查这类问题的一套思路完整写下来从原因分析到命令实操从误判排查到避坑清单尽量让你看完之后能自己动手解决而不是两手一摊去申请重启窗口。1. 先搞清楚你看到的“空间没释放”可能只是表象1.1 df 和 du 的“账本”不一样很多人看到“文件删了但磁盘空间没回来”第一反应是系统出问题了其实大多数时候不是 bug而是df和du这两条命令的统计口径不一样。du是从文件系统层面去累加文件占用的块它沿着目录树走遇到文件就统计文件大小遇到目录就递归进去继续统计。而df看的是文件系统整体的块设备使用情况它统计的是这个分区上所有已经被占用的物理块不管是哪个目录、哪个文件占的也不管这个文件是否还被“活着的进程”引用。举个可能不太恰当但很贴切的类比删除文件相当于把目录里的“条目标签”撕掉了你从书架外面看这本书确实不在列表里了。但如果有人正捧着这本书在读书页上的内容其实还摊在人手上书并没有真正回到书库的架位上。du只统计书架上能看到的书所以删掉之后它显示的空间就少了df统计的是整个书库的物理空间有人手上的书没还回来这部分空间就始终被算作“占用”。所以在排查的时候第一步不是急着找“罪魁祸首”而是先确定到底是不是“已经删除但未被释放”的情况。最典型的特征就是du -sh /路径显示的数据量已经明显减少但df -h显示的使用率纹丝不动且lsof L1能查到一堆标记为deleted的文件如果符合这三点那基本可以断定是资源占用问题而不是删除失败。1.2 删除文件真正做了什么这里要稍微解释一下 Unix/Linux 文件系统的“删除”机制理解了底层原理后面的排查才是有的放矢。在 Linux 上rm命令做的核心事情其实是两步检查权限、移除目录项。它并不负责“擦除磁盘上的数据”也不负责“即刻归还磁盘空间”。真正意义上的空间回收依赖于文件的引用计数变成 0 且没有进程再持有文件描述符。当你执行rm file.log时系统只是把文件所在目录中该文件的“名字”和对应的 inode 之间的关联删掉了同时把 inode 的链接计数减 1。如果此时没有任何进程打开过这个文件链接计数变成 0系统会立刻把这个 inode 标记为可复用对应的数据块也会进入空闲列表空间随即释放。但如果有一个进程早就打开过这个文件那么情况就完全不同了。进程通过文件描述符FD引用这个文件删除目录项只是让“路径名”失效FD 仍然指向这个 inodeinode 的引用计数还大于 0。这时文件虽然看不见了但它的数据块依然被系统占用直到持有 FD 的进程退出或者主动关闭这个 FD空间才会真正归还。这也是“文件删了但空间不回来”这个问题的根本原因所在。2. 最常见的元凶文件被进程“死死攥住”不放手2.1 被占用文件的典型场景根据我自己的排查经验这个问题的“头号嫌疑人”永远是“进程持有了已删除文件的句柄”。而且越是长期运行的服务越容易踩中这个坑。举几个我在生产环境真实遇到过的例子Nginx 或 Apache 的 access.log / error.log很多人删日志是直接rm /var/log/nginx/access.log但 master 进程和 worker 进程其实早就把 log 文件 open 了。这时候你再去nginx -s reopen它会重新创建新的日志文件旧文件的句柄才可能被释放。Java 应用或 Python 进程的 stdout/stderr 重定向如果你用nohup java -jar app.jar app.log 或者类似方式启动服务进程持续往这个 log 文件里写内容。你rm掉这个文件后进程的 FD 依然指向已删除的 inode磁盘空间一直被占用。数据库的 binlog、redo log 或临时文件MySQL、PostgreSQL 这类数据库对文件句柄的管理非常“专一”经常出现删掉 binlog 但主进程还握着句柄的情况。还有 sort buffer、临时排序文件虽然文件名叫#sql_xxx但实际上可能被 mysqld 进程占住了。视频转码、大数据计算等临时大文件像 FFmpeg 转码输出的临时 ts 文件或者 Spark/Flink 运行时产生的 shuffle 文件进程持有 FD 时被清理脚本删掉了空间也会卡住。2.2 用 lsof 精准定位“已删除但仍打开”的文件定位这个问题最趁手的工具是lsof。在排查“删了文件但空间没回来”的场景时我通常会直接敲下面几条命令# 列出所有已删除但仍被进程打开的文件最核心的命令 lsof L1 # 如果只想看某个分区/挂载点下的情况可以加挂载点过滤 lsof L1 /data # 或者用 grep 过滤出带 deleted 标记的行 lsof | grep deleted这里重点解释一下L1这个参数lsof L1表示列出 link count 小于 1 的文件。正常的文件至少会有一个目录项指向它link count 至少为 1当你rm掉文件后如果进程还持有 FD那么 link count 就会变成 0这类文件就是导致空间不释放的“钉子户”。输出的结果大致长这样COMMAND PID USER FD TYPE DEVICE SIZE/OFF NODE NAME java 1234 root 1w REG 253,0 1073741824 98452 /data/app/logs/app.log (deleted) mysqld 2233 mysql 11w REG 253,2 5368709120 98671 /var/lib/mysql/binlog.000012 (deleted)看到NAME列末尾有(deleted)标记的就说明这个文件已经被删除了但是对应 PID 的进程还握着它的文件描述符。SIZE/OFF列还能看到它当前占用的大小比如上面那个 5GB 的 binlog就是把你磁盘空间卡住的真凶。拿到 PID 之后还可以进一步确认到底进程持有哪些 FD# 查看某个进程的所有文件描述符对应的文件 ls -l /proc/1234/fd/ # 结果里会有类似这样的行 # lrwx------ 1 root root 64 Mar 1 10:00 1 - /data/app/logs/app.log (deleted)2.3 解决手段不要急着 reboot先 try “软重启”定位到具体进程之后空间能不能释放取决于这个进程能不能“放开”文件句柄。最理想的情况是进程支持“重新打开日志文件”的信号或命令。比如 Nginx 就可以通过kill -USR1 nginx_pid或者nginx -s reopen来重开日志文件这样旧文件的 FD 会被关闭空间自然就释放了。类似地syslog 服务可以通过重启rsyslog服务来释放日志文件句柄Apache 也可以apachectl graceful平滑重启。如果进程没有这种机制那就只能重启这个应用进程了。需要注意的是这里说的“重启进程”不等于“重启服务器”。比如是 Java 应用占用了日志句柄直接重启这个 Java 服务就行完全不需要把整台机器 reboot。有老哥可能会问“那我kill -9掉进程行不行”。当然行但如果这个进程是数据库、是核心业务直接 kill 可能会带来数据丢失、连接中断等更严重的故障。所以我的习惯是先尝试优雅重启比如 systemctl restart 、kill 指定信号等实在不行再考虑强杀。而且在 kill 之前先ps -ef | grep PID确认一下这个进程是什么别误杀了别的服务。一个非常反直觉的点是进程重启后空间立刻释放但df的输出往往不会马上刷新。所以耐心等几秒再执行df -h复查别因为“看着还没变”而重复重启。3. 除了进程占用还有几个容易被忽略的“坑”3.1 删了链接但真实数据一点没少这个坑比进程占用还要隐蔽尤其是刚接触 Linux 的同学很容易踩中。很多日志目录或数据处理目录为了省事会做软链接。比如/data/logs可能是一个软链接指向另一个挂载点的目录或者你在某个目录下删了个带符号的链接文件实际上只是删掉了快捷方式真正的数据还在目标路径下躺着。排查方法其实很简单看一眼文件属性就行ls -l /data/logs ls -l /path/to/file如果权限位前面有个l或者文件名后面有- /real/path说明你删的可能只是链接。这时候要用du -sh去检查实际指向的目录不能只看软链路径。3.2 隐藏文件和目录被 du 漏掉du -sh *是大家最常用的排查命令但这个命令有个天然缺陷它不会统计以.开头的隐藏文件和隐藏目录。举个例子如果你的/var/log目录下有个.cache隐藏目录里面躺着一个 20GB 的临时文件执行du -sh /var/log/*的时候它是不会出现的。很多人反复du都找不到大文件在哪最后发现空间就是被隐藏文件吃掉的。所以排查时我会刻意多看一眼隐藏文件# 查看指定目录下所有文件包括隐藏文件的大小 du -sh /var/log/.[!.]* 2/dev/null # 或者更直接一点 du -sh /var/log/* du -sh /var/log/.*3.3 挂载点被覆盖删了个“假的”目录这个坑在云服务器和虚拟机上尤其常见。比如你有个数据盘挂载在/data但是有人不小心在/data目录本身创建了新的目录或文件然后又卸载了数据盘。这时候你看到/data下面还有旧文件直接去删删除的可能只是原来数据盘挂载点上残留的文件并没有真正删到数据盘里的内容。更麻烦的情况是数据盘卸载后挂载点目录里其实还残留着你以前写在“挂载点下面”的旧数据这些数据因为被挂载点“盖住”了平时完全看不到。一旦卸载掉数据盘这些“隐藏的旧文件”就重新浮出水面占用的还是根分区的空间。排查时可以检查一下挂载关系df -h mount | grep data ls -la /data如果发现删除目录后df没有任何变化同时又怀疑挂载点有问题可以先卸载再查看umount /data df -h du -sh /data3.4 inode 耗尽空间还有但写不进去有些场景下“磁盘空间不足”其实根本不是空间块不够而是 inode 用完了。当你删除大量小文件时尤其是缓存目录、邮件队列、消息队列积压的小文件空间可能已经释放了但 inode 还没有被回收出来系统一样会报“No space left on device”。判断方法也很简单一条命令就能看出来df -i如果IUse%那一列已经到 100% 了那你首先要做的是找出哪个目录下的小文件最多然后批量清掉。查找大量小文件可以用# 找出某个目录下文件数量最多的子目录 find /data -maxdepth 2 -type d | while read dir; do echo $(ls -1 $dir | wc -l) $dir; done | sort -rn | head -20删除大量小文件时我习惯用rsync加--delete配合空目录来清空而不是rm -rf一条龙。因为文件数量太大时rm -rf可能执行很久而且一旦中途中断进程卡住不说也可能留下隐患。例如可以这样# 把目标的文件目录清空但保留目录本身 mkdir /tmp/empty_dir rsync -a --delete /tmp/empty_dir/ /data/cache/3.5 文件系统异常导致的 df 输出不准还有一种极端情况就是文件系统本身出现了异常导致df读取的元数据不准确。比如意外断电、强制关机后ext4 或 xfs 文件系统的日志可能需要回放此时df读数可能就是错的。遇到这种情况不要直接重启虽然重启可能触发文件系统自检而是先看下系统日志dmesg | tail -30 journalctl -k -f如果确认文件系统有异常标记建议按顺序做先sync确保数据刷盘再以只读方式检查修复文件系统。注意尽量不要在业务高峰期执行fsck尤其是 xfs 的xfs_repair务必在卸载状态下操作否则有数据损坏风险。4. 实战排查流程一整套命令按顺序打下来前面把原理讲清楚了这一节给出一套可以直接照搬的排查流程。我平时排查这类问题基本按照这个顺序走一遍五分钟内基本能定位到问题。4.1 第一步确认现象排除误判先同时跑这三条命令把现场数据记录下来df -h df -i du -sh / 2/dev/null | sort -hr | head -20这里有一个关键点如果df -h使用率高但du -sh /加起来却比df显示的小很多那基本可以断定是“已删除文件被进程占用”的类型。如果df -i显示 inode 满了则跳到 inode 处理分支。4.2 第二步用 lsof 揪出已删除但被占用的文件接着执行lsof L1如果系统没装 lsofCentOS 可以yum install -y lsofUbuntu 可以用apt install -y lsof。没有 lsof 的情况下也可以直接在/proc文件系统里翻进程的 fd# 遍历所有进程找到带 deleted 标记的 fd for pid in /proc/[0-9]*; do p${pid#/proc/} ls -l /proc/$p/fd 2/dev/null | grep -i deleted echo PID: $p done不过我还是建议安装 lsof它是排查这类问题的标准工具好用很多。找到带(deleted)标记的记录后先看SIZE/OFF列就能知道大概占了多少空间再根据COMMAND和PID判断是哪个服务。4.3 第三步判断该对进程做什么操作根据第二步的结果对症处理如果是日志文件优先尝试平滑重开日志。例如 Nginx 用nginx -s reopenApache 用apachectl gracefulsyslog 用systemctl restart rsyslog。如果是应用临时文件直接重启对应应用。如果是数据库的 binlog 或 redo log不要盲目删除文件后等空间自动释放而是要用数据库自身的清理机制比如 MySQL 的PURGE BINARY LOGS再去考虑重启。如果确实找不到是哪个进程再用fuser确认一下哪个进程占用了特定目录fuser -mv /data/logs4.4 第四步处理完进程后复查空间处理完进程或者重启完应用后别急着走继续复查df -h正常情况下之前卡住的空间会立刻释放。如果只是日志被占用且进程重新打开文件旧句柄释放后可用的空间马上就会涨回来。需要提一句的是df命令有时候会有一点点缓存延迟但通常不会超过几秒。如果两分钟后df还是老样子说明要么还有别的进程占用要么你删的根本不是文件系统上真正占空间的东西。4.5 第五步补充排查大文件和日志轮转策略问题解决之后我建议顺手把根因治理一下避免过两个月又来一次。比如大文件可以用find快速定位# 找出所有大于 100MB 的文件 find / -xdev -type f -size 100M -exec ls -lh {} \;还可以考虑配置 logrotate 来管理日志让日志按大小或日期轮转、压缩、删除而不是用rm硬删。数据库的 binlog 也要设置合理的 expire_logs_days临时目录建议写清理脚本时避免直接rm正在使用中的文件而是结合lsof先判断句柄占用情况。总之删除大文件之前先看一眼有没有进程正在使用这个习惯能帮你省下很多不必要的重启窗口。5. 常见问题速查与避坑经验5.1 问题速查表现象可能原因排查命令解决方向文件删了df 使用率没降文件被进程持有句柄lsof L1重启对应进程或重开日志文件df 显示满但 du 加起来没那么多已删除文件被占用或存在隐藏大文件lsof L1、du -sh .[!.]*定位并释放句柄删除大量小文件后空间依然不足inode 耗尽df -i删除更多小文件定期清理缓存删除了文件但目录还在增长程序仍在写入该文件因为句柄未释放lsof | grep deleted让程序重新打开日志文件df 输出突然异常偏高或偏低文件系统需要修复dmesg、fsck只读方式检查修复文件系统软链接目录下删除文件空间没变删的是链接不是源文件ls -l 查看链接指向删除真实路径下的文件卸载挂载点后空间反而变少了挂载点覆盖了原有内容mount、df -h检查挂载前目录残留重启服务器能解决但不想重启旧进程的句柄占用lsof L1重启对应服务即可5.2 两个非常实用的避坑技巧第一个技巧是处理日志文件的“假删除”时不要简单粗暴地rm日志文件更好的做法是清空文件而不是删除文件# 不删除 inode直接清空内容进程写入的 FD 仍然有效 truncate -s 0 /var/log/app.log # 或者 : /var/log/app.log这样做的优点是文件没有被删除进程不会产生“句柄指向已删除 inode”的问题空间也会立刻释放而且应用不用重启日志文件还能继续写。唯一的隐患是如果应用在文件中有 seek 偏移清空后写入位置可能不准确但大多数日志场景是 append 模式问题不大。第二个技巧是rm -rf执行时如果提示 “Device or resource busy”不要硬来。这不是权限问题也不是路径错误而是文件被占用了。这时候直接回头用lsof L1找占用者把占用者处理好文件自然就能删除或空间自然就能释放。5.3 关于虚拟机磁盘空间不足的提醒如果你在虚拟机里跑 Linux可能会遇到一种“假磁盘空间不足”虚拟机内部df -h释放了但宿主机的虚拟磁盘文件比如 vmdk并没有自动缩小。这是因为虚拟磁盘文件属于稀疏文件删除虚拟机内部的文件只是把块标记为可用但宿主机层面并不会主动去“归还”这些空间需要你在虚拟机内部写入零字节或者使用存储层级的回收机制后再在宿主机上执行磁盘压缩操作。这个跟 Linux 的 rm 释放逻辑是两回事不要搞混了。5.4 一句话经验总结别一遇到空间不释放就想着重启机器。重启解决的永远只是“进程句柄占用”这一类问题但代价是全量业务中断而且下次还会踩同样的坑。先确认 df 和 du 的差异再用 lsof 锁定占用进程最后按进程可控的方式释放句柄这套流程走下来基本可以做到不影响业务。我在实际处理中最深的体会是这类问题十有八九是日志文件被进程占用了尤其那些跑了几百天的老进程。以前我也踩过几次坑删完文件看没释放第一反应就是申请重启窗口后来学会先查 lsof基本几分钟就搞定了根本不用惊动领导和运维审批。建议每个 Linux 服务器的使用者和运维工程师都把这个排查顺序刻在脑子里下次再看到“磁盘满了”先从源头排查再考虑重启。
返回列表