一次 Docker overlay2 撑爆根分区的线上修复记录
本文最后更新于 2 天前
一次 Docker overlay2 撑爆根分区的线上修复记录
这次故障发生在一台 GPU 推理服务器上。
表面现象是磁盘满了,实际问题比“删点文件”复杂得多:Docker overlay2 占用失控、容器日志无限增长、部分已删除文件仍被进程持有,最后连 Docker 自己的清理命令都无法释放空间。
本文记录完整排查和修复过程。
1. 问题背景
服务器主要跑几个 AI 推理服务:
- Triton Inference Server
- TensorRT Engine 构建任务
- Python 后处理服务
- Nginx 网关
- Prometheus Node Exporter
机器配置如下:
| 项目 | 配置 |
|---|---|
| OS | Ubuntu 22.04 |
| Docker | 27.x |
| 文件系统 | ext4 |
| Docker Root Dir | /var/lib/docker |
| GPU | RTX 4090 |
| 推理框架 | TensorRT / Triton |
| 日志驱动 | json-file |
业务侧最早反馈是:
推理接口偶发 502,重启容器后短暂恢复,但十几分钟后再次异常。
这类问题很容易被误判成模型服务崩溃、GPU 显存不足、Nginx upstream 超时。实际根因在磁盘。
2. 现象与日志
首先看系统磁盘:
df -h输出:
Filesystem Size Used Avail Use% Mounted on
/dev/sda1 197G 197G 0 100% /
tmpfs 32G 1.8M 32G 1% /run
/dev/sdb1 1.8T 628G 1.1T 37% /data根分区已经 100%。
继续看 inode:
df -i输出:
Filesystem Inodes IUsed IFree IUse% Mounted on
/dev/sda1 13107200 12988401 118799 100% /
/dev/sdb1 122101760 2134419 119967341 2% /data磁盘空间和 inode 都接近耗尽。
这时 Docker 状态已经不稳定:
docker ps卡住十几秒才返回。
系统日志也开始报错:
journalctl -xe关键日志:
systemd-journald[482]: Failed to write entry to /var/log/journal/...: No space left on device
dockerd[1361]: failed to register layer: write /var/lib/docker/image/overlay2/layerdb/tmp/write-set-xxxx: no space left on device
containerd[1228]: failed to create shim task: no space left on device
nginx[24918]: connect() failed (111: Connection refused) while connecting to upstreamTriton 容器日志里也出现了写入失败:
docker logs triton-server --tail 100输出:
E0515 03:17:42.918642 1 logging.cc:43] failed to flush log file: No space left on device
E0515 03:17:43.104811 1 model_repository_manager.cc:1297] failed to update model repository index到这里可以确定:
不是 GPU 问题,不是模型问题,而是根分区被 Docker 打满后引发的连锁故障。
3. 快速定位大目录
先看根目录下的空间分布:
sudo du -xhd1 / | sort -hr | head -20输出:
188G /
184G /var
2.1G /usr
1.2G /opt
612M /lib继续查 /var:
sudo du -xhd1 /var | sort -hr | head -20输出:
181G /var/lib
2.7G /var/log
312M /var/cache再看 Docker 目录:
sudo du -xhd1 /var/lib/docker | sort -hr输出:
181G /var/lib/docker
147G /var/lib/docker/overlay2
26G /var/lib/docker/containers
5.8G /var/lib/docker/buildkit
1.2G /var/lib/docker/image重点已经很清楚:
/var/lib/docker/overlay2占用 147G/var/lib/docker/containers占用 26G- BuildKit cache 也有 5.8G
这不是单点文件膨胀,而是 Docker 存储整体失控。
4. 第一条错误路径:直接 prune
很多人第一反应是:
docker system prune -af这次执行后只释放了不到 2G:
Deleted Images:
untagged: registry.local/infer-preprocess:<none>
deleted: sha256:1e4d...
Total reclaimed space: 1.73GB问题依旧:
df -h /Filesystem Size Used Avail Use% Mounted on
/dev/sda1 197G 195G 1.6G 100% /这说明至少存在两类问题:
- Docker 认为这些层仍然被引用,所以不会清理;
- overlay2 里存在 Docker 元数据已经无法关联的残留目录。
生产环境不要看到 overlay2 大就直接执行:
sudo rm -rf /var/lib/docker/overlay2/*这会直接破坏镜像层、容器可写层、merged 目录关系。轻则容器启动失败,重则 Docker 元数据损坏。
5. 排查容器日志是否失控
先查 Docker json 日志:
sudo find /var/lib/docker/containers \
-name "*-json.log" \
-type f \
-exec du -h {} \; | sort -hr | head -20输出:
14G /var/lib/docker/containers/2f8c.../2f8c...-json.log
8.7G /var/lib/docker/containers/a91d.../a91d...-json.log
2.9G /var/lib/docker/containers/74be.../74be...-json.log
1.3G /var/lib/docker/containers/bbe2.../bbe2...-json.log定位对应容器:
docker ps -a --no-trunc | grep 2f8c输出:
2f8c6d7a... registry.local/triton:23.10 tritonserver ... triton-server查看日志内容:
sudo tail -n 20 /var/lib/docker/containers/2f8c*/2f8c*-json.log发现大量 TensorRT verbose 日志:
{"log":"[TRT] [V] Layer Conv_317 input dimensions: ...\n","time":"2026-05-15T03:10:21.713912831Z"}
{"log":"[TRT] [V] Tactic 7 time: 0.034ms\n","time":"2026-05-15T03:10:21.714018377Z"}根因之一确认:
容器使用 Docker 默认 json-file 日志驱动,但没有配置日志轮转,推理服务 verbose 日志持续刷盘。
临时截断日志:
sudo truncate -s 0 /var/lib/docker/containers/2f8c*/2f8c*-json.log
sudo truncate -s 0 /var/lib/docker/containers/a91d*/a91d*-json.log再次查看:
df -h /输出:
Filesystem Size Used Avail Use% Mounted on
/dev/sda1 197G 171G 27G 87% /第一阶段回收 26G,系统从完全不可写恢复到可操作状态。
6. 排查 deleted 文件句柄
磁盘满时还有一个经典陷阱:
文件已经被删除,但进程仍然持有文件句柄,空间不会释放。
检查:
sudo lsof +L1输出:
COMMAND PID USER FD TYPE DEVICE SIZE/OFF NLINK NODE NAME
python3 18421 root 7w REG 8,1 11891384320 0 917351 /var/log/infer/postprocess.log (deleted)
tritonser 19277 root 12w REG 8,1 6979321856 0 917482 /tmp/triton_verbose.log (deleted)两个被删除但仍占用空间的文件,加起来约 18G。
对 Python 服务先尝试 HUP:
sudo kill -HUP 18421无效。
这个服务没有实现日志 reopen,只能重启对应容器:
docker restart infer-postprocess
docker restart triton-server再次确认:
sudo lsof +L1 | head
df -h /输出:
Filesystem Size Used Avail Use% Mounted on
/dev/sda1 197G 153G 45G 78% /第二阶段释放 18G。
7. 深挖 overlay2 为什么还有 147G
日志和 deleted FD 清掉后,根分区仍然使用 78%。继续看 overlay2:
sudo du -xhd1 /var/lib/docker/overlay2 | sort -hr | head -30输出:
147G /var/lib/docker/overlay2
18G /var/lib/docker/overlay2/8f2a4c...
15G /var/lib/docker/overlay2/3c91d2...
13G /var/lib/docker/overlay2/d91ab7...
11G /var/lib/docker/overlay2/f73aa2...检查 Docker 视角的占用:
docker system df -v输出节选:
TYPE TOTAL ACTIVE SIZE RECLAIMABLE
Images 18 9 42.1GB 16.7GB (39%)
Containers 12 8 21.4GB 2.1GB (9%)
Local Volumes 7 5 18.8GB 3.2GB (17%)
Build Cache 76 0 5.8GB 5.8GBDocker 认为自身主要占用:
- Images 42.1G
- Containers 21.4G
- Volumes 18.8G
- Build Cache 5.8G
合计不到 90G。
但 /var/lib/docker 实际还有 153G。这里存在约 60G 差异。
这类差异通常来自:
- overlay2 孤儿目录;
- 容器上层写入过大;
- BuildKit 或中断构建残留;
- Docker 元数据损坏;
- 仍有进程挂载 merged 目录。
8. 找出最大的 upperdir
容器的可写层通常在 overlay2 的 diff 目录。
先找最大的 diff:
sudo find /var/lib/docker/overlay2 -maxdepth 2 -type d -name diff \
-exec du -sh {} \; | sort -hr | head -20输出:
18G /var/lib/docker/overlay2/8f2a4c.../diff
15G /var/lib/docker/overlay2/3c91d2.../diff
13G /var/lib/docker/overlay2/d91ab7.../diff看其中一个目录:
sudo du -xhd2 /var/lib/docker/overlay2/8f2a4c*/diff | sort -hr | head输出:
18G /var/lib/docker/overlay2/8f2a4c.../diff
17G /var/lib/docker/overlay2/8f2a4c.../diff/root/.cache
16G /var/lib/docker/overlay2/8f2a4c.../diff/root/.cache/huggingface这说明某个容器把 HuggingFace 模型缓存写进了容器层,而不是 volume 或宿主机目录。
继续找这个 layer 属于哪个容器:
docker inspect $(docker ps -aq) \
--format '{{.Name}} {{.GraphDriver.Data.UpperDir}}' | grep '8f2a4c'输出:
/model-warmup /var/lib/docker/overlay2/8f2a4c.../diff容器 model-warmup 的可写层写了 18G。
确认容器内路径:
docker exec model-warmup du -sh /root/.cache/huggingface输出:
17G /root/.cache/huggingface这个问题不是 Docker bug,是部署方式错误。
模型缓存不应该写进容器层。
9. 处理容器层膨胀
先把缓存迁出来:
mkdir -p /data/model-cache/huggingface
docker cp model-warmup:/root/.cache/huggingface /data/model-cache/修改 compose:
services:
model-warmup:
image: registry.local/model-warmup:2026-05-15
volumes:
- /data/model-cache/huggingface:/root/.cache/huggingface
- /data/models:/models
environment:
HF_HOME: /root/.cache/huggingface重建容器:
docker compose up -d --force-recreate model-warmup删除旧容器后再清理:
docker rm $(docker ps -aq -f status=exited)
docker system prune -f释放后:
df -h /输出:
Filesystem Size Used Avail Use% Mounted on
/dev/sda1 197G 132G 65G 68% /10. 清理 BuildKit 缓存
BuildKit 缓存虽然不是最大头,但这类服务器经常反复构建 TensorRT 镜像,不清理迟早会变成问题。
查看:
docker builder du输出:
ID RECLAIMABLE SIZE
wz0k... true 2.1GB
d7n1... true 1.8GB
p9aa... true 1.4GB清理:
docker builder prune -af输出:
Total reclaimed space: 5.76GB注意,不建议把这条命令简单丢到每天凌晨执行。CI 服务器可以清,但推理服务器上如果构建缓存用于加速紧急回滚镜像,清掉会增加恢复时间。
11. 排查 overlay2 孤儿层
这一步最容易误删。
先列出 Docker 当前知道的 UpperDir、LowerDir、WorkDir、MergedDir:
docker inspect $(docker ps -aq) \
--format '{{.Name}}
Upper={{.GraphDriver.Data.UpperDir}}
Work={{.GraphDriver.Data.WorkDir}}
Merged={{.GraphDriver.Data.MergedDir}}
Lower={{.GraphDriver.Data.LowerDir}}
' > /tmp/docker-layer-used.txt列出 overlay2 实际目录:
sudo find /var/lib/docker/overlay2 -maxdepth 1 -mindepth 1 -type d \
-printf '%f\n' | sort > /tmp/overlay2-all.txt提取正在被容器引用的目录 ID:
grep -oE '/var/lib/docker/overlay2/[^/:]+' /tmp/docker-layer-used.txt \
| awk -F/ '{print $NF}' \
| sort -u > /tmp/overlay2-used-by-containers.txt对比:
comm -23 /tmp/overlay2-all.txt /tmp/overlay2-used-by-containers.txt | head -50这只能说明“不被容器直接引用”,不能说明可以删除,因为镜像层也在 overlay2 里。
继续检查 Docker image layerdb:
sudo find /var/lib/docker/image/overlay2/layerdb -type f -name cache-id \
-exec cat {} \; | sort -u > /tmp/overlay2-used-by-images.txt合并引用:
cat /tmp/overlay2-used-by-containers.txt /tmp/overlay2-used-by-images.txt \
| sort -u > /tmp/overlay2-used.txt找疑似孤儿目录:
comm -23 /tmp/overlay2-all.txt /tmp/overlay2-used.txt > /tmp/overlay2-orphan-candidates.txt查看候选目录大小:
while read id; do
sudo du -sh "/var/lib/docker/overlay2/$id" 2>/dev/null
done < /tmp/overlay2-orphan-candidates.txt | sort -hr | head -30输出:
12G /var/lib/docker/overlay2/d91ab7...
9.4G /var/lib/docker/overlay2/f73aa2...
6.8G /var/lib/docker/overlay2/aa91c3...删除前,还要确认没有进程挂载它们:
mount | grep '/var/lib/docker/overlay2/d91ab7'
sudo lsof | grep '/var/lib/docker/overlay2/d91ab7'如果都没有输出,再记录候选清单:
sudo cp /tmp/overlay2-orphan-candidates.txt /data/backup/overlay2-orphan-candidates-$(date +%F).txt
sudo cp /tmp/docker-layer-used.txt /data/backup/docker-layer-used-$(date +%F).txt最终清理前先停止 Docker,避免运行中 layer 变化:
sudo systemctl stop docker
sudo systemctl stop containerd删除确认过的孤儿层:
while read id; do
sudo rm -rf "/var/lib/docker/overlay2/$id"
done < /tmp/overlay2-orphan-candidates.txt启动 Docker:
sudo systemctl start containerd
sudo systemctl start docker检查:
docker ps
docker system df
df -h /结果:
Filesystem Size Used Avail Use% Mounted on
/dev/sda1 197G 91G 106G 47% /至此,空间恢复到安全水位。
12. 最终恢复结果
| 指标 | 修复前 | 修复后 |
|---|---|---|
| 根分区使用率 | 100% | 47% |
/var/lib/docker/overlay2 |
147G | 64G |
/var/lib/docker/containers |
26G | 740M |
| BuildKit Cache | 5.8G | 120M |
| inode 使用率 | 100% | 34% |
docker ps 响应时间 |
12s ~ 30s | < 1s |
| SSH 登录耗时 | 20s+ | 正常 |
| Triton 502 | 高频出现 | 消失 |
业务侧 P95 延迟也恢复正常:
| 指标 | 修复前 | 修复后 |
|---|---|---|
| 推理接口 P50 | 42ms | 39ms |
| 推理接口 P95 | 980ms | 71ms |
| 推理接口 P99 | 3.8s | 142ms |
| 502 比例 | 2.7% | 0% |
延迟暴涨不是模型变慢,而是容器、日志、文件系统写入失败导致服务抖动。
13. 根因总结
这次事故不是单一原因。
根因链路如下:
flowchart TD
A[TensorRT verbose 日志持续输出] --> B[Docker json-file 日志无限增长]
C[HuggingFace 缓存写入容器层] --> D[overlay2 upperdir 膨胀]
E[BuildKit 构建中断] --> F[overlay2 残留层]
G[日志文件被删除但 FD 未释放] --> H[空间无法回收]
B --> I[根分区 100%]
D --> I
F --> I
H --> I
I --> J[dockerd 写 metadata 失败]
I --> K[journald 写入失败]
I --> L[容器随机退出]
L --> M[Nginx upstream 502]
最终结论:
Docker overlay2 撑爆磁盘,本质是日志、缓存、镜像层、容器可写层和构建缓存同时缺少边界控制。
14. 永久修复:配置 Docker 日志轮转
修改 /etc/docker/daemon.json:
{
"log-driver": "json-file",
"log-opts": {
"max-size": "100m",
"max-file": "3"
}
}重启 Docker:
sudo systemctl restart docker验证:
docker info | grep -A5 "Logging Driver"如果是新部署环境,建议直接使用 local driver:
{
"log-driver": "local",
"log-opts": {
"max-size": "100m"
}
}local 日志驱动对磁盘占用更友好,也会做压缩。线上长期运行的服务,不建议裸奔使用无限制的 json-file。
15. 永久修复:迁移 Docker data-root
根分区不应该承载 Docker 数据目录。
推荐把 Docker 数据迁到独立数据盘:
sudo systemctl stop docker
sudo systemctl stop containerd
sudo mkdir -p /data/docker
sudo rsync -aHAX --numeric-ids /var/lib/docker/ /data/docker/修改 /etc/docker/daemon.json:
{
"data-root": "/data/docker",
"log-driver": "local",
"log-opts": {
"max-size": "100m"
}
}启动:
sudo systemctl start containerd
sudo systemctl start docker确认:
docker info | grep "Docker Root Dir"输出应为:
Docker Root Dir: /data/docker确认服务正常后,再清理旧目录:
sudo mv /var/lib/docker /var/lib/docker.bak.$(date +%F)观察一周没有问题后再删除:
sudo rm -rf /var/lib/docker.bak.2026-05-15不要迁移后立刻删旧目录。出问题时还能回滚。
16. 永久修复:模型缓存不要写入容器层
AI 推理环境尤其容易踩这个坑。
这些目录不应该留在容器可写层:
/root/.cache
/root/.cache/huggingface
/root/.cache/torch
/tmp/triton
/tmp/tensorrt
/workspace/build推荐目录结构:
/data
├── docker
├── models
├── engines
├── model-cache
│ ├── huggingface
│ ├── torch
│ └── tensorrt
├── logs
└── datasetsCompose 示例:
services:
triton-server:
image: registry.local/triton:23.10
runtime: nvidia
volumes:
- /data/models:/models:ro
- /data/engines:/engines
- /data/model-cache:/root/.cache
- /data/logs/triton:/logs
environment:
NVIDIA_VISIBLE_DEVICES: all
HF_HOME: /root/.cache/huggingface
command:
- tritonserver
- --model-repository=/models
- --log-verbose=0重点是:
镜像只放程序,模型和缓存放宿主机数据盘,日志单独管理。
17. 巡检脚本
下面这个脚本用于检查:
- 根分区使用率
- inode 使用率
- Docker 日志大文件
- deleted 文件句柄
- overlay2 大目录
- BuildKit cache
保存为:
/usr/local/bin/docker-storage-check.sh内容:
#!/usr/bin/env bash
set -euo pipefail
ROOT_THRESHOLD=80
INODE_THRESHOLD=80
LOG_THRESHOLD="+1G"
echo "===== filesystem usage ====="
df -h /
echo
echo "===== inode usage ====="
df -i /
root_usage=$(df / | awk 'NR==2 {gsub("%","",$5); print $5}')
inode_usage=$(df -i / | awk 'NR==2 {gsub("%","",$5); print $5}')
if [ "$root_usage" -ge "$ROOT_THRESHOLD" ]; then
echo "WARN: root filesystem usage is ${root_usage}%"
fi
if [ "$inode_usage" -ge "$INODE_THRESHOLD" ]; then
echo "WARN: root inode usage is ${inode_usage}%"
fi
echo
echo "===== docker system df ====="
docker system df || true
echo
echo "===== large docker json logs ====="
sudo find /var/lib/docker/containers \
-name "*-json.log" \
-type f \
-size "$LOG_THRESHOLD" \
-exec du -h {} \; 2>/dev/null | sort -hr || true
echo
echo "===== deleted files still held by processes ====="
sudo lsof +L1 2>/dev/null | awk 'NR==1 || $7 > 1073741824 {print}' || true
echo
echo "===== largest overlay2 diff dirs ====="
sudo find /var/lib/docker/overlay2 \
-maxdepth 2 \
-type d \
-name diff \
-exec du -sh {} \; 2>/dev/null | sort -hr | head -20 || true加执行权限:
sudo chmod +x /usr/local/bin/docker-storage-check.sh配置 cron:
sudo crontab -e加入:
*/15 * * * * /usr/local/bin/docker-storage-check.sh >> /var/log/docker-storage-check.log 2>&118. 回滚方案
修复 overlay2 这类问题一定要有回滚路径。
18.1 日志截断回滚
truncate 日志不可逆,但不会影响容器运行。最多损失历史日志。
18.2 Docker data-root 迁移回滚
如果迁移后 Docker 异常:
sudo systemctl stop docker
sudo systemctl stop containerd恢复 /etc/docker/daemon.json 中的 data-root:
{
"data-root": "/var/lib/docker"
}如果旧目录还在:
sudo mv /var/lib/docker.bak.2026-05-15 /var/lib/docker启动:
sudo systemctl start containerd
sudo systemctl start docker18.3 overlay2 孤儿层删除回滚
这一步最麻烦。
删除前至少保留:
/tmp/overlay2-all.txt
/tmp/overlay2-used.txt
/tmp/overlay2-orphan-candidates.txt
/tmp/docker-layer-used.txt如果删除后容器异常,优先重新拉镜像和重建容器,不建议手工拼回 overlay2。
docker compose pull
docker compose up -d --force-recreateoverlay2 不是业务数据目录,不应该依赖它恢复业务数据。
19. 不建议做的事
19.1 不要直接删除 overlay2
错误示例:
sudo rm -rf /var/lib/docker/overlay2/*这基本等于破坏 Docker 存储。
19.2 不要盲目定时 prune
错误示例:
0 3 * * * docker system prune -af这会带来几个问题:
- 清掉构建缓存,CI 变慢;
- 清掉预拉取镜像,回滚变慢;
- 误删未使用但即将切换的镜像;
- 推理服务冷启动时间变长。
19.3 不要把模型下载到容器层
错误示例:
RUN python download_model.py如果模型很大,镜像会膨胀;如果运行时再下载,容器层会膨胀。更好的做法是模型仓库独立挂载。
20. 推荐监控项
Prometheus 建议至少加这些指标:
node_filesystem_avail_bytes
node_filesystem_size_bytes
node_filesystem_files_free
node_filesystem_files
node_filesystem_readonlyDocker 侧建议采集:
/var/lib/docker/overlay2 size
/var/lib/docker/containers size
large json log count
deleted fd size
docker system df告警阈值:
| 指标 | Warning | Critical |
|---|---|---|
| 根分区使用率 | 75% | 85% |
| inode 使用率 | 70% | 85% |
| 单个 json log | 1G | 5G |
| overlay2 总量 | 70% of disk | 85% of disk |
| deleted FD 占用 | 2G | 10G |
21. 架构调整前后对比
事故前:
flowchart LR
A[Triton 容器] --> B[/var/lib/docker/overlay2]
C[Python 服务日志] --> D[/var/lib/docker/containers]
E[模型缓存] --> B
F[BuildKit Cache] --> G[/var/lib/docker/buildkit]
B --> H[根分区]
D --> H
G --> H
问题是所有东西都压在根分区上。
调整后:
flowchart LR
A[Triton 容器] --> B[/data/models]
A --> C[/data/engines]
A --> D[/data/logs]
A --> E[/data/model-cache]
F[Docker data-root] --> G[/data/docker]
H[根分区] --> I[系统文件]
根分区只承载系统文件,Docker、模型、日志、缓存全部进入数据盘。
22. 配图建议
文章配图可以放在:
/source/img/docker-overlay2-disk-full/建议准备三张图:
cover.png
overlay2-structure.png
docker-storage-after.png22.1 cover.png
内容:
Docker overlay2 -> root filesystem 100% -> containers crash22.2 overlay2-structure.png
内容:
lowerdir + upperdir + workdir -> merged22.3 docker-storage-after.png
内容:
/data/docker
/data/models
/data/logs
/data/model-cache这三张图足够,不需要堆太多示意图。
23. 结论
这次故障的核心不是“Docker 占用大”,而是 Docker 存储没有边界。
最终处理顺序是:
先恢复可写空间
再处理日志
再处理 deleted FD
再定位容器可写层
再清 BuildKit
最后谨慎处理 overlay2 孤儿层长期方案也很明确:
- Docker data-root 不放根分区;
- Docker 日志必须限制大小;
- 模型、缓存、日志必须挂载到数据盘;
- 不要把 HuggingFace、TensorRT、Torch cache 写进容器层;
- overlay2 清理必须先判断引用关系;
- 磁盘和 inode 必须进入监控。
生产环境里,磁盘打满不是小问题。它会让 Docker、journald、systemd、SSH、业务容器一起退化。等到 Use% 变成 100% 再处理,通常已经晚了。




















