实战排查 Linux 生产环境磁盘空间占满但 du 查不到大文件(被删文件句柄未释放)深度剖析与线上无损恢复
实战排查 Linux 生产环境磁盘空间占满但 du 查不到大文件(被删文件句柄未释放)深度剖析与线上无损恢复
1. 问题背景
在 Linux 生产服务器的日常运维中,磁盘告警是最常见的故障之一。某天核心业务系统的监控告警系统突然弹出高危预警:某节点 /data 分区使用率达到 98%,可用空间不足 2GB。
按照常规排查流程,运维人员立刻登录服务器执行 df -h 确认磁盘使用情况:
[root@prod-node-01 ~]# df -h /data
Filesystem Size Used Avail Use% Mounted on
/dev/vdb1 200G 196G 4.0G 98% /data
确认分区确实几乎打满。然而当使用 du 命令递归扫描该挂载目录下的所有文件占用时,诡异的一幕出现了:
[root@prod-node-01 ~]# du -sh /data/* | sort -hr | head -n 10
12G /data/app-services
8.5G /data/mysql-data
1.2G /data/nginx-logs
所有子目录与可见文件的总占用加起来不足 25GB,而 df 显示已使用 196GB。剩余的约 170GB 磁盘空间神秘“蒸发”了。
2. 根因剖析:为什么 df 与 du 的统计结果不一致?
要理解这一现象,必须从 Linux 虚拟文件系统(VFS)的底层存储机制说起。
在 Linux 文件系统中,一个文件的存储由两部分构成:
- inode(索引节点):存储文件的元数据信息,包括文件属主、权限、创建时间、文件大小以及指向数据块(data block)的指针列表。同时 inode 内部维护着一个硬链接计数器
i_nlink。 - dentry(目录项):包含文件名与对应 inode 编号的映射关系。
当我们执行 rm -f /data/logfile.log 时,内核执行的实质是系统调用 unlink()。该调用仅仅是从父目录的 dentry 树中移除了该文件名,并将对应 inode 的 i_nlink 计数减 1。
关键在于:磁盘空间的真正回收,必须同时满足两个条件:
- inode 的硬链接计数
i_nlink == 0; - 引用该 inode 的进程打开句柄数(File Descriptor Open Reference Count)
i_count == 0。
如果某个长期运行的后台进程(如 Nginx、Java 应用、Python 脚本或 logstash)在文件被删除前已经打开了该文件,且之后一直没有关闭句柄,那么即使文件名被移除了,该文件的 inode 与数据块依然牢牢驻留在磁盘上。
du命令的统计逻辑:从指定目录树向下递归遍历所有可见的 dentry,将找到的每个文件大小累加。因为被删文件的目录项已经消失,du无法扫描到它,因此统计出的占用偏小。df命令的统计逻辑:直接读取文件系统超级块(Superblock)中记录的空闲块(Free Blocks)与总块数,反映的是磁盘底层的真实分配情况。只要句柄未释放,数据块依然被占用,df就会显示磁盘打满。
3. 定位罪魁祸首:快速揪出持有句柄的进程
面对这种情况,切忌盲目扩容或胡乱清理其他业务数据。我们需要使用 lsof(List Open Files)命令来精准定位持有已删除文件句柄的进程。
3.1 查找特定挂载点下的已删除未释放文件
# 语法:lsof +L1 <挂载点路径>
# +L1 表示只列出硬链接数小于 1(即已被 unlink 删除)的文件
lsof +L1 /data
或者在全系统范围内排查并按占用空间从大到小排序:
lsof | grep '(deleted)' | sort -k7 -nr | head -n 10
输出示例:
COMMAND PID USER FD TYPE DEVICE SIZE/OFF NODE NAME
java 104852 root 42w REG 252,17 182536110080 393220 /data/app/logs/catalina.out (deleted)
从输出结果可以清晰看到:
- COMMAND / PID:
java进程,进程 ID 为104852。 - FD:
42w,表示文件描述符为 42,以写入模式打开。 - SIZE/OFF:占用空间约为 182GB!
- NAME:
/data/app/logs/catalina.out (deleted),已被删除但句柄未关闭。
4. 线上优雅治理:无损在线释放空间
找到占用源后,常规但粗暴的做法是直接重启或强杀业务进程(kill -9 104852),这样内核在回收进程资源时会自动关闭其持有的所有句柄,空间即可瞬间释放。
但在核心生产环境中,重启可能导致正在执行的事务中断、长连接断开或服务抖动。如何在不重启、不中断进程的前提下在线释放空间?
4.1 方案:利用 /proc 伪文件系统进行在线截断(推荐)
Linux 的 /proc/<PID>/fd/ 目录下暴露了该进程打开的所有文件描述符的软链接。即使磁盘上的原始文件名已删除,内核依然允许通过 /proc/<PID>/fd/<FD> 访问并操作对应的数据块。
我们可以通过重定向清空该描述符:
# 检查该文件描述符确实对应目标文件
ls -l /proc/104852/fd/42
# 使用空内容截断(清空)该文件
: > /proc/104852/fd/42
# 或者
cat /dev/null > /proc/104852/fd/42
执行后立即复查磁盘:
df -h /data
此时会发现已用空间瞬间从 196GB 降回 14GB,可用空间恢复正常,且 Java 业务进程完全没有中断,依然正常运行并继续向文件追加新写入的日志。
5. 生产避坑与长效防范机制
线上应急解决后,必须建立制度与自动化配置,从根本上杜绝该问题再次发生:
5.1 正确的日志清理与轮转规范
- 严禁直接
rm正在写入的活跃日志:
对于运行中进程正在写入的大日志文件,若急需释放空间,严禁直接执行rm file.log,而应执行截断:> /data/logs/service.log - 规范配置 logrotate:
在配置日志切分时,若应用程序不支持通过信号重新打开日志文件,必须在logrotate配置中声明copytruncate指令:/data/app/logs/*.log { daily rotate 7 missingok notifempty compress copytruncate create 0640 appuser appuser }copytruncate会先将当前日志复制为备份,随后把原文件截断为 0 字节,从而保证原进程的文件句柄不失效,避免产生(deleted)孤儿文件。
5.2 建立自动化巡检与监控机制
在服务器的日常监控或 Node Exporter 巡检脚本中,可增加未释放大文件句柄的采集指标,提前预防磁盘隐性耗尽:
# 监控脚本示例:统计全机 deleted 文件总占用(单位 MB)
DELETED_SIZE_MB=$(lsof | grep '(deleted)' | awk '{sum += $7} END {print int(sum/1024/1024)}')
if [ "$DELETED_SIZE_MB" -gt 5120 ]; then
echo "WARNING: Deleted files holding over 5GB disk space: ${DELETED_SIZE_MB}MB"
fi
6. 总结
df 与 du 统计不一致是 Linux 存储设计中 inode 与引用计数的必然体现。遇到磁盘虚假打满时,牢记三步定位排查法:
- 查:
lsof +L1或lsof | grep '(deleted)'定位持有被删文件的 PID 与 FD; - 消:优先使用
: > /proc/<PID>/fd/<FD>在线清空,避免直接重启业务; - 防:统一引入
logrotate copytruncate机制,杜绝手动rm活跃日志。
- 点赞
- 收藏
- 关注作者
评论(0)