Linux 磁盘空间不释放问题排查(lsof 定位已删除仍被占用文件)
问题现象
生产环境中经常遇到这类场景:
df -h看到某分区磁盘使用率已经很高,甚至 100%。- 用
du -sh 目录逐层统计,却发现目录实际占用空间远小于df显示的使用量。 - 之前用
rm -f或日志清理脚本删除过大文件,但磁盘空间没有释放。
这种”删了文件空间却没回来”的情况,最常见原因是:文件虽然被删除,但仍有进程持有该文件的文件描述符(fd),内核不会真正回收其占用的磁盘块,直到所有引用都释放。
根因
Linux 文件系统中,文件通过 inode 管理磁盘块。rm 命令实际上是减少文件的硬链接计数(link count)。当硬链接计数降到 0,且没有任何进程打开该文件(文件描述符引用数为 0)时,内核才会释放 inode 及其占用的磁盘块。
如果进程还打开着这个文件(比如 Tomcat 的 catalina.out、Nginx 的 access log、业务应用的日志句柄),即使文件在目录树中已经不可见,磁盘块仍然被占用。
排查步骤
1. 确认磁盘空间确实紧张
df -h
关注点:Use% 接近或达到 100% 的分区。
2. 定位是哪些大文件/目录在占用空间
# 查看当前目录下各文件/目录占用
du -sh * 2>/dev/null | sort -rh | head -20
# 或从根目录开始排查某个挂载点
du -sh /path/* 2>/dev/null | sort -rh | head -20
如果 du 统计出来的大小明显小于 df 显示的使用量,就要怀疑存在”已删除但仍被占用”的文件。
3. 用 lsof 找出已删除但仍被占用的文件
3.1 全局扫描所有进程的 deleted 文件
lsof +L1
+L1 表示只列出 link count 小于 1 的文件,也就是已被删除但仍被进程持有的文件。
3.2 只查看指定挂载点下的 deleted 文件
lsof +L1 /path
例如只查 /work_app 分区:
lsof +L1 /work_app
3.3 查看指定进程打开的 deleted 文件
如果已经知道可疑进程,比如 Tomcat:
# 先找到进程 PID
ps -ef | grep java
# 再查看该进程打开的 deleted 文件
lsof -p <PID> | grep delete
示例输出(Tomcat 的 catalina.out 被删除后仍被占用):
java 257281 tomcat 1w REG 253,0 80839073792 202612073 /work_app/apache-tomcat-7.0.109_6088/logs/catalina.out (deleted)
java 257281 tomcat 2w REG 253,0 80839073792 202612073 /work_app/apache-tomcat-7.0.109_6088/logs/catalina.out (deleted)
字段说明:
PID:进程 ID(如257281)。USER:进程用户(如tomcat)。FD:文件描述符,1w表示标准输出(stdout)写模式,2w表示标准错误(stderr)写模式。TYPE:文件类型,REG表示普通文件。DEVICE:设备号。SIZE/OFF:文件大小,这里就是占用磁盘空间的字节数。NODE:inode 号。NAME:文件路径,末尾(deleted)表示文件已被删除。
从示例可以看到,catalina.out 被删除后仍然被 Tomcat 进程以 1w 和 2w 两个 fd 持有,大小约 80 GB。
4. 确认空间占用与进程的对应关系
如果 lsof +L1 列出的 deleted 文件很多,可以按大小排序快速定位大头:
lsof +L1 | awk '{print $7, $1, $2, $NF}' | sort -rn | head -20
注意:
lsof输出列数不固定,严格排序建议用lsof -F指定输出格式,或结合du -x排查。
解决方案
释放空间的关键是:让持有 deleted 文件的进程关闭文件句柄。常见做法有三种,按风险从低到高排列:
方案 1:清空/截断仍在写入的文件(推荐,无需重启服务)
如果文件没有被删除,只是日志持续膨胀,可以直接清空文件内容:
# 先确认是哪个 fd 在占用空间
lsof -p <PID> | grep 日志文件名
# 通过 /proc/<PID>/fd/<fd_num> 截断文件
# : > /proc/<PID>/fd/<fd_num>
echo "" > /proc/<PID>/fd/<fd_num>
例如:
: > /proc/257281/fd/1
这种写法不会删除 inode,只是把文件内容清空,进程可以继续往同一个 fd 写入,不需要重启服务。
不要直接
rm -f后再用>重定向,因为进程仍然持有旧的 deleted inode,新建的文件是另一个 inode,旧空间依然占着。
更安全的做法是使用 logrotate 的 copytruncate 机制:先复制原日志,再截断原文件,无需重启进程。
方案 2:重启占用进程
如果文件已经被删除且进程不需要保留当前日志流,可以重启进程释放 fd:
# 找到服务启动脚本或 systemd 服务
kill -9 <PID>
# 然后重新启动服务
以 Tomcat 为例:
# 切换到应用用户(避免 root 启动导致权限问题)
su - tomcat
cd /work_app/apache-tomcat-7.0.109_6088/bin/
# 先停止(或 kill)
kill -9 257281
# 再启动
sh startup.sh
注意:
-9是强制终止,可能导致正在处理的请求中断。对于在线业务,建议先用正常停止命令(如shutdown.sh或systemctl stop),等待进程优雅退出;只有正常退出失败时才使用kill -9。
预防措施
- 日志按大小/日期轮转
- 使用
logrotate配置按天或按大小切割日志,启用copytruncate或配合应用重启策略。 - Java 应用推荐配置
logging.properties或logback/log4j2的滚动策略,避免单文件无限增长。
- 使用
- 禁止直接
rm -f删除正在写入的日志- 清理前先
lsof确认是否有进程持有该文件。 - 对于运行中的服务,优先使用截断或轮转方式,而不是直接删除。
- 清理前先
- 监控与告警
- 对磁盘使用率设置告警阈值(如 80%、90%)。
- 对关键目录的
du与df差异设置监控,及时发现 deleted 文件泄漏。
- 应用日志输出规范
- 避免把 stdout/stderr 直接重定向到固定大文件;使用日志框架自带的滚动策略。
- 容器场景下,注意 Docker daemon 日志(
/var/lib/docker/containers/...)也可能因容器未重启而占用已删除日志空间。
常用命令速查
| 场景 | 命令 |
|---|---|
| 查看磁盘空间 | df -h |
| 查看目录大小 | du -sh /path |
| 查看所有 deleted 文件 | lsof +L1 |
| 查看指定目录下的 deleted 文件 | lsof +L1 /path |
| 查看指定进程的 deleted 文件 | lsof -p <PID> \| grep delete |
| 截断进程 fd 对应的文件 | : > /proc/<PID>/fd/<fd_num> |
| 按大小排序 deleted 文件 | lsof +L1 \| awk '{print $7, $1, $2, $NF}' \| sort -rn \| head |
总结
“文件删除空间不释放”本质上是进程还持有被删文件的描述符。排查三板斧:
df -h确认空间紧张。du -sh与df对比,怀疑 deleted 文件泄漏。lsof +L1或lsof -p <PID> | grep delete定位占用进程和 fd。
处理时优先选择截断文件或优雅重启进程;强制 kill -9 只作为兜底手段。配合日志轮转和监控告警,可以从根本上避免这类问题反复出现。