Linux 删除文件后磁盘仍满:用 df、du 和 lsof 排查
删除大日志后,磁盘空间不一定马上释放。有时 df -h 明明还有剩余容量,程序却报 No space left on device。本文按顺序区分三种情况:数据块占满、inode 耗尽,以及文件已删除但进程仍然打开它。
命令适用于使用 GNU coreutils 的 Linux 主机。请检查实际写入失败的路径,并在与应用相同的主机或容器挂载命名空间中执行。
1. 安装排查工具
Debian 或 Ubuntu 可安装缺少的工具:
sudo apt-get update
sudo apt-get install lsof util-linux coreutils python3
如果目标文件系统已经满了,先使用现有工具;安装软件包也需要空间。其他发行版可通过各自的软件包管理器安装这些工具。Python 只用于后面的可选演示。
df --help
du --help
findmnt --help
lsof -h
2. 确定出问题的文件系统
在同一个 shell 中,把 target 设为写入失败位置所在的已有目录。这里以 /var 为例:
target=/var
findmnt -T "$target" -o TARGET,SOURCE,FSTYPE,OPTIONS
df -hT "$target"
df -i "$target"
findmnt -T 查找包含该路径的文件系统。独立的 /var、容器 overlay 或网络挂载都要单独检查,只看 / 可能查错位置。
如果预期的外接硬盘没有出现在挂载结果中,先参考 Linux 外接硬盘识别与挂载排查确认设备和挂载点,再继续检查容量。
结合两份 df 输出判断:
| 现象 | 下一步 |
|---|---|
数据块 Use% 接近 100% |
查找占用空间的数据和已删除但仍打开的文件。 |
IUse% 接近 100%,可用 inode 很少 |
查找含有大量小文件的目录。 |
| 两者都有余量 | 检查应用的实际路径、挂载命名空间和配额。 |
df 显示文件系统统计信息,-i 切换到 inode 用量。对于存在固定 inode 容量限制的文件系统,即使还有数据块,也可能无法创建新文件。除了四舍五入后的百分比,还要看 Avail 和 IFree 的实际值。
3. 定位占用空间的目录
从上一步找到的挂载点开始:
mountpoint=$(findmnt -n -o TARGET -T "$target")
sudo du -xhd1 "$mountpoint" | sort -h
GNU du 遍历路径可达的文件。-x 限制在同一文件系统内,-d1 限制输出的目录深度。inode 紧张时使用 --inodes。接着进入占用较大的目录,例如:
sudo du -xhd1 /var | sort -h
sudo du --inodes -x -d1 /var | sort -n
du 总量小、df 用量大只是线索,不能直接断定有已删除文件。权限错误、被挂载点遮住的数据、文件系统元数据和快照也会影响比较。保留扫描错误信息。服务器繁忙时,可在任务稳定后重新测量;这些命令不会读取同一份一致性快照。
如果只是正常数据增长,先确认最大目录属于哪个应用,再使用它的保留或清理流程。大量小文件则应检查应用的缓存、队列保留策略。不要直接手工删除数据库或容器存储目录中的文件。
如果是使用 Yum 的旧系统,且已经定位到 /boot 旧内核或 Yum 缓存占用,可继续参考 Yum 更新时空间不足的处理方法。
4. 查找已删除但仍打开的文件
sudo lsof -nP +L1
lsof +L1 筛选链接数小于 1 的打开文件。关注普通文件(TYPE 为 REG)、PID、FD、SIZE/OFF,以及以 (deleted) 结尾的名称。-nP 关闭地址和端口名称解析。
多个进程或描述符可能同时持有同一个文件,因此不要把每一行的大小直接相加。SIZE/OFF 也不是已分配数据块的大小,稀疏文件尤其需要区分。
删除文件名会移除目录项。如果最后一个链接已经删除,但仍有进程打开该文件,Linux 会保留它,直到最后一个打开的文件描述符关闭。这是 unlink(2) 说明的行为。
操作前先确认所属服务。如果已经确认是被删除的 Nginx 日志,使用正在运行的实例对应的程序和配置重新打开日志:
sudo nginx -t && sudo nginx -s reopen
sudo lsof -nP +L1
df -hT /var/log/nginx
Nginx 的日志轮转文档说明,重新打开日志可以释放旧日志句柄。若实例使用自定义配置,应传入一致的 -c 参数,以及运行时使用的 -p 参数。重新打开失败时查看错误日志。其他服务应使用自己的日志重新打开机制,或安排重启所属服务;通用的 reload 并不保证关闭文件句柄。
不要为了快速释放空间而截断任意 /proc/PID/fd/FD,它可能指向业务数据。恢复后还要修正日志轮转配置,让应用在轮转后重新打开日志。
5. 用 8 MiB 文件复现问题
在临时目录至少还有 8 MiB 可用空间的 Linux 测试机上运行。示例只创建并删除自己的临时文件,检查当前 Python 进程,最后自动关闭文件:
python3 - <<'PY'
import os
from pathlib import Path
import subprocess
import tempfile
with tempfile.TemporaryDirectory(prefix="deleted-file-demo-") as directory:
path = Path(directory) / "demo.log"
with path.open("w+b") as handle:
for _ in range(8):
handle.write(b"x" * 1024 * 1024)
handle.flush()
os.fsync(handle.fileno())
path.unlink()
info = os.fstat(handle.fileno())
print(f"Path exists: {path.exists()}", flush=True)
print(f"Link count: {info.st_nlink}", flush=True)
print(f"Open file bytes: {info.st_size}", flush=True)
print(f"Allocated bytes: {info.st_blocks * 512}", flush=True)
subprocess.run([
"lsof", "-nP", "-a", "-p", str(os.getpid()), "+L1"
], check=True)
handle.seek(0)
assert handle.read(4) == b"xxxx"
print("Closed: the demo no longer holds the deleted file")
PY
开头输出为:
Path exists: False
Link count: 0
Open file bytes: 8388608
随后会输出已分配字节数和一行 lsof 记录。实际分配量取决于文件系统,路径应以 (deleted) 结尾。断言验证了进程仍能读取已删除文件,最后一行表示演示已经关闭句柄。
示例已在 Linux 的 Python 3.11.4 上测试。8 MiB 不一定会改变 df -h 四舍五入后的百分比;文件描述符和零链接数能直接展示这个机制。如果沙箱限制进程检查,请在普通主机 shell 中运行。
6. 验证恢复并排查剩余问题
在设置过 target 的 shell 中重新检查:
df -hT "$target"
df -i "$target"
sudo lsof -nP +L1
确认相关的已删除文件记录消失,可用空间或可用 inode 按预期恢复,并让应用重试原来的写入操作。lsof +L1 没有匹配项时可能返回退出码 1,应结合标准错误输出判断是否真的检查失败。
| 剩余现象 | 具体处理 |
|---|---|
du 提示权限不足 |
使用足够权限重新扫描,不要丢弃标准错误。 |
lsof 无法检查进程 |
在有适当权限的主机环境运行,并检查 /proc 访问限制。 |
| 没有已删除文件,但差距仍很大 | 用 findmnt 检查挂载,使用文件系统专用工具检查快照和空间分配。 |
| 有空间但写入仍失败 | 记录准确错误和路径,检查用户或项目配额以及挂载选项。 |
| 刚释放的空间很快再次占满 | 修正产生数据的任务或日志保留策略,在正常流量下复查增长。 |
如果容量已经恢复,但网站仍返回 5xx,可以接着用 rg 和 Python 分析 Nginx 5xx 日志,按时间、URL 和状态码缩小故障范围。
7. 小结
先找到写入失败路径所在的文件系统,再区分数据块和 inode 是否紧张。用 du 查找路径可达的数据,用 lsof +L1 查找已删除但仍打开的文件。通过所属服务支持的方式释放句柄后,同时验证可用容量和应用写入是否恢复。
- 原文作者:春江暮客
- 原文链接:https://www.bobobk.com/linux-disk-full-deleted-files.html
- 版权声明:本作品采用 知识共享署名-非商业性使用-禁止演绎 4.0 国际许可协议 进行许可,非商业转载请注明出处(作者,原文链接),商业转载请联系作者获得授权。