VPS 磁盘突然 100% 爆满?老哥手把手教你从 df 到清理,5 大空间杀手逐个干掉

明明没放什么文件,df -h 却显示磁盘 100% 爆满导致服务崩溃?本文讲清日志、Docker、被删仍占空间的文件等"看不见的空间去哪了",并给出 journalctl、docker system prune、ncdu、lsof 等可直接复制的排查清理命令。

一、先别慌:为什么"明明没放东西"磁盘却爆了

不知道你有没有经历过这种半夜血压拉满的场面:网站突然 500,SSH 登上去第一句就看到 No space left on device,数据库写不进、Nginx 建不了临时文件,整个服务全瘫。你心里嘀咕:"我这台 VPS 就跑个小博客,几 G 的东西,40G 的盘怎么可能满?"

这其实是每个玩 VPS 的老哥迟早要踩的坑。Linux 的"磁盘占用"对小白来说是个黑盒——你眼睛看得到的文件,往往只占真正占用的一小部分。那些真正吃空间的,是你看不见、或者"以为删了但其实没删掉"的东西。常见的元凶就这几类:

  • 日志文件无限膨胀:Systemd 的 journal 日志、Nginx/Apache 的 access.log 几亿行,你从不看,它默默长到几 G 甚至十几 G。
  • Docker 镜像和虚悬镜像:跑容器时拉的一堆镜像、构建缓存、没人用的 volume,悄咪咪吃掉几 G。
  • 被删掉但还被进程占用的"幽灵文件":文件被 rm 了,但有个进程还开着它的句柄,空间不会释放,重启那个进程才释放。这是 df 显示满、du 却算不出来最常见的原因。
  • inode 耗尽:盘还有空间,但 inode(文件索引节点)用光了,照样写不进任何文件。常见于 /tmp、PHP session 目录、邮件队列堆了几十万个小文件。
  • 缓存与旧内核:apt 的包缓存、snap 旧版本、更新后没清的旧内核,都是隐藏的空间吸血鬼。

所以排查的正确姿势不是上来一顿 rm -rf,而是先搞清楚空间到底被谁吃了,再精准清理。下面是一套我从 df 一路查到清理的标准流程。

二、标准排查流程:从 df 到定位到清理

记住一个铁律:df 看分区整体,du 看目录明细,lsof 看进程持有的已删文件。三者配合才能还原真相。

第一步:df -h 看哪个分区爆了

df -h
# 只看某个挂载点,比如根分区或 /var
df -h /
df -h /var

输出里 Use% 那一列 100% 的就是罪案现场。注意一定要看挂载点——有时候是 / 满了,有时候是 /var/home 单独的分区满了。

第二步:df -i 看 inode 有没有耗尽

df -i

IUse% 列。如果这里接近 100% 而容量还有剩余,恭喜你中招"inode 耗尽",问题不是大文件而是海量小文件。常见于 /var/spool/postfix/tmp、PHP 的 session 目录。

第三步:du 一层层往下挖

# 看根分区下各一级目录占用,按大小排序取前 20
du -h --max-depth=1 / 2>/dev/null | sort -hr | head -20
# 钻进最大的那个目录再往下挖
du -h --max-depth=1 /var 2>/dev/null | sort -hr | head -20
du -h --max-depth=1 /var/log 2>/dev/null | sort -hr | head -10

小技巧:sort -hr-h 是按人类可读单位(K/M/G)正确排序的,千万别漏掉 -h,否则 1.2G 会排在 900M 前面算错,让你追错目录。

第四步:ncdu 可视化交互排查(强烈推荐)

du 一层层敲有点累,老哥们更爱用 ncdu——一个基于 ncurses 的可视化磁盘分析工具,方向键导航、回车下钻,谁占空间一目了然。

# 装一下(Debian/Ubuntu)
apt install -y ncdu
# 扫描根分区,耐心等它数完
ncdu /
# 也可以只扫某个怀疑的目录
ncdu /var

进去之后:方向键移动,Enter 进目录,d 直接删除文件,q 退出。这是定位"空间去哪了"最快的方式,没有之一。新手第一次用 ncdu 基本都会惊呼"原来是你占了这么多"。

第五步:lsof 抓"幽灵文件"(df 满但 du 算不出时必用)

# 找出被删但进程还打开着、仍占用空间的文件
lsof +L1 | grep deleted
# 或者更全一点
lsof | grep deleted

SIZE/OFF 列就是它占着没释放的空间。这种文件你用 ls 根本看不到(目录项已经没了),但 df 算得进去。解决办法是重启持有它的那个进程

# 比如是 nginx 的日志句柄没释放
systemctl restart nginx
# 如果是某个服务,重启对应服务即可;实在不敢重启就 reboot

重启之后空间立马回来,比删文件还灵。这就是"df 满 du 不算"谜题的标准答案。

三、罪魁祸首 1:Systemd 日志文件膨胀

现在绝大多数 VPS(Ubuntu 18.04+、Debian 9+、CentOS 7+)都用 systemd,它的 journal 日志默认不限制大小。一旦某个服务疯狂报错(比如你配错的某个 daemon 每秒写 50 行错误),journal 能在几天内悄悄涨到 2~5G,你平时根本不会去看 /var/log/journal

先看看它现在吃得有多胖:

journalctl --disk-usage

临时瘦身,二选一:

# 只保留最近 100M 的日志
journalctl --vacuum-size=100M
# 或者只保留最近 7 天
journalctl --vacuum-time=7d

但这只是治标,下次还会涨。要治本得改配置,编辑 /etc/systemd/journald.conf

# 限制 journal 最大占用,比如 500M
SystemMaxUse=500M
# 始终为系统保留至少 1G 空闲空间,满了就丢旧日志
SystemKeepFree=1G
# 日志最长保留 1 个月(按需)
MaxRetentionSec=1month

改完让 journald 重新加载:

systemctl restart systemd-journald

配好这几行,你的 VPS 以后再也不会被日志撑爆,属于"装好就忘"的省心配置。

四、罪魁祸首 2:Docker 镜像、虚悬镜像与构建缓存

如果你这台 VPS 跑了 Docker,那它极有可能是最大的空间吞噬者。镜像、停止的容器、虚悬镜像(dangling,就是构建失败或换版后留下的:垃圾)、构建缓存、没人用的 volume,全都会堆在硬盘上。

先看 Docker 到底吃了多少:

docker system df

它会清晰列出 Images / Containers / Volumes / Build Cache 各自的占用。清理从最安全到最狠:

# 1. 清掉停止的容器、无用网络、虚悬镜像、构建缓存(不动正在用的)
docker system prune
# 2. 连未被任何容器引用的镜像也一起删(慎用,会删掉你没在跑的镜像)
docker system prune -a
# 3. 顺手清掉没人用的 volume(数据无价,先确认没重要数据!)
docker volume prune

我自己的习惯是每月跑一次 docker system prune -a 配合 docker volume prune,能轻松腾出几 G。如果你有 CI/构建频繁跑,建议写个定时任务:

# 写入 /etc/cron.weekly/docker-clean,每周自动清(保留正在用的)
cat > /etc/cron.weekly/docker-clean <<'EOF'
#!/bin/bash
docker system prune -af --filter "until=72h" >/dev/null 2>&1
EOF
chmod +x /etc/cron.weekly/docker-clean

这样只清 72 小时前没在用的东西,基本不会误伤正在跑的服务。

五、罪魁祸首 3:Nginx/Apache 的 access.log 狂飙

网站一旦被爬虫盯上、或者被人扫漏洞,access.log 能以每分钟几万行的速度狂飙,几天写满几十 G 不是梦。更坑的是:你直接 rm 掉这个日志文件,空间不会释放——因为 Nginx 还攥着那个文件句柄,直到进程重启。

正确做法是"截断"而不是"删除":

# 瞬间把文件清空为 0 字节,进程句柄不变,空间立刻释放
truncate -s 0 /var/log/nginx/access.log
truncate -s 0 /var/log/nginx/error.log
# 等价写法:重定向清空
> /var/log/nginx/access.log

但截断只是救急,真正的解决是上 logrotate 让日志自动切割、压缩、过期删除。给 Nginx 建个配置 /etc/logrotate.d/nginx

/var/log/nginx/*.log {
    daily
    missingok
    rotate 14
    compress
    delaycompress
    notifempty
    create 0640 www-data adm
    sharedscripts
    postrotate
        [ -f /var/run/nginx.pid ] && kill -USR1 $(cat /var/run/nginx.pid)
    endscript
}

这个配置每天切一次、留 14 天、自动 gzip 压缩,切完给 Nginx 发 USR1 信号让它重新打开日志文件。生效后你再也不会看到单日志文件涨到几十 G。kill -USR1 这个信号是关键——它让 Nginx 不重启用新文件,比 truncate 优雅得多。

另外几个省空间的小动作:

  • 把不重要的访问日志直接关掉或降到最低级别,访问量大的静态站可以只记 error。
  • 日志上外部收集(如用 Vector / rsyslog 转发到对象存储),本地只留少量。
  • Apache 用户同理,配 /etc/logrotate.d/apache2 并 reload。

六、其他常见的隐藏空间杀手

除了上面三大主角,下面这些也经常偷偷吃空间,顺手清掉绝不亏:

APT / YUM 包缓存

# Debian/Ubuntu:清掉下载缓存的 .deb 包,能回收几 G
apt clean
apt autoremove --purge
# CentOS/RHEL
yum clean all

Snap 旧版本(Ubuntu 桌面/部分云镜像)

Snap 默认保留一堆旧版本快照,能占好几 G。限制它只留 2 个版本:

snap set system refresh.retain=2
# 手动删禁用的旧版本
snap list --all | awk '/disabled/{print $1, $3}' | while read name rev; do snap remove "$name" --revision="$rev"; done

更新后没清的旧内核

每个旧内核 200~400M,更新一年能攒一堆。让 apt 自动处理最安全:

apt autoremove --purge
# 千万别手删 /boot 和 /lib/modules 下的文件,会搞崩启动

Core dump 与崩溃文件

一个进程崩了可能把几 G 内存 dump 到 /var/crashcore 文件。检查并清理:

du -sh /var/crash 2>/dev/null
rm -f /var/crash/*

家目录里的缓存和回收站

du -sh ~/.cache ~/.thumbnails ~/.local/share/Trash 2>/dev/null
# 找家目录下大于 500M 的文件
find ~ -type f -size +500M 2>/dev/null

七、总结 / 避坑清单

说到底,VPS 磁盘爆满 90% 都是"日志 + Docker + 幽灵文件"这三样。记住下面这张清单,下次半夜告警你就能 5 分钟搞定:

  • 先诊断再动手df -h 看分区、df -i 看 inode、du -h --max-depth=1ncdu 定位目录,别上来就 rm。
  • df 满但 du 算不出:90% 是被删但进程还打开的文件,用 lsof +L1 | grep deleted 找到,重启对应进程释放。
  • journal 日志journalctl --vacuum-size=100M 急救,并改 /etc/systemd/journald.confSystemMaxUse 永久封顶。
  • Dockerdocker system df 看占用,docker system prune -a 清虚悬镜像,定期 docker volume prune
  • Web 日志:用 truncate -s 0 截断而非删除;上 logrotate 自动切割,postrotate 发 kill -USR1 让 Nginx 重新开文件。
  • 治本靠监控:每台上新机先配磁盘告警(如 Node Exporter + Prometheus,或最简单 crontab 里 df -h | mail),别等 100% 才发现问题。
  • 别手删系统文件/boot/lib/modules、正在跑的日志句柄,删错直接开不了机。

把这几点养成习惯,你的 VPS 基本告别"磁盘突然爆满"的惊魂夜。省下的不只是空间,还有半夜被告警叫醒的血压。

延伸阅读

更多相关攻略推荐:【VPS 硬件选型指南 (CPU 篇) 04】AMD EPYC 还是【发行版选型 07】Arch Linux 深度科普:滚动发布、pac【VPS 进阶玩法精选 014】ARM 架构 VPS 值不值得上?A【对象存储 01】对象存储怎么选:Backblaze B2 vs C【TCP优化 01】美国 VPS 跑不快?多半是没开 BBR,一个命