Linux 磁盘 100% 但 du 查不出东西,五种情况逐个排

监控告警说根分区 100%,登录上去 du -sh /* 加起来只有 12GB,df 显示 40GB 已用。差了快 30GB,du 找不到任何东西。

这种情况基本都是"文件被删了但进程还开着",但也可能是别的原因。这篇按可能性排一下顺序。

先确认是哪个分区

bash 复制代码
df -h
df -i

两条都要看。df -h 看块使用率,df -i 看 inode 使用率。有一种情况是块还有空间但 inode 满了,表现是创建文件报 No space left on device,但 df -h 显示还有余量。

text 复制代码
Filesystem      Inodes  IUsed   IFree IUse% Mounted on
/dev/vda1      2621440 2621440      0  100% /

inode 满了通常是目录里有海量小文件,比如 session 文件、日志分片、邮件队列。定位:

bash 复制代码
for d in /var/*; do echo "$(find $d -xdev 2>/dev/null | wc -l) $d"; done | sort -rn | head

情况一:文件被删但进程还持有句柄

这是最常见的一种,占八成以上。

bash 复制代码
lsof +L1
# 或者
lsof -n | grep -i deleted

输出里会有这样的行:

text 复制代码
java    18342  app  3w  REG  253,1  28901376  /var/log/app/access.log (deleted)
nginx    2041  root  5w  REG  253,1  8912896  /var/log/nginx/access.log.1 (deleted)

文件被 rm 了,但进程的 fd 还指着那个 inode,空间不会释放。日志轮转脚本用 mv 或者 logrotate 配错时最容易出现------按理 logrotate 会发信号让进程重开文件,但如果进程不响应 SIGHUP 或者配置里忘了 postrotate,就漏了。

释放空间有两个办法。一,重启进程。二,不重启,直接截断:

bash 复制代码
: > /proc/18342/fd/3

注意是往那个 fd 里写空内容,不是 rm。这样 inode 立刻释放,进程继续往同一个 fd 写,日志接着记。对 Java 的 logback 有时候不生效,因为写缓冲还在,需要配合触发一次 flush。

用 truncate 更稳:

bash 复制代码
truncate -s 0 /proc/18342/fd/3

这个操作对正在写的进程有点风险,最好在业务低峰做。而且如果进程用的是追加模式(O_APPEND),截断后偏移量会归零,不会有空洞;如果是随机写,可能产生稀疏文件,看起来空间又回来了。

真正的修复是改 logrotate 配置:

text 复制代码
/var/log/nginx/*.log {
    daily
        rotate 14
            compress
                delaycompress
                    missingok
                        notifempty
                            create 0640 www-data adm
                                sharedscripts
                                    postrotate
                                            [ -f /var/run/nginx.pid ] && kill -USR1 $(cat /var/run/nginx.pid)
                                                endscript
                                                }
                                                ```
copytruncate 也能用,但它有丢日志的窗口(拷贝和截断之间有写入会丢)。对审计日志不能接受。

## 情况二:挂载点覆盖了目录

du 统计的是目录树,如果某个目录下挂了一个文件系统,du 默认会跨过去(除非加 -x 反而会限制)。反过来,如果挂载点被覆盖了,原来的内容看不见但仍占空间。

典型场景:容器里往 /data 写了一堆东西,然后又把一个卷挂到 /data 上,原来的文件还在底层文件系统里,新挂载遮住了它。df 算在根分区,du 看不到。

判断方法是对比总用量:

```bash
du -xsh /* 2>/dev/null | sort -h | tail
df -h /

-x 表示只统计同一文件系统,不跨挂载点。加上之后如果总额和 df 差很多,就说明有被遮住的数据。

处理办法是临时把挂载卸掉,清理完再挂:

bash 复制代码
umount /data
du -sh /data
# 清理后
mount /data

生产上要小心,umount 失败多半是有进程占着,用 lsof +f -- /data 找。

情况三:稀疏文件和预分配文件

有些程序会预分配整个文件大小,比如 MySQL 的 innodb 表空间、Kafka 的日志段、虚拟机的磁盘镜像。

bash 复制代码
ls -lh /var/lib/mysql/ibdata1
du -h /var/lib/mysql/ibdata1

ls 显示 12G,du 显示 200M。这种文件的实际占用小于逻辑大小,du 报的是实际块数,所以不会造成"du 查不出但 df 满"。

反过来有一种假稀疏:文件逻辑大小很小但占用很大,几乎不会出现。

情况四:tmpfs 和 /dev/shm

bash 复制代码
df -h | grep -E 'tmpfs|shm'

tmpfs 占的是内存,但 du 遍历 / 的时候会统计到它(除非加 -x)。如果某个程序往 /dev/shm 写了几个 G,df 显示的是 tmpfs 自己的使用率,不影响根分区。但如果 /tmp 挂了 tmpfs,容器内存会被吃掉------这个和磁盘满无关,但现象很像,值得一起查。

bash 复制代码
du -sh /dev/shm/* 2>/dev/null

情况五:被删的目录里还有打开的文件

lsof +L1 只列出 link count 为 0 的文件。如果整个目录被删了,里面的文件 link count 还是 1,lsof 抓不到。

bash 复制代码
lsof -n | grep -i deleted

不加 +L1 会列出所有带 deleted 标记的项,包括那些"文件删了但 fd 还在"但 link count 不为 0 的情况。

更彻底的办法是遍历 /proc/*/fd:

bash 复制代码
find /proc/*/fd -lname '* (deleted)' -printf '%p -> %l\n' 2>/dev/null

想按大小排序:

bash 复制代码
for p in /proc/[0-9]*; do
  pid=$(basename $p)
    for fd in $p/fd/*; do
        target=$(readlink $fd 2>/dev/null)
            case "$target" in
                  *"(deleted)"*)
                          size=$(stat -Lc %s $fd 2>/dev/null)
                                  [ -n "$size" ] && echo "$size $pid $target"
                                          ;;
                                              esac
                                                done
                                                done | sort -rn | head -20
                                                ```
## 一份能直接跑的排查脚本

```bash
#!/usr/bin/env bash
set -uo pipefail

echo "=== 1. 分区使用率 ==="
df -hP | awk 'NR==1 || $5+0 >= 80'

echo "=== 2. inode 使用率 ==="
df -iP | awk 'NR==1 || $5+0 >= 80'

echo "=== 3. 被删除但仍被占用的文件 ==="
lsof -n 2>/dev/null | awk '/deleted/ {print $1, $2, $7, $NF}' | sort -k3 -rn | head -20

echo "=== 4. 前十大目录(不跨挂载点)==="
du -xhd1 / 2>/dev/null | sort -h | tail -10

echo "=== 5. 挂载点 ==="
findmnt -t ext4,xfs,btrfs -o TARGET,SOURCE,SIZE,USED,USE%

注意 set -uo pipefail 没加 -e,因为 lsof 在某些系统上返回非零,加了 -e 脚本会提前退出。

一次真实的时间线

某次线上:df 显示根分区 98%,du 只有 15G。lsof +L1 发现一个 nginx worker 持有 22G 的 access.log.1(deleted)。原因是 logrotate 配了 create 但 postrotate 里的 kill -USR1 没生效------pid 文件路径写错, -f 判断为假,命令静默跳过,sharedscripts 也不报错。

修复:改 pid 路径,加上 logrotate -d 干跑验证,再在 postrotate 里加一行检查返回值。之后每次轮转都正常。

几个预防措施

日志目录单独分区,写满不影响业务;logrotate 配 maxsize 而不是只看 daily;给关键进程加 file descriptor 泄漏监控;容器里限制日志文件大小,Docker 的 json-file driver 默认不轮转,一天能写几十 G。

相关推荐
ynchyong2 小时前
交换级链路聚合-lacp及manual
网络·链路聚合·eth-trunk
2401_868534782 小时前
论网络虚拟化技术在企业网络中的应用
网络
盛世宏博智慧档案3 小时前
一站式智慧档案解决方案,如何实现防火防盗漏水多维度安全监测实践
服务器·网络·数据库
雷✘3 小时前
C++ 类与对象的初始化机制、成员访问边界与拷贝优化
java·网络·c++
Lzg_na3 小时前
BGP 协议详解:背景、作用、报文格式与实际流程
网络
小张成长计划..3 小时前
【Linux】32:Socket编程2-UDP
网络·网络协议·udp
皓月盈江3 小时前
GO使用Gin框架实现模板定义、解析与渲染
linux·golang·go·gin·定义模板·解析模板·渲染模板
xixiaoyunya3 小时前
搭一个浏览器即用的二进制文件分发点:目录规范与权限边界实践
网络·windows
T量子永生3 小时前
Unisoc(紫光展锐) Kernel 6.18 DDK 编译框架详解
linux·运维·spring