JavaLYG

Linux 删除日志后磁盘空间不释放:用 lsof 与 /proc 定位打开引用

Linux 提示磁盘空间不足,删掉大日志后,du 已经变小,df 却几乎没动。先别继续删,更别上来就重启整台机器。本文针对本地 Linux 文件系统,用 df、lsof 和 /proc 找出“已经删除、仍被进程打开”的文件,区分其他占用原因,再选择能控制影响范围的释放方式。

一、文件名没了,不代表文件已经退场

删除普通文件时,系统首先移除目录中的名字。名字所指向的 inode 如果还有其他硬链接,或者仍有进程持有打开引用,文件的数据就不一定立即回收。日志进程可以继续对原来的描述符写入,并不会因为你执行了删除就自动改写新文件。

这也是 df 与 du 经常“对不上账”的原因:df 看文件系统的整体分配;du 沿目录遍历目前能访问到的文件。被删除的文件没有路径入口,du 可能找不到它,文件系统却仍给它记着空间。两者口径不同。

不过,这只是候选原因,不是看到差值就能确诊。文件系统元数据、快照、保留空间、权限不足及挂载覆盖等,也会影响比较;网络文件系统与容器存储还要结合各自实现判断。

Linux已删除文件占用排查:确认文件系统、定位打开引用、按服务释放、双重验收

二、先确认告警发生在哪个文件系统

从应用报错的目录出发,不要拿根分区的 df 和另一块数据盘的 du 比较。下面命令以 /var/log 为示例,不会删除文件;在繁忙机器上,目录遍历仍可能增加 I/O,先从小范围开始。

# 换成实际发生告警的现存目录
target_dir=/var/log
df -hT "$target_dir"
df -i "$target_dir"
sudo du -x -h --max-depth=1 "$target_dir"
sudo lsof -nP +L1

df 的 -T 显示文件系统类型,-i 切换为 inode 使用情况。块空间还有余量但 inode 耗尽时,新文件照样创建失败;继续追着一个大日志处理,方向就错了。du 的 -x 不跨其他文件系统,但它统计的仍只是给定目录,不代表整个挂载点。

三、用 lsof 找进程,再核对实际分配

lsof 的 +L1 选择链接数小于 1 的打开文件。重点看 TYPE 是否为 REG、PID、FD、DEVICE、NODE 和 NAME,常见线索是 NLINK 为 0,名字末尾有 (deleted)。有结果并不等于它就是告警来源,还要对应到出问题的文件系统。

# 示例 PID、FD 必须替换为上一步核实的数字
target_pid=12345
target_fd=7
sudo lsof -nP -a -p "$target_pid" +L1
sudo readlink "/proc/$target_pid/fd/$target_fd"
sudo stat -L -c '逻辑字节=%s 分配块=%b 块单位=%B' "/proc/$target_pid/fd/$target_fd"

这里 -a 很重要:它让 PID 条件和 +L1 条件相交,避免把本来想限定的查询扩大。FD 显示 7w、7u 时,/proc 路径只填写数字 7,不包含访问模式后缀。

SIZE/OFF 不能一律当成可释放字节。同一 inode 可能由多个进程或多个 FD 打开,直接累加会重复计算;稀疏文件的逻辑长度也可能远大于磁盘占用。stat 的 %b 与 %B 给出分配块数和块单位,可辅助判断,但仍不是一定能立即回收的承诺。

进程退出或关闭描述符后,/proc 路径可能消失,这是正常竞态。遇到 Permission denied,检查实际执行权限、proc 挂载限制和容器 PID 可见性;看不到不等于没有。

四、用一个小实验验证机制

将代码保存为 unlink_demo.py,运行 python3 unlink_demo.py。示例只删除新建的测试文件,不要替换成生产日志路径。

import os
import tempfile
from pathlib import Path

# 默认临时目录需有空闲空间;仅创建和删除自己的测试文件
with tempfile.TemporaryDirectory() as 目录:
    路径 = Path(目录) / "demo.log"
    with 路径.open("w+b") as 文件:
        文件.write(b"x" * (8 * 1024 * 1024))
        文件.flush()
        os.fsync(文件.fileno())
        路径.unlink()
        状态 = os.fstat(文件.fileno())
        文件.seek(0)
        print("原路径存在:", 路径.exists())
        print("链接数:", 状态.st_nlink)
        print("逻辑字节:", 状态.st_size)
        print("仍可读取:", 文件.read(1) == b"x")

本次在隔离临时目录实际执行了同等逻辑,并用 lsof 辅助核验:原路径不存在,链接数为 0,逻辑大小为 8388608 字节,但已打开句柄仍能读出内容;退出 with 块关闭句柄后,对应 fd 消失。实验没有触碰业务日志,也没有重启任何服务。

测试期间目录 du 为 0,lsof 仍能找到该文件。这证明“目录看不见”和“进程不再占用”不是同一回事;没有用宿主机整体 df 的瞬时差值推算精确释放量,避免把其他进程的并发写入混进结论。

五、怎么释放:先找服务的正确关闭方式

确认占用来源后,优先查该服务的日志轮转与重新打开日志接口。正确流程一般是让服务按自身文档关闭旧日志并打开新日志,再核验旧 inode 的引用是否消失。不能把某个信号当成所有程序通用的“释放按钮”;不支持对应处理逻辑的程序可能直接退出。

如果服务没有安全的重新打开机制,应先评估业务影响、数据持久化和可用副本,再在维护窗口执行有控制的单服务重启。这里不给盲用的 kill -9 或整机重启命令:它们可能让空间下降,也可能把尚未落盘的数据一起带走。

也不建议直接截断 /proc 中的描述符路径。那是在修改仍由业务使用的对象,数据库、队列和审计日志尤其不能照抄;普通日志也可能因当前写入偏移、写入模式等产生新的问题。释放空间不是唯一验收项,业务数据完整性同样算数。

六、没有找到 deleted 文件,换条排查路线

现象下一步不要误判
df -i 接近耗尽定位海量小文件及其生成者空闲字节不代表能继续创建文件
du 比 df 小很多,lsof 无目标核对扫描范围、权限、挂载覆盖与快照不要据此宣称文件系统损坏
已找到 deleted 大文件,处理后仍占用检查同 inode 的其他进程和引用一个 FD 关闭不等于全部释放
空间短暂下降后再次增长核对新日志增速、轮转保留与异常循环释放存量没有治理新增
容器内外数字不同分别核对挂载、命名空间和宿主存储不要把容器视图当成宿主全貌

七、验收要同时看空间与业务

  • 记录处理前后的目标挂载点、时间、df 块空间及 inode 使用情况。
  • 重新运行限定 PID 的 lsof,再核对该 inode 的其他持有者,确认目标引用已释放。
  • 检查应用健康、错误日志及新日志是否继续写入正确路径,权限是否正确。
  • 观察一段代表性业务窗口,确认不是刚释放就被异常日志重新填满。
  • 落实日志轮转、保留策略与磁盘告警;阈值结合增长速度设置,不只盯固定百分比。

故障记录应说清:哪个文件系统、谁持有哪个对象、如何释放、业务是否正常。

八、参考资料与适用边界

本文是长期有效的故障排查,不把既有机制包装成新版本热点。命令按 Linux、GNU 工具和 lsof 语义编写;不同发行版、权限与文件系统可能有差异。机制实验已执行,生产服务释放步骤仅为操作原则,未对读者环境做过验证。

🔕 评论已关闭