服务器磁盘满了,但文件不知道去哪了
「磁盘爆了」是个人站长投诉率最高的故障之一。典型场景是:监控告警显示根分区 95%,你登上服务器,用 du -sh /* 看了一圈,各项加起来才几个 G,可 df -h 明明说 40G 全满了。多出来的空间凭空蒸发了,找不到凶手。
这种「df 和 du 对不上」的现象在 Docker 环境里尤其常见。原因通常是两类:一是容器日志无限增长把空间吃光,二是被删除但进程仍持有的文件没有真正释放。本文把这两类问题和排查手法讲透,并给出一套自动清理方案。
第一步:先搞清楚是哪一层满了
排查的第一步永远是确认满的是哪个挂载点,而不是上来就删文件。执行:
df -hT
# 典型输出
# Filesystem Type Size Used Avail Use% Mounted on
# /dev/vda1 ext4 40G 38G 0.5G 99% /
# overlay overlay 40G 38G 0.5G 99% /var/lib/docker/overlay2/xxxx
# tmpfs tmpfs 1.6G 0 1.6G 0% /dev/shm注意 Docker 的 overlay 文件系统会把自己的使用量算进根分区,所以在 Docker 主机上,「根分区满」和「Docker 占用大」经常是同一件事,不要被吓到,也不要急着扩容,先把真正的占用者找出来。
第二步:用 du 逐层缩小范围
从根目录开始,一层一层往下钻,找出占用最大的目录:
# 只看根目录下的一级目录,按大小排序
du -xhd1 / 2>/dev/null | sort -rh | head -20
# 输出示例
# 24G /var
# 8.1G /www
# 3.2G /usr
# 512M /opt这里的 -x 参数非常重要,它表示「不跨越文件系统」,也就是只统计当前挂载点下的内容。如果不加 -x,du 会跑进 /mnt、/data 这些另外挂载的磁盘,甚至钻进网络文件系统里,导致命令卡死。这是 du 在服务器上最经典的踩坑点。
假设 /var 是 24G,接着钻进去:
du -xhd1 /var 2>/dev/null | sort -rh | head -10
# 常见结果
# 22G /var/lib/docker
# 1.1G /var/log这样三步之内基本就能锁定元凶。绝大多数情况会落在 /var/lib/docker 或 /var/log 上。
第三步:处理容器日志吞噬磁盘
Docker 默认使用 json-file 日志驱动,且默认不限制大小。一个跑得久、输出又啰嗦的应用(比如带 debug 日志的 Node 服务、或者是不断报错的 PHP-FPM),日志文件能轻松涨到几十 GB。查看各容器日志占用:
# 查看每个容器日志文件的实际大小
du -sh /var/lib/docker/containers/*/*-json.log 2>/dev/null \
| sort -rh | head -10
# 更友好的方式,直接列容器 ID 和名称
docker ps -a --format '{{.ID}}\t{{.Names}}\t{{.Status}}'
# 查看某个容器的日志大小
du -sh /var/lib/docker/containers/$(docker inspect -f '{{.Id}}' myapp)/*-json.log找到大日志之后,正确的清理方式是「截断」而不是「删除」:
# 正确:清空内容但保留文件,Docker 的文件句柄依然有效
truncate -s 0 /var/lib/docker/containers/CONTAINER_ID/CONTAINER_ID-json.log
# 也可以这样
: > /var/lib/docker/containers/CONTAINER_ID/CONTAINER_ID-json.log
# 危险:直接 rm 掉
# rm /var/lib/docker/containers/.../xxx-json.log
# 这会删掉目录项,但 Docker 进程仍持有句柄,
# 空间不会释放,df 依然显示满 —— 这就是「df 和 du 对不上」的元凶临时清理治标,治本要限制日志大小。全局配置在 /etc/docker/daemon.json:
{
"log-driver": "json-file",
"log-opts": {
"max-size": "50m",
"max-file": "3"
}
}这表示每个容器日志文件最大 50MB,最多保留 3 个滚动文件,单个容器最多占 150MB。改完执行:
systemctl restart docker需要注意:这个配置只对新创建的容器生效,已经运行的旧容器不会自动应用。要让现有容器生效,必须重建它们:
docker compose down
docker compose up -d如果不想中断服务,可以在 docker-compose.yml 里给单个服务单独配置:
services:
app:
image: myapp:latest
logging:
driver: json-file
options:
max-size: "50m"
max-file: "3"这时用 docker compose up -d 重建该服务即可,日志策略会随之生效。
第四步:处理「已删除但仍占用的文件」
如果 du 加起来远小于 df 的已用量,排名前几的嫌疑就是「deleted 文件」。这些文件被 rm 掉了,但仍有进程打开着句柄,内核不会回收空间,直到进程关闭句柄。查找方法是用 lsof 或 /proc:
# 方法一:用 lsof(需要安装 lsof)
lsof -nP +L1 | head -30
# 输出中 SIZE/OFF 的 NLINK 为 0 且文件标注 (deleted) 的就是
# 方法二:不装任何工具,直接读 /proc
ls -l /proc/*/fd 2>/dev/null \
| grep '(deleted)' \
| awk '{print $NF}' | sort | uniq -c | sort -rn | head找到之后,有两种处理方式。第一种是重启持有该文件的进程:
# 找出是谁持有
lsof -nP +L1 | grep deleted
# 比如是 nginx,就平滑重载(多数情况下 reload 就能释放)
nginx -s reload
# 或者重启服务
systemctl restart nginx第二种是不重启服务,直接把文件内容清零(适用于日志类文件):
# 用 lsof 拿到的文件描述符号,直接截断
# 注意路径里的 (deleted) 要去掉,写真实路径
: > /www/wwwlogs/access.log不过要注意,用 : > 截断一个已经 deleted 的文件路径,如果路径对应的是新创建的同名文件,清的就是新文件,而不是那个被删除的旧句柄。所以最稳妥的方式还是重启或重载进程,让内核彻底回收。
第五步:清理 Docker 的闲置资源
Docker 有几类资源会持续累积:停止的容器、无标签的悬空镜像、未被引用的网络、以及构建缓存。这些都可以用 prune 命令批量清理。执行前先看一眼能被清掉多少:
# 查看整体占用
docker system df
# 输出示例
# TYPE TOTAL ACTIVE SIZE RECLAIMABLE
# Images 42 8 12.4GB 8.1GB (65%)
# Containers 15 6 2.3GB 1.1GB (47%)
# Local Volumes 20 4 4.2GB 2.8GB (66%)
# Build Cache 120 0 3.6GB 3.6GB然后按需清理:
# 清理停止的容器、悬空镜像、未使用网络、构建缓存(不动卷)
docker system prune -f
# 连未被使用的卷一起清理(会删数据,执行前务必确认)
docker volume prune -f
# 只清构建缓存
docker builder prune -f
# 清理超过 7 天的构建缓存
docker builder prune --filter until=168h -f务必注意 docker volume prune 的破坏性:没有被任何容器引用的卷会被直接删除。如果你有过「先停容器再重建」的操作习惯,某些数据卷在停容器期间就会变成「未使用」状态,一个 prune 就可能把数据库数据干掉。所以这条命令在数据库服务器上应该禁止自动执行,或者配合白名单使用。
清理悬空镜像也要留意:它删的是 <none>:<none> 的中间层镜像。如果你习惯用 docker build 反复构建同名 tag,旧版本会变成悬空镜像,清理是安全的。但如果某个镜像只是暂时没有 tag,清理后可能无法再找回对应层。
用 ncdu 做交互式排查,比 du 更直观
逐层敲 du 命令在目录层次深的时候相当费力。ncdu 是一个交互式的磁盘占用分析工具,进入界面后可以用方向键在目录树里自由穿梭,一眼就能看出哪个子目录异常膨胀,按 d 还能直接删除选中的文件。安装和用法如下:
# 安装
apt-get install -y ncdu
# 扫描根分区(-x 同样表示不跨文件系统)
ncdu -x /
# 只扫描 Docker 目录,跳过网络挂载
ncdu -x /var/lib/docker对于容量大、文件多(上百万个 inode)的服务器,ncdu 首次扫描可能耗时几分钟,可以用 -o 把扫描结果导出到文件,之后反复查看而不用重复扫描:
# 第一次扫描并导出
ncdu -x -o /tmp/scan.json /
# 之后从文件打开,秒开
ncdu -f /tmp/scan.json这招在排查历史快照时特别有用:如果磁盘是在某个时间点突然涨起来的,把两个不同时间的扫描结果对比一下,就能立刻锁定是哪个目录涨的。
建立自动清理与告警
手工清理解决当下问题,自动化解决长期问题。下面这个脚本做两件事:磁盘超阈值时自动清理,清理后仍偏高就发告警。
#!/bin/bash
set -uo pipefail
THRESHOLD=85
LOG="/var/log/docker-cleanup.log"
now() { date '+%F %T'; }
USED=$(df -h / | awk 'NR==2 {print $5}' | tr -d '%')
echo "[$(now)] root usage: ${USED}%" >> "$LOG"
if [ "$USED" -lt "$THRESHOLD" ]; then
echo "[$(now)] below threshold, skip" >> "$LOG"
exit 0
fi
# 1. 截断超大的容器日志(保留文件句柄)
find /var/lib/docker/containers -name '*-json.log' -size +100M 2>/dev/null \
| while read -r f; do
echo "[$(now)] truncating $f ($(du -h "$f" | cut -f1))" >> "$LOG"
truncate -s 0 "$f"
done
# 2. 清理 Docker 闲置资源(不动卷)
docker system prune -f --filter "until=168h" >> "$LOG" 2>&1
# 3. 清理过期日志文件
find /www/wwwlogs -name '*.log' -mtime +30 -size +50M -exec truncate -s 0 {} \;
# 4. 复查,仍然偏高则告警
USED_AFTER=$(df -h / | awk 'NR==2 {print $5}' | tr -d '%')
if [ "$USED_AFTER" -ge "$THRESHOLD" ]; then
echo "[$(now)] STILL HIGH: ${USED_AFTER}% after cleanup" >> "$LOG"
# 这里可以接邮件 / webhook 通知
# curl -s -X POST https://your-webhook -d "disk ${USED_AFTER}%"
fi加进 crontab,每天凌晨执行一次:
30 4 * * * /bin/bash /opt/ops/docker-cleanup.sh如果主机上有多个分区需要监控,可以把 df 循环改成遍历 df -hP | awk 'NR>1 {print $6}' 的输出,逐个判断阈值。
几个容易忽略的占用来源
除了 Docker,还有几个地方经常悄悄吃空间。journal 日志:systemd 的日志默认可能占几百 MB 到几 GB,用 journalctl --disk-usage 查看,用 journalctl --vacuum-size=200M 限制。PHP 会话文件:/tmp 或 /var/lib/php/sessions 下的 sess_ 文件,如果 GC 没跑起来会越积越多。宝塔面板的备份目录:/www/backup 下会累积大量数据库和网站压缩包,很多人备份成功了却忘了清理。内核版本:/boot 分区小的话,多次升级内核后旧版本会在 /usr/lib/modules 里堆积,可以用 du -sh /usr/lib/modules/* 检查。
还有一类容易被忽略的是 Nginx 的日志切割失效。如果 logrotate 配置有问题,access.log 可能几个月没切割,单个文件几十 GB。检查方法:
ls -lh /www/wwwlogs/*.log | sort -k5 -rh | head
# 确认 logrotate 配置
cat /etc/logrotate.d/nginx结语
磁盘故障的排查逻辑其实很朴素:先用 df 定位挂载点,再用 du 逐层缩小范围,最后用 lsof 查出「df 和 du 对不上」的隐藏占用。真正的长期解决方案是两条:给 Docker 日志设上限,给清理任务设自动化。把这两件事做掉,磁盘告警基本就再也不会半夜打扰你了。