容器文件 Quota:容器为什么会把宿主机磁盘写满?怎么限制?
容器文件 Quota容器为什么会把宿主机磁盘写满怎么限制实验环境Ubuntu 24.04 / 内核 6.8.0-106-generic / Cgroup v2 / Docker 29.1.3overlayfs根分区 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/vda140G当前已用 5.7Gdf-h/var/lib/docker# Filesystem Size Used Avail Use% Mounted on# /dev/vda1 40G 5.7G 32G 15% /USED_BEFORE5882856# KB启动一个常驻容器往可写层里写 1.5GB注意这里走的是容器可写层的 overlay upper数据最终落在宿主机磁盘dockerrun-d--namefilltest ubuntu:24.04sleep120dockerexecfilltestbash-cdd if/dev/zero of/fill.bin bs1M count1500 21 | 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_MID7418948占用增量(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 size1G文档说它能给容器可写层设配额。我们在实验机上试试dockerrun--rm--namesizetest --storage-optsize1G ubuntu:24.04bash-c df -h / | tail -2 dd if/dev/zero of/big.bin bs1M count2000 21 | tail -1 ls -l /big.bin df -h / | tail -2 输出注意没有任何报错Filesystem Size Used Avail Use% Mounted on overlay 40G 5.7G 32G 15% / ← 看到的是整块宿主机盘 ... 20000 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 size1G既没报错也没生效——容器照常写出 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 size1G就安全了其实容器照样能把盘写爆。生产上绝不能只靠这个参数保命尤其在 ext4 根分区上。四、可行方案 AXFS project quota机制级验证既然 ext4 不行我们用 XFS project quota 把机制跑通。在实验机上创建一个 XFS 镜像文件loop 挂载并启用prjquotaddif/dev/zeroof/root/xfs_quota.imgbs1Mcount1024statusnone 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并设置硬限 500MBmkdir-p/root/xfsmnt/projdir xfs_quota-x-cproject -s -p /root/xfsmnt/projdir 100/root/xfsmnt xfs_quota-x-climit -p bhard500m 100/root/xfsmnt xfs_quota-x-creport -p/root/xfsmnt# Project ID Used Soft Hard# #100 0 0 512000 ← 500MB 硬限已生效试着往里写 700MB超过配额ddif/dev/zeroof/root/xfsmnt/projdir/big.binbs1Mcount700# dd: error writing .../big.bin: No space left on device# 5010 records in# 5000 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-creport -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-size1m --log-opt max-file2\ubuntu:24.04bash-cwhile true; do echo\log line ...\; donesleep6CID$(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-file2保留 2 个文件每个不超过max-size1m卡在 ~1.2MB。不限制的话这个文件会无限增长直到撑爆磁盘。生产建议所有长驻容器都加--log-opt max-size --log-opt max-file或在daemon.json里全局配置默认日志驱动与上限。六、可行方案 C临时/内存数据用--tmpfs限大小对于临时文件、Core dump 这类丢了也无所谓的数据最干净的做法是根本不落盘——用tmpfs内存文件系统并显式限制大小dockerrun--rm--tmpfs/tmp:size100m ubuntu:24.04bash-c df -h /tmp | tail -1 echo --- 写 200MB(应卡在 100M) --- dd if/dev/zero of/tmp/t.bin bs1M count200 21 | tail -3 # Filesystem Size Used Avail Use% Mounted on# tmpfs 100M 0 100M 0% /tmp ← 严格限在 100M# 1010 records in# 1000 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) 新盘格式化为 XFScrc/finobt 打开现代内核默认mkfs.xfs-f-mcrc1,finobt1/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 size1G才会通过 XFS project quota 真正限制每个容器可写层的大小——这正是本文第四节用 loop 镜像验证过的同一套机制只是从演示变成了生产数据根。其底层细节值得记住XFS 的 project quota 是按项目 IDproject 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——从源头不让垃圾进可写层第二道推荐数据根用 XFSpquotaper-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 是经典的悄悄吃满盘元凶。可以从两个方向堵# 方向一容器里直接关掉 coreulimitdockerrun--ulimitcore0... ubuntu:24.04# core 文件最大 0等于不落盘# 方向二宿主内核把 core 重定向到管道/特定目录不进容器可写层# /proc/sys/kernel/core_pattern |/usr/bin/coredump-handler %P %u 经管道交给专用程序处理配合日志轮转--log-opt和临时文件上--tmpfscore dump、日志、临时文件这三类意外增长源就都被兜住了。十一、真实翻车案例三则理论说再多不如看几个真出过事故的场景案例一日志不轮转一周写满盘。某 Java 服务用 logback 但忘了配maxFileSize/maxHistory容器跑了一周app.log长到 30GB宿主机/100%上游网关全部 5xx。根因就是本文第二节复现的可写层无上限。正确的修法应用层轮转 容器--log-opt max-size10m --log-opt max-file5双保险。案例二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 core0关掉 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 size1G在本机 ext4 根分区上被静默忽略写 2GB 无报错无警告原因是不支持 XFS project quota用 XFS prjquota跑通了 project 配额机制500MB 硬限精准卡死超额写入No space left on device--log-opt max-size日志轮转当前文件 ~149KB 一个满 1MB 的轮转文件总量 ~1.2MB 封顶、--tmpfs size100mdf 显示 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本篇讲空间隔离配额下一篇讲带宽隔离限速三者合起来才是完整的容器磁盘治理。