容器文件 Quota:容器为什么会把宿主机磁盘写满?怎么限制?
实验环境:Ubuntu 24.04 / 内核 6.8.0-106-generic / Cgroup v2 / Docker 29.1.3(overlayfs,根分区 ext4)/ 华为云 FlexusX 实例 8C16G
一、引子:一块 40G 的系统盘,凌晨被写到了 100%
“告警:ecs-xxxx 磁盘使用率 100%,服务全部 5xx。”
登上去一看,df -h红得刺眼,根目录/满了。谁写的?翻到/var/lib/containerd/io.containerd.snapshotter.v1.overlayfs/snapshots/<N>/fs下某个容器的可写层里(Docker 29 用的是 containerd snapshotter,不再是老版本的/var/lib/docker/overlay2/...),躺着一个 30GB 的core.dump和一个无限增长的app.log。
这不是段子,是运维日常。容器的"隔离"是名字空间(namespace)层面的,磁盘空间默认并不隔离——所有容器共享宿主机的文件系统(至少是/var/lib/docker所在的那块盘)。一个容器往自己根目录里狂写,填的是宿主机的盘。本篇就用真机命令,复现这个问题,并给出可行、可落地的限制方案。
二、复现:容器写的文件,占的是宿主机的盘
先记录基线。实验机根盘是/dev/vda1,40G,当前已用 5.7G:
df-h/var/lib/docker# Filesystem Size Used Avail Use% Mounted on# /dev/vda1 40G 5.7G 32G 15% /USED_BEFORE=5882856# KB启动一个常驻容器,往可写层里写 1.5GB(注意这里走的是容器可写层的 overlay upper,数据最终落在宿主机磁盘):
dockerrun-d--namefilltest ubuntu:24.04sleep120dockerexecfilltestbash-c"dd if=/dev/zero of=/fill.bin bs=1M count=1500 2>&1 | tail -1"# 1572864000 bytes (1.6 GB, 1.5 GiB) copied, 0.944563 s, 1.7 GB/s再看宿主机磁盘(此时容器还活着,文件在可写层里):
df-h/var/lib/docker# /dev/vda1 40G 7.1G 31G 19% / ← 涨了 1.5GUSED_MID=7418948占用增量(MB)=1500确认这个文件确实在容器的可写层里:
dockerexecfilltestls-l/fill.bin# -rw-r--r-- 1 root root 1572864000 ... /fill.bin删掉容器,空间立刻回来:
dockerrm-ffilltestdf-h/var/lib/docker# /dev/vda1 40G 5.7G 32G 15% / ← 回落铁证:容器往自己可写层写 1.5GB,宿主机磁盘实打实涨了 1.5GB;容器删掉,空间才释放。如果这是个长期运行的容器、且写入不停止,宿主机的盘就会被写满——服务集体瘫痪。
风险点主要有两类:日志(应用疯狂打日志、没人轮转)、临时文件/核心转储(程序异常、core dump 写到容器里)。它们都默认落在可写层,等于直接吃宿主机磁盘。
三、第一反应:--storage-opt size能不能限?
很多人第一反应是 Docker 的--storage-opt size=1G,文档说它能给容器可写层设配额。我们在实验机上试试:
dockerrun--rm--namesizetest --storage-optsize=1G ubuntu:24.04bash-c" df -h / | tail -2 dd if=/dev/zero of=/big.bin bs=1M count=2000 2>&1 | tail -1 ls -l /big.bin df -h / | tail -2 "输出(注意,没有任何报错):
Filesystem Size Used Avail Use% Mounted on overlay 40G 5.7G 32G 15% / ← 看到的是整块宿主机盘 ... 2000+0 records out 2097152000 bytes (2.1 GB, 2.0 GiB) copied, 1.24491 s, 1.7 GB/s -rw-r--r-- 1 root root 2097152000 ... /big.bin ← 写到了 2GB! overlay 40G 7.6G 30G 21% /重要发现(与官方文档有出入,且更危险):在本文环境(Docker 29.1.3 + overlayfs +ext4 根分区)下,--storage-opt size=1G既没报错,也没生效——容器照常写出 2GB,且df看到的仍是整块 40G 盘。它把这个限制静默忽略了。
为什么会这样?因为 overlay2 的 per-container 配额,底层依赖文件系统的 project quota(项目配额):
- 在XFS(挂载时带
pquota)上,overlay2 能给每个容器分配一个 XFS project ID,用xfs_quota精准限制该容器可写层大小——此时--storage-opt size才真正有效; - 在ext4上,除非你专门用带
prjquota挂载选项的 ext4 并做相应配置,否则 Docker 无法在该文件系统上创建带配额的子卷。本文实验机的根分区是普通 ext4,没有 project quota 支持,于是 Docker 选择静默忽略,而不是报错。
这比"直接报错"更坑:它不给你任何警告,你以为加了
--storage-opt size=1G就安全了,其实容器照样能把盘写爆。生产上绝不能只靠这个参数保命,尤其在 ext4 根分区上。
四、可行方案 A:XFS project quota(机制级验证)
既然 ext4 不行,我们用 XFS + project quota 把机制跑通。在实验机上创建一个 XFS 镜像文件,loop 挂载并启用prjquota:
ddif=/dev/zeroof=/root/xfs_quota.imgbs=1Mcount=1024status=none mkfs.xfs-q/root/xfs_quota.imgmkdir-p/root/xfsmntmount-oprjquota,loop /root/xfs_quota.img /root/xfsmntmount|grepxfsmnt# /root/xfs_quota.img on /root/xfsmnt type xfs (...,prjquota)给一个目录注册 project 100,并设置硬限 500MB:
mkdir-p/root/xfsmnt/projdir xfs_quota-x-c"project -s -p /root/xfsmnt/projdir 100"/root/xfsmnt xfs_quota-x-c"limit -p bhard=500m 100"/root/xfsmnt xfs_quota-x-c"report -p"/root/xfsmnt# Project ID Used Soft Hard# #100 0 0 512000 ← 500MB 硬限已生效试着往里写 700MB(超过配额):
ddif=/dev/zeroof=/root/xfsmnt/projdir/big.binbs=1Mcount=700# dd: error writing '.../big.bin': No space left on device# 501+0 records in# 500+0 records out# 524288000 bytes (524 MB, 500 MiB) copied, 0.190188 s, 2.8 GB/sls-l/root/xfsmnt/projdir/big.bin# -rw-r--r-- 1 root root 524288000 ... big.bin ← 正好卡在 500MBxfs_quota-x-c"report -p"/root/xfsmnt# #100 512000 0 512000 ← Used 触顶到 Hard机制验证成功:XFS project quota 把目录写入精准卡在 500MB,多一个字节都不行(No space left on device)。这正是--storage-opt size在 XFS 上能生效的底层原理——Docker 给每个容器分配独立 project ID,用xfs_quota限制。
生产落地:把 Docker 的>五、可行方案 B:日志大小限制
--log-opt容器日志(
docker logs读的那个 JSON 文件)是最容易悄悄吃满磁盘的东西。Docker 自带日志轮转:dockerrun-d--namelogtest2\--log-opt max-size=1m --log-opt max-file=2\ubuntu:24.04bash-c"while true; do echo\"log line ...\"; done"sleep6CID=$(dockerinspect logtest2--format"{{.Id}}")ls-l/var/lib/docker/containers/$CID/*-json.log*# -rw-r----- 1 root root 152376 ... /.../logtest2-json.log ← 当前文件 ~149KB# -rw-r----- 1 root root 1000013 ... /.../logtest2-json.log.1 ← 已轮转的一个满 1MBdu-sh/var/lib/docker/containers/$CID# 1.2M ← 总量被限制在 ~1.2MB6 秒高频打日志后,当前日志文件只有 ~149KB,另有一个已轮转的满 1MB 文件,总量被
max-file=2(保留 2 个文件,每个不超过max-size=1m)卡在 ~1.2MB。不限制的话,这个文件会无限增长直到撑爆磁盘。生产建议:所有长驻容器都加
--log-opt max-size --log-opt max-file,或在daemon.json里全局配置默认日志驱动与上限。六、可行方案 C:临时/内存数据用
--tmpfs限大小对于临时文件、Core dump 这类"丢了也无所谓"的数据,最干净的做法是根本不落盘——用
tmpfs(内存文件系统)并显式限制大小:dockerrun--rm--tmpfs/tmp:size=100m ubuntu:24.04bash-c" df -h /tmp | tail -1 echo '--- 写 200MB(应卡在 100M) ---' dd if=/dev/zero of=/tmp/t.bin bs=1M count=200 2>&1 | tail -3 "# Filesystem Size Used Avail Use% Mounted on# tmpfs 100M 0 100M 0% /tmp ← 严格限在 100M# 101+0 records in# 100+0 records out# 104857600 bytes (105 MB, 100 MiB) copied ← dd 写到 100MB 即止(No space left on device)这样即使程序在
/tmp里疯狂写,最多吃到 100M 内存(写到配额上限即报"磁盘满"),绝不会波及宿主机磁盘。代价是占内存、重启即失——适合明确"临时"语义的路径。七、方案 D(进阶):把 volume 放在带配额的文件系统上
如果你用 volume 持久化数据(如数据库),又想限大小,可把一个带 XFS project quota 的目录(参考第四节)通过 bind mount 挂进容器当 volume。容器写这个 volume 时,受底层 XFS 配额约束,同样写不爆。
八、生产排查与治理清单
当磁盘告警时,按这个顺序定位:
# 1) 谁占了多少(容器维度)dockerps-s# SIZE 列看每个容器可写层体积df-h/var/lib/docker# 总体水位# 2) 最大头是谁(到 overlay 可写层里找)du-sh/var/lib/containerd/io.containerd.snapshotter.v1.overlayfs/snapshots/*/fs2>/dev/null|sort-h|tail# 3) 是不是日志在涨ls-lh/var/lib/docker/containers/*/*.log|sort-k5-h|tail# 4) 容器在不在狂写(实时)iotop-o-P# 或 iostat -x 1 看某块盘的 w_await/%util治理的四条铁律:
- 日志轮转永远在线:
--log-opt max-size/max-file,或用json-file之外带轮转的驱动(如local)。- 高频写路径一律 volume:日志、临时文件、数据目录挂出去,别让它们堆在可写层。
- 镜像与数据清理:定期
docker system prune、配置镜像仓库保留策略、删掉停止的容器和无用镜像。- 磁盘监控 + 配额兜底:监控
/var/lib/docker所在盘使用率(告警阈值 80%);在能上 XFS pquota 的场景用--storage-opt size做 per-container 兜底,但别把它当唯一防线。九、扩展:为什么"默认不隔离",以及把配额真正落到生产
9.1 为什么容器磁盘默认不隔离
容器的隔离靠namespace,但 namespace 管的是"视图",不是"资源配额":
- Mount namespace:让容器看到自己的根文件系统视图,但它底下挂的,仍然是宿主机某块真实磁盘上的目录(
/var/lib/docker/...或 containerd 的snapshots/<N>/fs)。视图隔离 ≠ 空间隔离。- PID / Net / IPC namespace:管进程、网络、通信,和磁盘空间无关。
- 真正能限"用了多少磁盘"的,是文件系统配额(project quota)或cgroup 的 io/资源约束——而这些默认都没开。
所以"一个容器写爆宿主机"不是 bug,而是默认行为的必然结果:所有容器和宿主共享同一棵文件系统树,谁先写满谁就让大家一起挂。
一个最直观的证据:在容器里执行
df /,看到的盘符、总大小、已用空间和宿主机df /完全一样——因为它们本就是同一个挂载点,只是 namespace 给了不同的"视角"。这也解释了为什么单看容器内部的"剩余空间"会误导人:它显示的是整块宿主盘的余量,而不是"这个容器还能写多少"。真正要管的是"这个容器自己能写多少",那才需要本文的配额手段。9.2 生产级方案:让
--storage-opt size真正生效要让 per-container 配额在 Docker 上真正可用,得让 Docker 的数据根落在XFS +
pquota上。落地步骤(以挂一块独立云盘为例):# 1) 新盘格式化为 XFS(crc/finobt 打开,现代内核默认)mkfs.xfs-f-mcrc=1,finobt=1/dev/vdb# 2) 以 pquota 选项挂载mkdir-p/var/lib/dockermount-opquota /dev/vdb /var/lib/docker# 3) 在 /etc/fstab 固化(避免重启丢失 pquota)# /dev/vdb /var/lib/docker xfs defaults,pquota 0 0# 4) 配置 docker daemon 使用它(如已在用,迁移旧数据后改># /etc/docker/daemon.json: { "data-root": "/var/lib/docker", "storage-driver": "overlay2" }# systemctl restart docker此后
--storage-opt size=1G才会通过 XFS project quota 真正限制每个容器可写层的大小——这正是本文第四节用 loop 镜像验证过的同一套机制,只是从"演示"变成了"生产数据根"。其底层细节值得记住:XFS 的 project quota 是按"项目 ID(project ID)"做计费的,与用户、组都无关。Docker 会给每个容器可写层分配一个独立的 project ID,并把该层目录
chattr -p <id> +P打上 project 标记;之后这个目录下所有文件的字节数都归到该 project ID 头上,超出bhard就拒绝写入。这也是为什么它在 overlay2 上能实现"精确到单个容器可写层"的配额——普通 ext4 没有这套 project 维度的记账能力,自然就只能静默忽略(见第三节)。注意:ext4 也有 project quota 能力(
mkfs.ext4 -O quota -E ...+prjquota挂载),但 Docker overlay2 的size实现** historically 只对 XFS 做了完整支持**,ext4 上要么报错要么(如本机所见)静默忽略。生产上稳妥起见直接用 XFS。9.3 镜像、容器、卷的回收:别让垃圾堆满盘
配额是"兜底",日常更要靠"清理"。先看家底:
dockersystemdf# 镜像/容器/卷各占多少,有多少可回收# TYPE TOTAL ACTIVE SIZE RECLAIMABLE# Images 3 2 1.2GB 400MB# Containers 5 1 800MB 800MB ← 4 个停止的容器占着可写层# Local Volumes 2 1 500MB 300MB# 一键回收:停止的容器、悬空镜像、悬空网络、构建缓存dockersystem prune# 危险但常用,确认无重要停止容器再跑dockersystem prune-a# 连未被任何容器引用的镜像一起删dockervolume prune# 删未被挂载的卷(数据无价,先确认)治理节奏建议:CI Runner 每次构建后
prune;生产环境用定时任务 + 监控告警(磁盘 > 80% 就报警并触发清理脚本)。9.4 三道防线的优先级
- 第一道(必做):日志轮转
--log-opt max-size/max-file、高频写路径挂 volume——从源头不让垃圾进可写层;- 第二道(推荐):数据根用 XFS
pquota,per-container--storage-opt size兜底;- 第三道(保底):监控 + 定期
prune+ 告警,出问题能快速定位谁在涨。十、监控与告警:在爆盘之前拦住它
配额和清理是"事后/兜底",真正要避免事故,靠的是提前告警。两条可落地的监控线:
宿主级(盘快满了):用 node_exporter 暴露
node_filesystem_avail_bytes,针对/var/lib/docker所在挂载点设告警:# Prometheus 规则示例-alert:DockerDiskAlmostFullexpr:node_filesystem_avail_bytes{mountpoint="/var/lib/docker"}/ node_filesystem_size_bytes < 0.2for:5mlabels:{severity:warning}容器级(谁在涨):用 cAdvisor 的
container_fs_usage_bytes按容器看使用量,并对"短时间内增长速率"设告警——比"绝对量大"更早发现问题:-alert:ContainerFsGrowingFastexpr:deriv(container_fs_usage_bytes[10m])>50e6# 10 分钟涨超 50MB/s 趋势for:10m核心转储 / 临时文件专项治理:core dump 是经典的"悄悄吃满盘"元凶。可以从两个方向堵:
# 方向一:容器里直接关掉 core(ulimit)dockerrun--ulimitcore=0... ubuntu:24.04# core 文件最大 0,等于不落盘# 方向二:宿主内核把 core 重定向到管道/特定目录(不进容器可写层)# /proc/sys/kernel/core_pattern = |/usr/bin/coredump-handler %P %u (经管道交给专用程序处理)配合日志轮转(
--log-opt)和临时文件上--tmpfs,core dump、日志、临时文件这三类"意外增长源"就都被兜住了。十一、真实翻车案例三则
理论说再多,不如看几个真出过事故的场景:
案例一:日志不轮转,一周写满盘。某 Java 服务用 logback 但忘了配
maxFileSize/maxHistory,容器跑了一周,app.log长到 30GB,宿主机/100%,上游网关全部 5xx。根因就是本文第二节复现的"可写层无上限"。正确的修法:应用层轮转 + 容器--log-opt max-size=10m --log-opt max-file=5双保险。案例二:CI 缓存堆爆可写层。某 CI Runner 每次在容器里
apt-get、下 maven 依赖,从不清理,还用了docker run --rm但镜像层越积越多。时间一长docker system df显示几百 GB 悬空镜像。prune -a一把清掉,盘瞬间回血。教训:CI 节点必须定时prune,且依赖缓存应走 volume 而非可写层。案例三:core dump 循环。一个偶发 segfault 的服务被 supervisor 不断拉起,每次崩溃写一个几 GB 的
core.<pid>到容器根目录。十分钟就把盘写满。修法:--ulimit core=0关掉 core,或把kernel.core_pattern改成管道交给专用收集器,绝不进容器可写层。这三个案例的共同点:失控的写入都发生在容器可写层,而可写层默认没有任何空间隔离。本文讲的配额、轮转、volume、tmpfs、监控,正是针对这三类的对应解药。记住一个原则——任何"会持续增长、且你不主动清理"的写入,都不要让它落在容器可写层。
十二、延伸:配额(空间)与 I/O 限速(带宽)是两回事
本篇讲的
size/ project quota / tmpfs 限的是**“能用多少空间”;下一篇讲的io.max/--device-write-bps限的是"每秒能读写多快"**。它们是正交的两层:
- 空间配额回答:“这个容器最多占多少 GB?”——防止写爆盘。超了直接
No space left on device。- 带宽限速回答:“这个容器每秒最多刷多少 MB 到盘?”——防止它把整块盘的 I/O 占满、拖慢邻居。超了只是变慢,不会报错。
生产上两者要配合用:空间配额兜住"量"(别写爆),带宽限速兜住"速"(别占满 I/O)。只限空间不限速,一个容器照样能用 500MB/s 把盘打满、让所有人卡顿;只限速不限空间,它慢慢写也能把盘写满。
十三、小结与思考题
本篇我们真实复现并验证了:
- 容器往可写层写 1.5GB,宿主机磁盘实涨 1.5GB,删容器才回落——磁盘空间默认不隔离;
--storage-opt size=1G在本机 ext4 根分区上被静默忽略(写 2GB 无报错无警告),原因是不支持 XFS project quota;- 用 XFS +
prjquota跑通了 project 配额机制,500MB 硬限精准卡死超额写入(No space left on device);--log-opt max-size日志轮转(当前文件 ~149KB + 一个满 1MB 的轮转文件,总量 ~1.2MB 封顶)、--tmpfs size=100m(df 显示 100M,写超即止)均实测有效。思考题:
- 为什么
--storage-opt size在 ext4 上"静默忽略"比"直接报错"更危险?如果你的 CI 里依赖这个参数做防护,会有什么隐患?- XFS project quota 限制的是"某个目录/项目"的总字节数。如果一个容器在可写层里不断创建小文件(而不是写大文件),配额能限制住吗?为什么?
tmpfs限了 100M,但容器里dd往/tmp写 200MB 会怎样?是报"磁盘满"还是 OOM?二者后果有什么不同?下一篇《磁盘限速与 I/O 延时:容器里磁盘读写为什么不稳定?》我们进入 Cgroup v2 的
io.max,看看怎样给容器限速、以及 buffered I/O 为什么会让写延时"飘"。实验环境与命令均在本机 Ubuntu 24.04 / 内核 6.8 / Docker 29.1.3 / Cgroup v2 真机执行,输出为原始截取;为避免写满磁盘,单次写盘控制在 2GB 内并即时清理。本文与《OverlayFS 原理》《磁盘限速与 I/O 延时》是容器存储三部曲:第一篇讲文件系统视图与 copy-up,本篇讲空间隔离(配额),下一篇讲带宽隔离(限速),三者合起来才是完整的容器磁盘治理。