云服务器磁盘写满实战:从 df 到 lsof 的八步定位法
线上机器磁盘写满,是运维里最容易让人手忙脚乱的一类故障:告警只说「使用率 100%」,数据库可能已经写不进 binlog,Nginx 也拒绝再写 access.log。这篇按我实际处理过的一次 40GiB 系统盘写满事故,整理出一套可以照着敲的八步定位法,命令都在 Ubuntu 22.04 上验证过。
先立一条原则:先看空间,再删东西
清磁盘最大的风险不是删得慢,而是删错。所以我习惯先花两分钟把「谁在占空间」问清楚,再动手。整个过程分三层:
- 文件系统层:
df - 目录层:
du - 进程层:
lsof
三层都过一遍,基本不会误删。
第一步:df -hT 看清设备与文件系统类型
df -hT
输出里三个信息最关键:挂载点、容量、文件系统类型。比如:
Filesystem Type Size Used Avail Use% Mounted on
/dev/vda1 ext4 40G 40G 0 100% /
tmpfs tmpfs 909M 0 909M 0% /dev/shm
/dev/vdb1 ext4 100G 12G 83G 13% /data
重点看 Mounted on 是 / 的那一行。如果 /data 还很空而 / 满了,说明问题不在数据盘,而在系统盘上的日志、缓存或容器层——这时候清理思路完全不同,不要急着动数据盘。
顺带确认一下:文件系统类型是 xfs 还是 ext4,会影响到后面的 du 参数习惯和 inode 数量策略,记下来。
第二步:df -i 排除 inode 耗尽
这一步最容易被跳过,但坑最深。
df -i
如果看到:
Filesystem Inodes IUsed IFree IUse% Mounted on
/dev/vda1 2621440 2621440 0 100% /
那就不是「文件太大」,而是「文件太多」。此时 du 会告诉你目录只有几百 MB,但新文件就是写不进去,报 No space left on device。这类问题在小文件密集的场景特别常见:
- PHP 的 session 目录
/var/lib/php/sessions - 邮件队列
/var/spool/postfix/maildrop - 应用把每个请求写成一个小文件做缓存
- 大量未轮转的按天日志
定位办法是找目录里的文件数量:
for d in /var /tmp /home /opt; do
echo -n "$d: "; find "$d" -xdev -type f 2>/dev/null | wc -l
done
-xdev 很重要,它保证不跨文件系统,不会把挂载进来的数据盘也算进来。
第三步:du 分层下钻,定位到具体目录
确认是空间问题后,从根开始一层层往下钻。关键是两个参数:-x 不跨文件系统,--max-depth=1 只看一层。
du -xh --max-depth=1 / 2>/dev/null | sort -rh | head -15
找到最大的目录后,把它当新的根继续钻:
du -xh --max-depth=1 /var 2>/dev/null | sort -rh | head -15
du -xh --max-depth=1 /var/log 2>/dev/null | sort -rh | head -15
我那次事故钻到第三层就定位了:/var/lib/docker/overlay2 占了 31G。原因是一个调试容器把 stdout 重定向到了容器内的文件,日志既没轮转也没落到宿主机,全堆在镜像层里。
再补一个找大文件的快捷命令:
find / -xdev -type f -size +500M -printf '%s\t%p\n' 2>/dev/null | sort -rn | head
第四步:lsof 找「已删除但仍被占用」的文件
这是最经典的陷阱:du 显示总共只占 20G,df 却显示 40G 全满。原因是某个进程还持有已被删除文件的句柄,空间不会释放。
lsof -nP +L1 2>/dev/null | head -20
+L1 表示「链接数小于 1」,也就是文件在目录里已经不存在了,但还有进程打开着。输出里的 SIZE/OFF 列就是被占住的字节数。
如果系统里没装 lsof,用 /proc 也能查:
for p in /proc/[0-9]*; do
ls -l "$p/fd" 2>/dev/null | grep -i deleted
done
处理办法只有一条:重启持有该文件的进程。rm 再多次都没用,因为 inode 还被引用着。
systemctl restart myapp
df -h /
注意一个常见误操作:直接 kill -9 进程。如果是数据库或消息队列,先确认能否安全重启,别为了回缩 2G 空间把服务搞坏。
第五步:容器场景单独算账
如果机器上跑了 Docker,du 看到的数字和你以为的往往不一致,因为 overlay2 里有大量硬链接,du 会重复统计也可能低估。用 Docker 自己的账本更准:
docker system df -v
常见三类占用:
- 悬空镜像(
<none>标签):docker image prune -f - 停止的容器和它们的可写层:
docker container prune -f - 未使用的 volume:
docker volume prune -f,执行前务必确认没有容器依赖
我一般不会一上手就跑 docker system prune -a --volumes,因为它会删掉所有未运行容器引用的镜像和卷,回滚成本很高。先看 df -v 的具体清单,再针对性地清。
另外提醒一句:容器日志的默认驱动是 json-file,默认不限制大小。给 /etc/docker/daemon.json 加上限比事后清理有效得多:
{
"log-driver": "json-file",
"log-opts": { "max-size": "50m", "max-file": "3" }
}
改完 systemctl reload docker,只对新创建的容器生效,老容器要重建。
第六步:journald 与业务日志的限额
用 systemd 的系统,/var/log/journal 很容易悄悄涨到几个 G:
journalctl --disk-usage
journalctl --vacuum-size=200M
永久生效要改 /etc/systemd/journald.conf:
[Journal]
SystemMaxUse=500M
SystemMaxFileSize=50M
业务日志交给 logrotate,一个能直接用的模板:
/var/log/myapp/*.log {
daily
rotate 7
compress
delaycompress
missingok
notifempty
copytruncate
}
copytruncate 适合不支持重新打开日志文件的程序,代价是轮转瞬间可能丢几行。能 reopen 的程序尽量用 postrotate 发信号,更可靠。
第七步:按安全顺序清理
清理顺序我固定成这个,风险从低到高:
- 包管理器缓存:
apt-get clean、journalctl --vacuum-size=200M - 明确的旧日志、旧备份:确认文件名带日期且已上传到对象存储
- 容器悬空镜像、停止的容器
- 应用缓存目录:先停服务再删,避免半删状态
- 最后才考虑扩盘或迁移数据
每一步之后都重新 df -h / 看增量,确认这一步真的起作用。如果清理了 5G 但 df 几乎没变,说明还有被占用的已删除文件,回到第四步。
第八步:让它别再发生
故障处理完,补三件事:
第一,加监控。磁盘使用率 80% 告警、90% 严重告警,inode 使用率单独告警一条,很多人只配了前者。
第二,日志治理。确认 journald 限额、业务日志轮转、容器日志上限三处都配了,这三处覆盖了小规格云服务器上 90% 的「莫名涨满」。
第三,拆分磁盘。系统盘只放系统和日志,业务数据放到独立数据盘,挂到 /data 并写进 /etc/fstab(建议用 UUID 而不是 /dev/vdb1,避免设备名漂移)。这样即使日志失控,也只影响系统盘,不会连带业务数据一起出事。
踩坑清单
du不跨设备统计,查根目录忘了-x会把数据盘算进来,白忙一场。df满了但du不大,八成是已删除仍被占用的文件,去查lsof +L1。- 只看
df -h不看df -i,inode 耗尽会被误判成「空间没满啊」。 rm -rf大目录时如果目录里文件上百万,删除本身就很慢,用find ... -delete分批更可控。- 清理
/tmp前确认没有服务在用,很多程序把 socket 放在那里。 - 扩盘之后别忘了
growpart和resize2fs,只扩了云盘容量不扩文件系统,df完全不会变。
这套流程跑熟了,一次磁盘告警从发现到恢复大概十分钟。真正花时间的从来不是敲命令,而是忍住别在第一分钟就 rm -rf。
- 点赞
- 收藏
- 关注作者
评论(0)