Docker 存储清理与防膨胀实践
背景:线上 40G 系统盘某天突然 100% 写满,导致 MariaDB 写入抛
error 1114 The table 'article' is full,周报落库失败。经排查,根因是日志/构建缓存堆积 + 已删除文件仍被进程持有句柄。本文记录排查思路、清理手段以及长效防御措施。
一、问题表象与根因定位
1.1 表象
- 应用日志:
java.sql.SQLException: The table 'article' is full(MySQL error 1114) - 服务器:
df -h /显示 40G 盘已用 39G,Avail 0
1.2 易错点:InnoDB 表空间没有硬上限
MySQL/MariaDB 的 error 1114 直觉上像是"表大小受限",但 InnoDB 默认自扩展表空间(innodb_data_file_path 未指定 max)没有硬上限。出现此错误基本等价于磁盘或 inode 耗尽 ,导致表空间文件无法继续扩展。先查磁盘,再查数据库。
1.3 定位思路:df 与 du 不一致的经典陷阱
bash
df -h / # 文件系统视角:看挂载点总容量
du -sh /var/* # 目录项视角:累加可见文件
如果 df 显示已用 35G,但 du 只统计到 20G,中间差的 15G 就是已删除但仍被进程持有句柄的文件。常见来源:
- 进程(如 Java、Gradle Daemon、Nginx)打开着的日志被
rm掉 - Docker 容器内进程持有的文件
定位方法:
bash
lsof +L1 | grep deleted # 列出所有"已删除但仍有进程打开"的文件
释放方法:杀掉持有句柄的进程,或重启对应容器:
bash
pkill -9 -f GradleDaemon # 例:释放被删的 .gradle 缓存
docker compose restart nginx nacos redis service
二、一次性清理操作(按安全度从高到低)
2.1 应用日志(bind-mount 目录)
项目中 nginx / nacos / redis 的日志通过 ./logs/* bind mount 到宿主机,不受 Docker json-file 日志驱动的 max-size 限制,会无限增长。
bash
cd /data/jiahetng
du -sh ./logs/* # 先看各服务占用
find ./logs/nginx -name "*.log" -mtime +7 -delete
find ./logs/nacos -name "*.log" -mtime +7 -delete
find ./logs/redis -name "*.log" -mtime +7 -delete
2.2 Docker 自身资源
bash
docker system df # 查看镜像/容器/卷/构建缓存占用
docker image prune -a -f # 删除未被使用的镜像
docker builder prune -a -f # 清理构建缓存(buildkit 多阶段构建的中间层)
docker volume prune -f # 删除未被容器使用的 volume(⚠️ 确认无重要数据)
docker system prune -a -f 是上面几条的组合拳,但会同时删未使用的镜像和停止的容器,确认后再用。
2.3 宿主机残留
/root/.gradle:CI/CD 或本地构建残留的依赖缓存(本次 15G)/var/log/.osbak:云厂商 OS 备份残留(本次 6.4G),确认内容后rm -rfjournalctl --vacuum-size=500M:systemd 日志(若在/var/log/journal而非 tmpfs)
三、长效防御:让存储不再膨胀
3.1 容器日志轮转(json-file 驱动)
在 docker-compose.yaml 每个服务下统一配置:
yaml
logging:
driver: "json-file"
options:
max-size: "20m" # 单个日志文件上限
max-file: "10" # 最多保留 10 个轮转文件
⚠️ 重要限制 :json-file 的 max-size 只对 Docker 管理的容器 stdout/stderr 日志生效。bind-mount 到宿主机的应用日志(如 ./logs/nginx:/var/log/nginx)走的是容器内应用自己写文件的路径,不受此约束,必须用 logrotate 或应用侧配置。
3.2 bind-mount 日志的 logrotate
在宿主机 /etc/logrotate.d/jiahetng 中配置:
conf
/data/jiahetng/logs/nginx/*.log {
daily
rotate 7
compress
missingok
notifempty
copytruncate
}
/data/jiahetng/logs/nacos/*.log {
daily
rotate 7
compress
missingok
notifempty
copytruncate
}
/data/jiahetng/logs/redis/*.log {
daily
rotate 7
compress
missingok
notifempty
copytruncate
}
copytruncate 是关键:先复制再截断原文件,避免 Nginx/Nacos 仍持有原句柄导致"截断后空间不释放"。
3.3 镜像层瘦身
- 多阶段构建(如
node:20-alpine AS builder→nginx:alpine),运行镜像不含编译依赖 .dockerignore排除node_modules、dist、.git等- 基础镜像优先
*-alpine
3.4 定期巡检脚本(cron)
bash
#!/bin/bash
# /etc/cron.daily/jiahetng-disk-check
THRESHOLD=85
USAGE=$(df / | awk 'NR==2 {print $5}' | tr -d '%')
if [ "$USAGE" -ge "$THRESHOLD" ]; then
docker system prune -f
find /data/jiahetng/logs -name "*.log" -mtime +7 -delete
fi
四、本次事故复盘
| 占用项 | 大小 | 处理 |
|---|---|---|
./logs/* bind-mount 日志 |
5.5G | 删 7 天前日志 + 加 logrotate |
/root/.gradle 构建缓存 |
15G | rm -rf + pkill GradleDaemon 释放句柄 |
/var/log/.osbak 厂商备份 |
6.4G | rm -rf |
| Docker overlay2(活动镜像) | 2.9G | 不动 |
| MariaDB 数据 | 227M | 不动 |
核心教训:
- MySQL
table is full≠ 表真满了,先看磁盘。 df与du对不上时,一定有"幽灵文件"被进程持有,lsof +L1找出来,杀进程或重启容器。json-file的max-size只管 Docker 自己的 stdout,bind-mount 的应用日志必须单独轮转。