欢迎光临
我们一直在努力

报错排查 03|磁盘满了却找不到大文件:lsof +L1 与 /proc 实战

这大概是每个运维都撞过的一幕:

$ df -h /
Filesystem     Size Used Avail Use% Mounted on
/dev/sda2       98G   98G     0 100% /
​
$ sudo du -xsh / 2>/dev/null
43G     /

df 说 98G 用满了,du 把整台机器翻完只找到 43G。

剩下的 55G 去哪了?

我当时的做法很蠢:du 换了六七种参数跑了一遍又一遍,甚至开始怀疑磁盘坏了。直到有人提醒我一句:「你删过什么大文件吗?」

那一刻我才反应过来——删掉了,但空间没回来。 因为文件虽然从目录里消失了,可还有一个进程死死攥着它不放。

〇、先花两分钟:为什么「删了文件」空间还在

先建立这个认知,后面全通。

Linux 里,删除一个文件其实是两步,而且这两步是分开的:

  • 从目录里摘掉这个文件名(rm 干的事);

  • 释放它占用的数据块(当没有任何进程还打开着它时,才发生)。

  • 换个比喻:你把书从图书馆的书架上撤下来了,但还有个人正捧着这本书在读——那本书还在馆里,还占着地方。管理员要等这个人放下书,才能真的把它收走。

    这时候 ls、du 都看不见它了(目录里已经没有这个名字),但 df 是按文件系统实际占用统计的,它照样算这一份。

    所以就会出现:

    df 数字 > du 数字,差值就是「被进程攥着的已删除文件」

    这也是一个特别经典的运维事故:日志把磁盘打满 → 你 rm 掉日志文件 → 磁盘还是满的。因为写日志的进程(比如 nginx、java 应用)句柄一直开着,文件根本没释放。

    先把基线说清楚:上面那个「98G 满、du 只有 43G」的场面,是我为了讲清楚构造出来的典型现场。真实情况是——我机器上 lsof +L1 有输出,但一条都不占磁盘。这一点最容易被误判,先看清楚:

    $ sudo lsof +L1
    COMMAND   PID   USER FD   TYPE DEVICE SIZE/OFF NLINK   NODE NAME
    systemd      1   root 76u   REG    0,1        0     0   11264 /memfd:systemd-udevd (deleted)
    pipewire  4174 rainbow 34u   REG    0,1     2312     0    7190 /memfd:pipewire-memfd:flags=0x0000000f,type=2,size=2312 (deleted)
    gnome-she 4614 rainbow 32u   REG    0,1    36346     0    7199 /memfd:mutter-anonymous-file-wayland-keymap (deleted)
    gnome-she 4614 rainbow 78u   REG    0,1 67108864     0      27 /memfd:pulseaudio (deleted)

    ⚠️ 第一件必须记牢的事:lsof +L1 有输出,不等于磁盘有问题。

    看 NAME 列——上面每一条都以 /memfd: 开头,那是内存文件(进程在内存里造出来的匿名文件,DEVICE 列也不是真实块设备,显示成 0,1 这类)。它们rm 掉不释放磁盘,因为压根不在磁盘上。第三条那个 64 MiB 的 pulseaudio 看着挺唬人,占的是内存。

    真正要找的长这样:DEVICE 是真实块设备(比如 8,2、253,1)、NAME 是真实路径、末尾带 (deleted)。下一节就是。

    不装 lsof 也有办法查,同一条命令的另一种写法:

    $ sudo find /proc/*/fd -lname '*deleted*' -printf '%p -> %l\\n' 2>/dev/null | head -3
    /proc/1/fd/76 -> /memfd:systemd-udevd (deleted)
    /proc/4174/fd/34 -> /memfd:pipewire-memfd:flags=0x0000000f,type=2,size=2312 (deleted)
    /proc/4614/fd/78 -> /memfd:pulseaudio (deleted)

    怎么读:/proc/<PID>/fd/<编号> = 「哪个进程的第几个文件描述符」,箭头后面是它指向的文件。两个命令一一对得上——/proc/1/fd/76 就是 lsof 里 systemd 那条 76u。

    (想亲眼看看这个现象是怎么发生的,我在文末写了一段三分钟就能跑完的复现脚本,只动 /tmp,跑完自己清理。)

    一、打法一:lsof +L1(最快,先跑这个)

    一台真出了事的机器上,输出长这样(下面是按经典事故构造的示意,方便你看清每一列):

    $ sudo lsof +L1
    COMMAND   PID USER   FD   TYPE DEVICE   SIZE/OFF NLINK   NODE NAME
    nginx     1042 root   2w   REG  8,2  12884901888     0 524301 /var/log/nginx/access.log (deleted)
    mysqld    2311 mysql 12u   REG  8,2   6442450944     0 524318 /var/log/mysql/slow.log (deleted)

    怎么读——这张表只要看四列:

    列看什么
    COMMAND / PID 谁攥着这个文件——要释放就得动这个进程
    SIZE/OFF 它占了多少空间(上面两条加起来约 18G,正好补上了缺口)
    NLINK 0 是关键字:链接数为 0 = 这个文件已经没有任何目录引用了
    NAME 末尾 有 (deleted) 字样 = 已删除但未释放

    先把 SIZE/OFF 加起来,看看是不是正好等于你缺的那部分空间——对上了,案子就破了。

    二、打法二:直接看 /proc(不装 lsof 也能查)

    lsof 需要额外装(Ubuntu 上 sudo apt install lsof)。但 lsof 能看的东西,/proc 里本来就有。

    每个进程在 /proc/进程号/fd/ 下,把自己的每个打开文件列成一个符号链接,链接指向哪里一目了然:

    $ sudo ls -l /proc/1042/fd | head -5
    lrwx—— 1 root root 64 Sep 27 03:00 0 -> /dev/null
    lrwx—— 1 root root 64 Sep 27 03:00 1 -> /var/log/nginx/access.log (deleted)
    lrwx—— 1 root root 64 Sep 27 03:00 2 -> /var/log/nginx/error.log

    怎么读:

    • 左边 1、2 这些数字是文件描述符编号(相当于「这个进程手上有几号牌」);

    • 箭头后面是它指向的真实文件;

    • 箭头后面带 (deleted) 的,就是我们要找的。

    上面那份是构造的现场。这套读法你随时能在自己机器上验证——看当前终端自己的 fd:

    $ ls -l /proc/self/fd
    total 0
    lrwx—— 1 rainbow rainbow 64 Sep 28 10:02 0 -> /dev/pts/1
    l-wx—— 1 rainbow rainbow 64 Sep 28 10:02 1 -> pipe:[58522]
    l-wx—— 1 rainbow rainbow 64 Sep 28 10:02 2 -> pipe:[50927]
    lr-x—— 1 rainbow rainbow 64 Sep 28 10:02 3 -> /proc/6788/fd

    这是我终端里的真实输出。怎么读:

    编号指向是什么
    0 /dev/pts/1 标准输入——你的终端
    1 / 2 pipe:[…] 标准输出 / 标准错误(在管道里就长这样)
    3 /proc/6788/fd ls 自己正在读的那个目录

    看清楚了吗——它就是一张「编号 → 文件」的对照表,没有别的玄机。把 0/1/2 之外的某个编号后面看成 xxx (deleted),就是你要抓的那个洞。

    批量扫全系统(不用装任何东西):

    sudo find /proc/*/fd -lname '*deleted*' -printf '%p -> %l\\n' 2>/dev/null

    怎么读:输出一行就是「某个进程的某个 fd 指着某个已删除的文件」。这条命令是这类问题里最通用的探针,任何 Linux 都能跑。

    想看得更细,连它占多少空间一起看:

    sudo ls -l /proc/1042/fd/1
    # 用 stat 看这个 fd 指向的 inode 大小
    sudo stat -L /proc/1042/fd/1 | grep -E "Size|Blocks"

    三、打法三:只想知道"哪个服务在漏",按进程聚合

    如果进程很多,直接按占用量排序,一眼看出元凶:

    sudo lsof +L1 -S 2>/dev/null | awk 'NR>1 {print $1, $2, $7, $9}' | sort -k3 -rn | head

    怎么读:sort -k3 -rn 是「按第三列(大小)从大到小排」,排在最上面的就是最大的那个洞。

    四、怎么释放:三条路,按代价从低到高

    找到之后,问题变成「怎么把空间要回来」。

    路线一:重启服务(最稳,优先选)

    sudo systemctl restart nginx

    进程一重启,旧的句柄全部关掉,空间立刻释放。

    代价是有服务中断(哪怕只有一两秒)。所以:

    • 能重启就重启,这是最干净的做法;

    • 心里有数:重启会让连接断开,负载均衡后面的其他节点能顶住的话,影响就是可接受的。

    路线二:清空文件内容而不删文件(更平滑,但要改配置)

    如果这个文件是日志,而且服务配置支持重新打开日志文件(比如 nginx 收到 USR1 信号会重开日志),那可以:

    sudo truncate -s 0 /proc/1042/fd/1

    怎么读:truncate -s 0 是「把这个文件截断成 0 字节」。这里操作的不是文件路径(路径早没了),而是通过 /proc 里的句柄直接操作那个文件对象——文件还开着,但里面的内容被清空了,空间当场还回来,进程完全无感知。

    ⚠️ 两个必须知道的限制:

  • 这招只对「持续追加写」的文件安全(日志就是典型)。如果进程用固定偏移读写(比如数据库文件),截断会让它以为数据还在,接下来读到一片空洞——那是数据损坏;

  • 你清的是「现在这个句柄」指向的文件。如果进程之后又按原路径创建了新文件(很多服务会),旧句柄还会留着,得配合 USR1 之类的重开日志信号。

  • 所以:不确定文件性质时,老老实实重启服务。

    路线三:查清根因,从源头掐断

    释放完空间,还有一件事必须做:问清楚它为什么会涨到撑满。

    • 日志没轮转?→ 配 logrotate(这是最常见的原因,很多服务默认的轮转策略形同虚设);

    • 服务把 stdout 重定向到了一个从不切割的文件?→ 改成交给 journald,或者配 journald 的容量上限(/etc/systemd/journald.conf 里的 SystemMaxUse);

    • 应用在写一个自己不清理的临时文件?→ 这是应用 bug,要反馈。

    不查根因,下周同一时间你会再来一次。 这不是危言耸听,是这类故障的标准复现周期。

    五、三个坑

    坑 1:rm 之后 df 没变化,就以为删错了文件,接着删别的。

    删对了。空间没释放不是因为你删错了,是因为有进程开着它。 这时候正确的动作是去查谁开着它(打法一),而不是继续删——继续删只会把现场破坏掉,甚至删掉不该删的东西。

    坑 2:日志文件用 rm 而不是 truncate。

    这是最标准的错误示范。日志文件的正确清理方式是清空内容(truncate -s 0 或 : > 文件),然后让服务重新打开:

    sudo truncate -s 0 /var/log/nginx/access.log

    原因:rm 会把文件名摘掉,服务还攥着旧句柄继续往「那个已经不存在的文件」里写,空间一直不释放;而清空内容不改变 inode,服务继续写同一个文件,一切正常。

    坑 3:lsof 的数字和 /proc/PID/fd 对不上。

    lsof -p PID | wc -l 会比 ls /proc/PID/fd | wc -l 多——因为 lsof 会把工作目录、root 目录、可执行文件、内存映射的库也算进去,而这些不占文件描述符。要判断「这个进程还剩多少 fd 额度」,用 /proc/PID/fd 的数字才准(这在下一篇讲 Too many open files 时会用到)。

    附:三分钟亲手复现一次

    光看文字不够直观。下面这段脚本你可以直接贴进终端跑——它只在 /tmp 下操作,结束时自己清理,不碰系统任何其他东西:

    #!/bin/bash
    # 演示「删除后空间不释放」,全程只在 /tmp 下
    D=/tmp/demo-deleted
    mkdir -p "$D"

    echo "① 删除前的 /tmp:"; df -h /tmp | tail -1

    # 造一个 100M 的文件(/tmp 小的机器把 100 改成 50)
    dd if=/dev/zero of="$D/big.log" bs=1M count=100 status=none
    echo "② 造好 100M 文件后:"; df -h /tmp | tail -1

    # 起一个后台进程打开它(扮演"一直不放手"的服务)
    tail -f "$D/big.log" > /dev/null &
    HOLDER=$!
    sleep 1

    # 教科书式的误操作:日志用 rm 删
    rm "$D/big.log"
    echo "③ rm 之后(文件名没了,空间没回来):"; df -h /tmp | tail -1
    echo " 谁在攥着它:"
    ls -l /proc/$HOLDER/fd 2>/dev/null | grep deleted

    # 释放:把攥着它的进程干掉
    kill $HOLDER 2>/dev/null
    sleep 1
    echo "④ 进程结束后:"; df -h /tmp | tail -1

    rm -rf "$D"; echo "清理完成"

    我在自己机器上跑了一遍,原样贴出来(我这台的 /tmp 是 tmpfs,也就是内存盘——照样能演示,这个机制跟文件系统类型无关):

    ① 删除前的 /tmp:
    tmpfs 3.7G 8.0K 3.7G 1% /tmp
    ② 造好 100M 文件后:
    tmpfs 3.7G 101M 3.6G 3% /tmp
    ③ rm 之后(文件没了,空间没回来):
    tmpfs 3.7G 101M 3.6G 3% /tmp
    谁在攥着它:
    lr-x—— 1 rainbow rainbow 64 Sep 28 15:18 6 -> /tmp/demo-deleted/big.log (deleted)
    ④ 进程结束后:
    tmpfs 3.7G 8.0K 3.7G 1% /tmp

    三个细节值得多看一眼:

    • ③ 和 ② 一模一样(都是 101M)——rm 完毫无变化,这就是事故现场;

    • 铁证那行开头的 lr-x——,第一个字符 l 说明它是个软链接(/proc 里的 fd 都是),紧跟的 r-x 说明这个句柄是只读打开的(tail -f 只读),6 -> 是文件描述符编号;

    • ④ 一 kill,101M 当场掉回 8.0K——不是「慢慢回收」,是立刻释放。

    这就是全部原理。 生产环境上,把你的 nginx、java、mysql 代进那个「HOLDER」的位置,就是你现在遇到的事故。

    速查表(文末收藏版)

    谁在占用已删除文件   → sudo lsof +L1         (先看 NAME:/memfd: 开头的是内存文件,不占磁盘)
    (不装 lsof 的版本) → sudo find /proc/*/fd -lname '*deleted*' -printf '%p -> %l\\n' 2>/dev/null
    按大小排序找元凶     → sudo lsof +L1 | awk 'NR>1{print $1,$2,$7,$9}' | sort -k3 -rn | head
    看某个进程开了哪些文件 → sudo ls -l /proc/PID/fd
    判断 df 与 du 的差值 → df -h / + sudo du -xsh /   (差值 = 被攥住的已删除文件)
    释放方式一(最稳)   → sudo systemctl restart 服务名
    释放方式二(平滑)   → sudo truncate -s 0 /proc/PID/fd/N   ← 仅限追加写的日志文件
    清空日志的正确姿势   → sudo truncate -s 0 /var/log/xxx.log   (不要用 rm)
    切日志(nginx)     → sudo kill -USR1 $(cat /run/nginx.pid)
    根治:配日志轮转     → /etc/logrotate.d/ 下加配置
    根治:限制日志总量   → /etc/systemd/journald.conf 里 SystemMaxUse=500M

    赞(0)
    未经允许不得转载:171主机测评 » 报错排查 03|磁盘满了却找不到大文件:lsof +L1 与 /proc 实战
    分享到: 更多 (0)

    评论 抢沙发

    • 昵称 (必填)
    • 邮箱 (必填)
    • 网址