跳到主要内容

创刊于 2016 总第 3337 期


Goslion的Blog

一个专注技术与写作的个人博客,记录日常与技术沉淀,马上十年了


2026年10月9日 星期五 总第 3337 期 今日 2 篇 · 存稿 418 篇

Linux 磁盘满了怎么排查

从 df 到 du,再到已删除却被进程占用的文件和 inode 耗尽,一套按顺序执行的排查流程,附安全清理清单、在线扩容与预防配置。

磁盘满了,先别急着删

磁盘告警时最容易犯的错,是直接 rm -rf /var/log/*,结果空间一点没释放,日志还丢了。

原因下面会说,先按这个顺序走一遍,通常几分钟就能定位到是谁在吃空间。

第一步:分清是空间满了还是 inode 满了

bash
df -h                 # 各挂载点的空间使用率
df -i                 # 各挂载点的 inode 使用率

输出示意:

text
Filesystem      Size  Used Avail Use% Mounted on
/dev/vda1        99G   94G     0 100% /
/dev/vdb1       500G  120G  355G  26% /data
text
Filesystem      Inodes  IUsed   IFree IUse% Mounted on
/dev/vda1      6553600 6553600      0  100% /
/dev/vdb1     32768000 1200000 31568000   4% /data

这两种"满"报错信息一样(都是 No space left on device),但排查方向完全不同:

  • df -h 的 Use% 到 100% —— 空间真的被数据占满了;
  • df -i 的 IUse% 到 100% —— 空间还有富余,但文件数量到顶了,同样写不进新文件。

小文件特别多的机器(session、缓存碎片、邮件队列)经常是后一种。

第二步:用 du 逐层下钻

bash
du -xh --max-depth=1 / | sort -rh | head -20

输出示意:

text
45G     /var
12G     /home
3.1G    /usr

-x 很关键 —— 不加它会跨到其它挂载点,看到的数字是虚的。

定位到大目录后逐层往下:

bash
du -xh --max-depth=1 /var | sort -rh | head -20
du -sh /var/* | sort -rh | head

du 在大目录上会跑很久,赶时间可以直接看已知的几个大户:/var/log、/var/lib/docker、/var/cache、/home、/tmp。

用 du 时有三个地方容易看错:

  1. -x 别省,否则会把挂载在下层的盘也算进来;
  2. 硬链接默认只算一次,要按"每个链接都占一份"来统计得加 -l;
  3. 稀疏文件按实际占用算,所以 du 常常远小于 ls -l 显示的大小 —— 这是正常的,不是漏算。

第三步:df 和 du 对不上 —— 已删除但没释放的文件

这是最经典也最容易被忽略的情况:df 说占用 90%,du 加起来只有 30%。

原因是有进程还持有已被 rm 掉的文件句柄,空间自然不释放;du 遍历目录树看不到它,df 统计块设备能看到它。

bash
lsof +L1                        # link count 为 0 的文件,也就是"已删除但被打开"
lsof | grep deleted | head

输出示意:

text
COMMAND  PID   USER   FD   TYPE DEVICE  SIZE/OFF NLINK   NODE NAME
nginx   1183   root    2w   REG  253,1 20.4G     0  131091 /var/log/nginx/access.log (deleted)

输出里有 PID 和 FD,处理方式二选一:

bash
# 方式一:重启对应服务(日志类最常见,重启后自动释放)
systemctl restart nginx

# 方式二:不重启,把该句柄截断为 0(对日志文件有效)
: > /proc/1183/fd/2

注意必须是 : >(截断),而不是 rm —— 删掉 /proc 下的路径不解决任何问题,空间依旧不释放。

这也是为什么开头那个 rm -rf /var/log/* 没用:文件被删了,但写日志的进程还开着句柄。

第四步:inode 耗尽的排查

bash
df -i                                            # 先确认是不是 inode 满了
find / -xdev -type f 2>/dev/null | wc -l         # 全盘文件总数

# 找出哪个目录下文件最多
find / -xdev -type f 2>/dev/null | sed 's|/[^/]*$||' | sort | uniq -c | sort -rn | head

输出示意:

text
 812340 /var/lib/php/sessions
  42117 /var/spool/postfix/deferred
   3106 /var/log

常见元凶是大量小文件:session 文件、缓存碎片、/var/spool 下的邮件队列、被按秒切割的日志。

确认后整目录删除比逐个删快得多,rm -rf /path/to/dir 之后重建空目录即可。

第五步:几个稳定的空间大户

1. systemd 日志

bash
journalctl --disk-usage            # 当前占用多少
journalctl --vacuum-size=200M      # 收缩到 200M
journalctl --vacuum-time=7d        # 只保留最近 7 天

想永久限制,改 /etc/systemd/journald.conf,然后 systemctl restart systemd-journald:

ini
SystemMaxUse=500M
SystemMaxFileSize=50M

2. 包管理器缓存与旧内核

bash
apt-get clean && apt-get autoremove --purge     # Debian / Ubuntu
yum clean all                                   # CentOS / RHEL
package-cleanup --oldkernels --count=2          # CentOS 保留最近 2 个内核

3. Docker

bash
docker system df              # 分开看:镜像 / 容器 / 数据卷 / 构建缓存
docker system prune -a        # 清掉未使用的镜像与构建缓存(会删东西,看清提示再确认)

第六步:按时间清理历史文件

只删够老的文件,别碰正在写的。养成先干跑再真删的习惯:

bash
# 先干跑:只打印清单,确认没有误伤
find /var/log -type f -name "*.log" -mtime +30 -print

# 确认后再真删
find /var/log -type f -name "*.log" -mtime +30 -delete

-mtime +30 表示修改时间在 30 天以前。目录里文件极多时,find -delete 比 rm -rf 稳,不会因为参数过长而失败。

第七步:确实不够用了 —— 在线扩容

排查完发现没有可清理的,那就扩。LVM 下三步,第 3 步不能省,否则卷大了文件系统还是不认识新空间:

bash
vgs                                    # 先看卷组里还有多少 VFree 可分配
lvs                                    # 看现有逻辑卷与大小

lvextend -L +10G /dev/mapper/vg0-root  # 给逻辑卷加 10G

# 让文件系统认识新空间,命令按文件系统类型区分
resize2fs /dev/mapper/vg0-root         # ext4:参数是设备
xfs_growfs /                           # XFS:参数是挂载点,不是设备

两个容易踩的点:

  1. ext4 支持在线扩容,也支持缩容(但要先卸载);
  2. XFS 只能扩、不能缩,选文件系统时就要想清楚。

清理时的几个坑

  • 别删正在写的日志文件。rm 之后进程仍持有句柄,空间不释放、内容也没了。要么 truncate -s 0 file,要么先停服务。
  • rm 大文件不等于立刻释放空间。删除只是解除链接,真正回收取决于还有没有进程打开着它。
  • du 小于 df 是正常的。文件系统元数据、被删句柄、稀疏文件都会造成差异;只有差得离谱时才需要按第三步查。
  • 别在业务高峰做全盘 du 或 find,这两条命令都会把磁盘 IO 打满,反而拖垮服务。

预防

日志轮转配好,比事后救火省事得多。/etc/logrotate.d/myapp:

ini
/var/log/myapp/*.log {
    daily
    rotate 7
    compress
    missingok
    notifempty
    copytruncate     # 复制后截断原文件,进程不用重启(正好绕开句柄占用问题)
}

另外三条:

  1. journald 上限按上面的配置写死;
  2. 监控不能只看空间,df -i 一起报;
  3. 容器日志单独限大小:--log-opt max-size=50m --log-opt max-file=3。

总结

  1. 先分清是空间满还是 inode 满:df -h 和 df -i 都要看;
  2. du -xh 逐层下钻,-x 别跨挂载点,注意硬链接与稀疏文件的影响;
  3. df 与 du 差得多 → lsof +L1 找已删除但被占用的文件,用 : > /proc/PID/fd/N 截断;
  4. 稳定大户按顺序清:journald、包管理器缓存、旧内核、Docker、应用日志;
  5. 清理历史文件先 -print 干跑,确认后再 -delete;
  6. 真的不够用就扩容,LVM 三步走完记得 resize2fs 或 xfs_growfs;
  7. 长期靠 logrotate 与各项上限配置,别等告警了才动手。