磁盘占满的表现往往不是「磁盘错误」,而是各种莫名其妙的故障:MySQL 突然起不来、日志写不进去、容器反复重启、网站报 500。本文给一条从「定位是谁占的」到「安全清理」的完整路径。
第一步:确认是哪块盘满了
# 整体使用率
df -h
1. 注意两件事:
1. 1. inode 也可能满(小文件太多时 df -h 显示有空间但写不进)
df -i
1. 2. 容器内的 /var/lib/docker 可能挂在另一块盘
df -h /var/lib/dockerdf -h 有空间但写不进,八成是 inode 满了。
df -i看 IUse%,接近 100% 就是这个问题——大量小文件(如 session、日志碎片)会先耗尽 inode 而不是空间。
第二步:找出谁占的(du 的正确用法)
# 从根往下逐层找,每层只看前 10 个
du -sh /* 2>/dev/null | sort -rh | head -10
1. 锁定大目录后继续往下钻
du -sh /var/* 2>/dev/null | sort -rh | head -10
du -sh /mnt/dockerv/* 2>/dev/null | sort -rh | head -10
1. 找单文件大于 100M 的
find / -xdev -type f -size +100M -exec ls -lh {} \; 2>/dev/null | head -20
1. 只看 inode 占用最多的目录(小文件密集)
find / -xdev -type d 2>/dev/null | while read d; do
n=$(ls -U "$d" 2>/dev/null | wc -l)
[ "$n" -gt 1000 ] && echo "$n $d"
done | sort -rn | head -10第三步:常见占用源(按实际频率排序)
| 占用源 | 典型大小 | 清理方式 | 风险 |
|---|---|---|---|
| Docker 镜像与构建缓存 | 几 GB – 几十 GB | docker system prune -a | 会删未使用的镜像,需确认 |
| 容器日志(json-file) | 单容器可达 GB | 配 max-size 限制 + 清空 | 低 |
| journald 系统日志 | 1-4 GB | journalctl --vacuum-size=200M | 低 |
| MySQL binlog | 几 GB | PURGE BINARY LOGS | 高:删了无法恢复到该时间点 |
| MySQL 慢日志 / 错误日志 | 几百 MB | 轮转或清空 | 低 |
| 备份文件累积 | 随备份策略 | 按保留策略删旧备份 | 高:删了没备份 |
| 过期证书归档 | 几十 MB | certbot 自动管理,慎删 | 中 |
| 系统包缓存 | 几百 MB | dnf clean all / apt clean | 低 |
按「风险从低到高」的顺序处理,先做无损的:
# 1. 清包管理器缓存(完全无损)
dnf clean all # Alibaba Cloud Linux / CentOS / Fedora
1. apt clean # Debian / Ubuntu
1. 2. 限制并清理 journald(保留最近 200M)
journalctl --vacuum-size=200M
journalctl --vacuum-time=14d
1. 3. Docker 清理(先看会删什么,加 --dry-run 不实际删)
docker system df # 看占用分布
docker image prune -a # 删未使用的镜像(不删容器)
docker builder prune # 删构建缓存
1. docker system prune -a --volumes # 危险:连 volume 一起删,慎用第四步:容器日志是隐形大户
Docker 默认日志驱动是 json-file,无上限。一个话多的容器几天就能写出几个 GB。
# 看各容器日志大小
du -sh /var/lib/docker/containers/*/*-json.log 2>/dev/null | sort -rh | head -10
1. 看单个容器的日志配置
docker inspect <容器名> --format '{{json .HostConfig.LogConfig}}'
1. 清空某个容器的日志(不重启容器,安全)
truncate -s 0 $(docker inspect <容器名> --format '{{.LogPath}}')根治:在 compose 里给所有服务配日志轮转(max-size + max-file):
# compose.yaml 全局锚点,所有服务引用
x-logging: &default-logging
driver: json-file
options:
max-size: "16m"
max-file: "3"
services:
wp:
logging: *default-logging
nginx:
logging: *default-loggingmax-size: 16m+max-file: 3意味着单容器日志最多 48MB- 已经跑起来的容器需要重建才生效(
up -d会重建) - 不要用
driver: none:虽然省空间,但出问题时完全没有日志可查
第五步:MySQL 相关的大文件(风险最高)
-- 看 binlog 占用
SHOW BINARY LOGS;
-- 看当前正在写哪个(这个不能删)
SHOW MASTER STATUS;
-- 删除指定时间之前的 binlog(谨慎:删了就无法恢复到该时间点)
PURGE BINARY LOGS BEFORE '2026-08-01 00:00:00';
-- 或按文件删(保留最后一个)
PURGE BINARY LOGS TO 'mysql-bin.000123';binlog 清理的三个前提:① 有近期全量备份 ② 确认不需要按时间点恢复 ③ 保留时间满足合规要求。三个都不满足就别删,改为调小
expire_logs_days让它自动轮转。
-- 更安全的做法:设自动过期(默认 30 天,可调小)
SET GLOBAL expire_logs_days = 7;
-- 写入 my.cnf [mysqld] expire_logs_days=7第六步:被删除但空间没释放(经典坑)
df 显示磁盘满,但 du 加起来远小于磁盘总量——说明有进程还持有已删除文件的句柄。
# 找出「已删除但被进程占用」的文件
lsof +L1 2>/dev/null | head -20
1. 或
lsof | grep deleted | awk '{print $1, $2, $7, $9}' | sort -k3 -rn | head -10
1. 典型场景:日志被 rm 了,但 nginx/apache 还拿着句柄,空间不释放
1. 处理:重启那个进程(或让它重新打开日志文件)
1. nginx -s reopen ← 无需重启,重新打开日志文件
1. systemctl restart xxx- 永远不要用
rm删正在被写入的日志:用truncate -s 0,或配 logrotate - nginx 可以用
nginx -s reopen重开日志,无需重启服务 - 容器场景下
truncate容器日志路径即可,不影响容器运行
第七步:预防(比清理更重要)
| 措施 | 配置位置 | 效果 |
|---|---|---|
| 容器日志轮转 | compose logging 锚点 | 单容器日志封顶 48MB |
| journald 限额 | /etc/systemd/journald.conf | 系统日志封顶 |
| binlog 自动过期 | my.cnf expire_logs_days | MySQL binlog 自动轮转 |
| 备份保留策略 | backup.sh 里删 N 天前 | 备份不无限累积 |
| 磁盘告警 | cron + 阈值检查 | 80% 时提前收到通知 |
最后一条最容易被忽略——等磁盘 100% 才发现就晚了,很多服务在写不进日志时才开始报错,排查方向会被误导。
# 简单的磁盘告警脚本(放进 cron,每天跑)
#!/bin/bash
THRESHOLD=80
USE=$(df / | awk 'NR==2 {gsub("%",""); print $5}')
if [ "$USE" -ge "$THRESHOLD" ]; then
echo "磁盘使用 ${USE}%,超过 ${THRESHOLD}%" | \
mail -s "磁盘告警 $(hostname)" you@example.com
fi总结
- 先看 df -i:有空间但写不进,八成是 inode 满了
- 按风险从低到高清理:包缓存 → journald → docker 镜像 → 容器日志 → 最后才动 binlog 和备份
- 容器日志默认无上限,必须配 max-size,这是最高频的隐性占用
- df 满但 du 小 = 有进程持有已删文件,用 lsof +L1 找,重启进程或 reopen 日志
- 80% 就该告警,等到 100% 时故障现象会误导排查方向