一次 Linux 磁盘疑难排查方法论:df/du 不一致、幽灵文件、inode 耗尽四步定位法

系统性整理 Linux 磁盘空间与 inode 排查的四步方法论,涵盖 df/du 不一致、已删除文件句柄、幽灵文件、inode 耗尽、日志轮转失效等真实场景,并给出止血与根治 Checklist。

问题背景

生产环境 Linux 服务器磁盘告警是运维日常最常见的告警之一。传统经验是「df -h 看满、du -sh 定位大目录」,但真实故障远比这复杂:df 显示 98% 已满,du 却只算出 30GB;删掉 60GB 日志后空间不释放;df -i 100% inode 耗尽,df -h 却显示还有空间;日志轮转配置正确却仍堆积海量小文件……这些「经典但不常见」的场景往往让一线工程师手足无措,排错耗时数小时甚至跨天。

本文基于真实生产案例,提炼出一套可复用的 四步磁盘疑难排查方法论,并附带止血 Checklist 与根治 Checklist。目标是把「碰运气式排查」变成「流程化作战」。


故障现象

典型表现(真实案例汇总):

  1. df/du 不一致df -h 显示 / 已用 98%(仅剩 2GB),但 du -sh /* 累加只到 30GB,差距 60GB+。
  2. 删除大文件后空间不释放rm -f /var/log/app/*.log 后 df 依然 98%,进程仍在向已删除文件句柄写数据。
  3. inode 耗尽(df -i 100%)df -h 显示还有 40% 空间,但新建文件报「No space left on device」,ls 卡死或极慢。
  4. 日志轮转失效:配置了 logrotate + copytruncate,但 Java 进程仍持续写旧文件,产生海量小文件导致 inode 打满。
  5. overlay2 /var/lib/docker 目录暴涨:Docker 宿主机 overlay2 目录 inode 耗尽,容器无法创建(df -i 100%)。

这些现象的共同点是:文件系统元数据与用户态视图脱节,需要从内核、进程、文件句柄三个层面同时看。


排查过程

第一步:确认「谁在说谎」——df vs du 差异定位

1
2
3
4
5
6
# 1. 先看整体
df -hT /
df -iT /

# 2. 找差异最大的挂载点(通常是 / 或 /var)
du -xhd1 / 2>/dev/null | sort -h | tail -20

关键判断

  • 如果 df -h 满但 du 不满 → 极大概率是「已删除但仍被进程打开的文件」(幽灵文件)。
  • 如果 df -i 100% 而 df -h 未满 → inode 耗尽,需重点排查小文件海啸。

第二步:定位幽灵文件(已删除但仍占用空间)

1
2
3
4
5
# 找被删除但仍打开的文件(+L1 表示 nlink=0)
lsof +L1 | grep -E "(deleted|DEL)" | awk '{print $NF, $(NF-1)}' | sort | uniq -c | sort -rn | head -20

# 或者用 /proc 直接看
ls -l /proc/*/fd/* 2>/dev/null | grep -E "(deleted|(deleted))"

真实案例:Java 收集进程把 62GB 的旧日志文件句柄一直 hold 着,NLINK=0,truncate -s 0 /proc/<pid>/fd/<fd> 可瞬间释放 60GB 空间。

第三步:inode 耗尽专项排查

1
2
3
4
5
6
7
8
9
# 1. 确认 inode 分布
df -i

# 2. 找小文件最多的目录(按文件数排序)
find /var/log -type f | wc -l
find /var/log -type f -printf '%i\n' | sort | uniq | wc -l   # 实际 inode 数

# 3. 定位海量小文件源头
find / -xdev -type f 2>/dev/null | xargs ls -i 2>/dev/null | awk '{print $1}' | sort | uniq -c | sort -rn | head -10

常见根因:

  • Docker overlay2 目录海量小文件(容器日志未轮转 + 镜像层未 GC)
  • /var/spool/clientmqueue(sendmail 队列)
  • /var/log/journal systemd 日志未限制大小
  • 应用日志目录缺少 logrotate 或 copytruncate 配置错误

第四步:日志轮转与进程打开文件句柄交叉验证

1
2
3
4
5
6
7
8
# 检查 logrotate 是否真正生效
logrotate -d /etc/logrotate.d/app 2>&1 | grep -E "(error|warning|skipping)"

# 检查进程是否真的重新打开了日志
ls -l /proc/<pid>/fd/ | grep log

# 终极核查:已打开但 nlink=0 的文件
lsof | awk '$5=="REG" && $NF ~ /deleted/ {print $2, $3, $NF}' | sort | uniq -c | sort -rn

解决方案

止血 Checklist(5 分钟内恢复容量)

  1. 幽灵文件释放(最常见):

    1
    2
    3
    4
    
    # 找到最大已删除文件句柄
    lsof +L1 | awk '$NF ~ /deleted/ {print $(NF-1), $NF}' | sort -k1 -n | tail -5
    # 针对性 truncate(零停机)
    truncate -s 0 /proc/<pid>/fd/<fd>
    
  2. inode 紧急释放

    1
    2
    3
    4
    
    # 清理 Docker 构建缓存与无用镜像
    docker system prune -af --volumes
    # 清理 systemd journal
    journalctl --vacuum-time=7d
    
  3. 临时挂载新盘扩容(当根分区彻底满了):

    1
    2
    3
    4
    
    mkdir /mnt/tmp
    mount /dev/nvme1n1p1 /mnt/tmp
    rsync -aXv /var/log/ /mnt/tmp/log/
    mount --bind /mnt/tmp/log /var/log
    

根治 Checklist(永久杜绝同类问题)

  1. 日志轮转必须配置 copytruncate(针对 Java/Node 等不重开文件的进程):

     1
     2
     3
     4
     5
     6
     7
     8
     9
    10
    
    /var/log/app/*.log {
        daily
        rotate 30
        compress
        delaycompress
        missingok
        notifempty
        copytruncate          # ← 关键
        create 0644 app app
    }
    
  2. Docker 日志驱动限制 + 自动清理

    1
    2
    3
    4
    5
    6
    7
    
    {
      "log-driver": "json-file",
      "log-opts": {
        "max-size": "100m",
        "max-file": "3"
      }
    }
    

    定时任务:docker system prune -af --volumes --filter "until=168h"

  3. 监控双指标(df + inode):

    • Prometheus node_exporter 同时暴露 node_filesystem_avail_bytesnode_filesystem_files_free
    • 告警规则:node_filesystem_avail_bytes / node_filesystem_size_bytes < 0.1 node_filesystem_files_free < 100000
  4. sudoers 与应用最小权限收敛(防止误操作导致日志目录 inode 爆满)。


根因分析

根本原因始终是文件系统状态与进程视图不同步

  • df 读的是 VFS superblock(已分配块数)
  • du 读的是目录树(可见文件)
  • 已删除但仍打开的文件,其 inode 的 block 仍被标记占用,直到最后一个文件描述符关闭
  • inode 耗尽是目录项(dentry)与 inode 分配表的资源耗尽,与数据块无关

理解这三层语义,就能解释 90% 的「诡异磁盘问题」。


预防措施

  1. 部署前 Checklist

    • 所有日志文件必须配置 logrotate(含 copytruncate)
    • Docker 宿主机必须限制 json-file 日志大小
    • 根分区预留至少 20% 缓冲 + inode 监控
  2. 变更管理

    • 任何涉及日志目录的变更必须执行 lsof +L1 巡检
    • 大版本升级前执行 df -i 基线比对
  3. 可观测性

    • 所有服务器同时上报「磁盘空间使用率」与「inode 使用率」两条曲线
    • 建立「df - du 差值」监控面板,差值 > 5GB 立即告警

总结

Linux 磁盘疑难问题从来不是「空间满了」这么简单,而是元数据、进程句柄、日志轮转三者三角关系的失衡。本文提出的四步方法论(确认差异 → 定位幽灵文件 → inode 专项 → 日志轮转验证)已在生产环境多次验证,可将平均排查时间从 2 小时缩短到 15 分钟。

核心口诀

先看 df -i,再看 lsof +L1,最后看 logrotate 是否真的 copytruncate。

把这套 Checklist 做成运维 SOP,磁盘类 P1 告警即可「按图索骥、分钟级止血」。

使用 Hugo 构建
主题 StackJimmy 设计