Linux服务器磁盘空间突然减少怎么办?日志、缓存与大文件排查

凌晨三点收到告警:根分区使用率 92%,十分钟前还是 61%。登录上去 df 一看,可用空间少了十几个 G,但业务没上线新版本、也没人传文件——这种"磁盘空间突然消失"的戏码在运维生涯里几乎每个月都要演一次。绝大多数情况下,元凶就三类:日志暴涨、被删除但未释放的文件、某个目录下悄悄堆积的大文件(数据库备份、core dump、容器镜像层)。本文给出一套从"确认现象"到"定位目录"再到"安全清理"的完整排查链路,命令可直接复制执行。

一、第一步永远是 df:确认是哪个分区、哪种"满"

很多人一上来就 du 找大文件,其实先要确认两件事:满的是哪个挂载点,以及是空间满还是 inode 满。这两者表现完全不同,处理方式也不同。

# 以人类可读方式查看各分区空间使用
df -h

# 查看 inode 使用情况(小文件海啸时必看)
df -i

# 只看某个目录所在分区
df -h /var/log

# 排除伪文件系统,看真实磁盘
df -hT -x tmpfs -x devtmpfs

怎么读 df -h 的输出:关注 Use% 与 Avail 两列。需要注意的是,ext4 文件系统默认会为 root 保留 5% 的空间,所以你可能会看到 Use% 显示 100% 但 Avail 还有几个 G(这是 root 专属额度),普通用户写入会报 "No space left on device",而 root 还能写。用 tune2fs -l /dev/vda1 | grep -i reserved 可以看到保留块数量。

如果 df -i 里 IUse% 接近 100% 而空间还剩很多,说明是 inode 耗尽,典型诱因是海量小文件(邮件队列、session 文件、容器层、npm 缓存)。这种情况 du 看目录大小没啥意义,要统计文件数量:

# 统计各目录下文件数量,找出小文件重灾区
for d in /*; do echo -n "$d "; find "$d" -xdev -type f 2>/dev/null | wc -l; done | sort -rn -k2 | head -20

另一个高频陷阱:df 显示已满,但 du 统计的总和远小于已用空间,差值就是"被删除但仍被进程持有的文件"占用的空间。这类文件在目录里已经看不见,du 自然统计不到,但内核并未释放其数据块。

二、定位大目录:du 的正确用法

du(disk usage)统计目录实际占用的磁盘块。核心用法是"逐层下钻":先看一级目录,再进最大的那个目录继续看。

# 查看根下一级目录大小(只看当前分区,不跨挂载点)
du -xh --max-depth=1 / 2>/dev/null | sort -rh | head -20

# 进入可疑目录,继续下钻
du -sh /var/* 2>/dev/null | sort -rh | head
du -sh /var/log/* 2>/dev/null | sort -rh | head

# 查看当前目录下各文件/目录大小并排序
du -sh * | sort -rh | head -20

# 只看某一层,不递归
du -sh --max-depth=1 /data

参数含义必须记住:-s 只显示汇总值(summarize),不加 -s 会把每一层子目录都打印出来,刷屏到你没法看;-h 人类可读单位;-x(--one-file-system)非常关键,表示不跨越文件系统边界,否则 du -sh / 会把挂载在 /mnt 上的几 T 存储也算到根分区头上,导致误判。

2.1 直接找大文件:find

# 查找大于 100MB 的文件,按大小排序显示前 20 个
find / -xdev -type f -size +100M -exec ls -lh {} \; 2>/dev/null | awk '{print $5, $9}' | sort -rh | head -20

# 更简洁:只列路径和大小
find /var -type f -size +500M -printf '%s %p\n' 2>/dev/null | sort -rn | head

# 按修改时间找最近 1 天内变动过的大文件(排查"突然"减少最有效)
find / -xdev -type f -mtime -1 -size +50M -printf '%TY-%Tm-%Td %TH:%TM  %10s  %p\n' 2>/dev/null | sort -k3 -rn | head -20

# 找 7 天前的大日志,作为清理候选
find /var/log -type f -name "*.log*" -mtime +7 -exec ls -lh {} \;

第二条命令中的 -printf '%s %p\n' 是排查利器:%s 是字节数,%p 是完整路径,%T 系列是修改时间。用它输出后交给 sort -rn 排序,比 ls -lh 再解析要准确得多(ls 的 K/M/G 单位不能直接数值排序)。

2.2 交互式神器 ncdu

如果命令行排序看得累,装一个 ncdu,它会先扫描再给出可交互的目录大小排行,用方向键就能逐层进入:

# 安装
yum install -y ncdu    # 或 apt install -y ncdu

# 扫描根目录(限制在当前文件系统)
ncdu -x /

# 扫描并导出结果供后续分析
ncdu -x -o /tmp/ncdu-root.json /

判断标准:如果某个目录在 ncdu 中占比超过 60% 且你叫不出它的用途,就要进去看具体文件类型再决定删不删。

三、日志:最常见的空间杀手

3.1 systemd 日志(journald)

使用 systemd 的系统(CentOS 7+、Ubuntu 16.04+)会把日志写到 /var/log/journal/(持久化)或 /run/log/journal/(内存,重启丢失)。journald 日志是二进制格式,不能直接 rm,要用自带的清理命令。

# 查看 journal 日志占用空间
journalctl --disk-usage

# 按大小清理,只保留最近 500MB
journalctl --vacuum-size=500M

# 按时间清理,只保留最近 7 天
journalctl --vacuum-time=7d

# 按文件数量清理
journalctl --vacuum-files=5

# 查看某个服务最近 100 行日志
journalctl -u nginx -n 100 --no-pager

治本要改配置 /etc/systemd/journald.conf,把下面几项取消注释并设定值,然后 systemctl restart systemd-journald:

[Journal]
SystemMaxUse=1G          # 持久化日志总上限
SystemKeepFree=2G        # 至少为磁盘保留的空闲空间
MaxRetentionSec=7day     # 日志最长保留时间
Compress=yes             # 压缩旧日志

判断标准:journalctl --disk-usage 输出超过 2G 就该处理了;如果输出是 0 但 /run 目录占用高,说明日志写在内存里,此时应创建 /var/log/journal 目录并重启 journald,让它落盘受配额管控。

3.2 应用日志与 Nginx/Apache 日志

应用日志通常在 /var/log 及其子目录。用 du 找出最大的那个,再判断处理方式:能轮转的交给 logrotate,不能轮转的先压缩归档。

# 查看 /var/log 下各目录大小
du -sh /var/log/* | sort -rh | head

# 安全清理:先清空而不是删除(避免进程句柄失效)
> /var/log/nginx/access.log
# 或者
truncate -s 0 /var/log/nginx/access.log

# 压缩归档 7 天前的日志
find /var/log -type f -name "*.log" -mtime +7 -exec gzip {} \;

# 删除 30 天前已压缩的日志
find /var/log -type f -name "*.gz" -mtime +30 -delete

这里有个必须强调的纪律:正在被进程写入的日志文件,用 > file 或 truncate 清空,而不要用 rm 删除。rm 之后进程仍然持有文件句柄,空间不会释放(df 依旧满),而且日志会继续写进一个你看不见的文件里,直到进程重启。正确的清空姿势是重定向或 truncate。

3.3 已经 rm 了怎么办:找回被占用的空间

# 列出已被删除但仍被进程打开的文件
lsof | grep deleted

# 更精确的查看
lsof +L1
lsof /var | grep -i deleted

输出里能看到进程名、PID 和文件大小(SIZE/OFF 列)。处理方式有两种:一是重启对应进程(systemctl restart nginx),这是最干净的;二是如果进程不能停,可以清空它持有的文件描述符:

# 通过 /proc 清空,无需重启进程
: > /proc/PID/fd/FD号

# 例如
: > /proc/1234/fd/1

注意:对于正在被写入的日志,清空 fd 后进程会从当前偏移量继续写,可能生成稀疏文件,空间表现上会被"撑回来",所以长期方案仍然是配置 logrotate 并让程序支持重新打开日志(reopen)。

3.4 建立容量基线,把救火变成预防

每次都等到告警再来处理,说明缺少容量基线。建议给每个分区记录三个数字:当前使用率、日均增长量、预计可撑天数。日均增长量可以用连续两天的 df 结果相减得到,也可以直接统计日志目录最近一天新增文件的总大小。

# 统计最近 24 小时新增文件占用的总字节数
find /data -xdev -type f -mtime -1 -printf '%s\n' 2>/dev/null | awk '{s+=$1} END {printf "近24小时新增 %.2f MB\n", s/1024/1024}'

# 连续采样两次 df,算出增量
df -P /data | awk 'NR==2{print $3}' > /tmp/used.$(date +%F)

得到日均增量后,用剩余可用空间除以它,就是预计可撑天数。判断标准:预计可撑天数少于 30 天就该扩容或加强清理策略,少于 7 天属于紧急状态,需要立刻处理而不是等告警。进一步的做法是把 df 使用率做成定时采集(每 10 分钟一次写入时间序列),画成趋势图——容量问题的拐点在趋势线上往往比在阈值告警里早几天就暴露出来了。对容易失控的目录(如 /data/backup)还可以用文件系统配额兜底:xfs 用 xfs_quota 设置项目配额,ext4 用 setquota,给它们单独设一个上限,即使脚本写错或业务暴涨,也不会把整个分区甚至根分区拖垮。

四、其他容易被忽略的空间占用源

占用源典型路径排查命令安全处理方式
包管理器缓存/var/cache/yum、/var/cache/apt/archivesdu -sh /var/cache/*yum clean all / apt-get clean
Docker 镜像与卷/var/lib/dockerdocker system dfdocker system prune -a(确认无在用容器)
容器日志/var/lib/docker/containers/*/*-json.logfind /var/lib/docker/containers -name "*-json.log" -size +1G配置 log-opts max-size,或 truncate 清空
core dump 文件/var/lib/systemd/coredump、程序目录find / -name "core.*" -size +100M确认无用后删除,并限制 coredump 大小
临时文件/tmp、/var/tmpdu -sh /tmpsystemd-tmpfiles --clean 或手动清理
数据库 binlog / WAL/var/lib/mysql、/var/lib/postgresqldu -sh /var/lib/mysql按保留策略 PURGE BINARY LOGS,勿直接删
已删除未释放文件无路径lsof | grep deleted重启进程或清空 /proc/PID/fd
备份文件堆积/data/backup、/rootfind / -name "*.sql.gz" -mtime +30按备份保留周期清理并加轮转

其中 Docker 场景值得单独说一句:容器里跑的服务如果日志不打轮转,一个 json.log 涨到几十 G 是常事。根治办法是在 /etc/docker/daemon.json 里配置:

{
  "log-driver": "json-file",
  "log-opts": {
    "max-size": "100m",
    "max-file": "3"
  }
}

改完执行 systemctl reload docker,注意这只对新建容器生效,已有容器需要重建。

五、标准排查步骤清单

  1. 确认现象:df -h 看是哪个挂载点,df -i 判断是否 inode 耗尽;记录当前 Use% 作为基准。
  2. 判断是否"幽灵文件":lsof | grep deleted,有输出就优先处理(重启进程或清空 fd),这一步往往能立刻回收几十 G。
  3. 定位大目录:du -xh --max-depth=1 / | sort -rh | head -20,锁定可疑一级目录。
  4. 逐层下钻:对可疑目录反复执行 du -sh 目录/* | sort -rh | head,直到找到具体文件。
  5. 确认文件性质:用 file 文件名、ls -lh、head -n 5 判断是日志、备份还是数据库文件,不要凭扩展名想当然。
  6. 安全清理:正在写入的日志用 truncate -s 0;历史日志先 gzip 归档再删;数据库文件必须走数据库自身的清理命令。
  7. 清理 systemd 日志:journalctl --vacuum-size=500M 与 --vacuum-time=7d。
  8. 治本:配置 logrotate、journald 配额、Docker 日志大小限制,并加入监控告警(建议 80% 预警、90% 严重)。
  9. 复核:再次 df -h 确认回收量符合预期,若空间未释放回到第 2 步。

小结

磁盘空间突然减少,本质上是一个"定位问题"而不是"清理问题"。记住这条主线:df 确认分区与类型 → lsof 排幽灵文件 → du 逐层下钻 → find 定位大文件 → 按文件性质选择清理方式 → 用 logrotate/journald 配额治本。养成"删除前先确认、清空优先于删除、清理后必须复核"三个习惯,可以避免绝大多数次生故障。

避坑清单

  • 不要对正在写入的日志执行 rm,空间不会释放,用 > file 或 truncate -s 0。
  • du 一定要加 -x,否则跨挂载点统计会让你找错方向。
  • 不要用 rm -rf /var/log/* 这种粗暴命令,会删掉目录结构与权限,导致服务重启后无法写日志。
  • 数据库数据文件、binlog 绝不能手动 rm,用 PURGE BINARY LOGS / VACUUM 等官方命令,否则会损坏实例。
  • Use% 100% 但 Avail 还有空间是 ext4 的 root 保留块(默认 5%),用 tune2fs -m 1 /dev/vda1 可调整比例,但别设成 0。
  • 清理前先确认是否有备份策略,尤其是挂载在 /data 下的业务数据,删错无法恢复。
  • inode 满和空间满是两回事,df -i 才是判断依据,别在小文件场景里死磕 du。