服务器入坑指北:容器的层占用与清理边界
镜像删了,磁盘占用却没降;同一份基础层被反复下载;想清理,又不敢下手——怕把正在跑的容器一起删掉。
顺序是先分清"被引用的层"与"无引用的层",再清理:列出谁在引用 → 判断哪些层已无引用 → 按"先停用、后清理"动手。顺序反了,要么误删在跑的东西,要么白清一场。
这篇落在内核层与内容寻址规范层(统一写"运行时"),不点名任何容器产品;给三段可直接粘贴的内容:看清层构成 → 判引用 → 清理与保留。取值用占位符。
层是怎么堆起来的:overlay 与四个字段
容器镜像是由"层"堆起来的,运行时把若干只读层与一个可写层叠加成一个视图——这套叠加由 overlay(overlayfs)完成。一个挂载项由四个字段组成:
lowerdir=:只读下层,可以多个、以冒号分隔;upperdir=:可写上层;workdir=:工作目录,必须与上层在同一个文件系统上;- 再加一个挂载点。于是"只读层"与"可写层"的关系就写在挂载项里。
这一层还有两个必须记住的语义(下面的read-only层与可写层就是靠这几个字段拼出来的): whiteout(白障):在下层删除一个文件时,运行时不是去改下层(它只读),而是在上层放一个白障把它遮掉——"删了"在层里是一个占位标记,不等于空间立刻回来;copy_up(写时复制):修改下层的文件时,先把它复制到上层,因此同一份内容会同时存在于上下两层——这是"同一份内容多占一份"的机制原因。
判据:看清一个挂载项的四个字段,就知道这一层的占用由谁构成。
看清层构成块
findmnt --target <路径> # 现读该路径落在哪个挂载项上(含叠加选项)
cat /proc/self/mountinfo # 现读挂载表:同一信息的内核视角
du -s --block-size=1 <路径> # 现读占用(统一口径)
du --apparent-size <路径> # 表观大小:与实际占用是两个口径
df -h # 文件系统可用量:清理前后同口径对比
在拿这两个数下结论之前,先把口径差看清:du 统计的是目录树里文件占用的块,df 统计的是文件系统的可用量;两者的差还包括已删除但仍被进程持有的文件。所以 du 与 df 对不上是常态,别直接把差额算成"都被容器吃了"(空间与 inode 耗尽的分流见《磁盘还有空间却写不进去》那篇)。
判据:du -s --block-size=1 <路径> 与 du --apparent-size 的差值说明实际占用与表观大小的偏离;df -h 用来看清理后可用量有没有真的变化;若怀疑是 inode 而不是空间,用同族的 df -i 再看一眼。
另外几个只读入口可以互相印证:mountinfo(也就是 /proc/self/mountinfo)与 /proc/mounts 是内核视角的挂载表,mount 也能列出挂载项——三者应当互相一致。
判引用块:看"层被谁引用"
在清之前,先把这条前提看清:要看的是层被谁引用,不是镜像还在不在列表里。删除一个镜像不等于删掉它用过的所有层——旧的构建层可能不再被任何镜像引用,却仍然占着磁盘。
两边的只读入口:
cat manifest.json # 清单:这次由哪些层组成(以摘要引用)
cat index.json # 索引:标签指向哪个清单(标签只是名字)
ls blobs/ # 层内容按摘要存放的地方(清单引用的摘要对应的是存放的层内容)
判据:读清单看"这一层被谁引用"——sha256: 形态的摘要才是内容身份,layer 是层的语义单位;这套布局描述的是"镜像由哪些层组成",它不是通用的引用视图(换一种存放形态就不一定有这几个文件)。另外注意:清单里引用的摘要是对存放的层内容计算的,所以别拿"把文件重新算一遍摘要去对上清单"当判据——真正的引用关系看运行时侧的只读视图(按"有哪些容器在用这个镜像"筛选),清理前必须用它确认"没有在跑的实例依赖它"。
动手之前,先把两条看清
别忘了 挂载命名空间:容器里的挂载视图与宿主机不在同一个命名空间,在宿主上看到的那一堆叠加挂载项,才是你付钱的那部分占用(跑在 guest 里也同样算你的)。
还有一条以现读为准的前提:运行时对无引用层的回收(分层回收,GC)时机各不相同,别假设它会在固定时刻自动发生;而层内容在存储侧是以 blob 为单位按摘要存放的,同一个 blob 被多处引用时,清掉一处并不会让占用立刻下降。所以"清完占用没变"未必是没清掉,可能只是还有别的引用。
清理与保留块
清理与卸载属高危写操作,动手前先确认没有实例依赖。
umount <挂载点> # 卸载一个层叠加(高风险写操作,需回滚点;先确认无实例依赖)
du -s --block-size=1 <路径> # 读回:占用确实下降了吗
df -h # 读回:可用量的变化与口径差能否解释
保留策略的写法:先写下"要能回滚几代",再清。
- 按剩余空间清理的后果很具体:下次要回滚时,那一代已经被删了;
- 按
回滚代际清理是用空间换"能回去"; - 判据:要回滚时那一代还在。这里不给代数——它由你的回滚需求定。
判断"谁还在用"靠的是引用计数:一个层被几个镜像或实例引用,决定了它能不能被清。摘要是内容身份、标签只是名字——能不能回收,看的是前者而不是后者。
现象到动作的判定表
| 现象 | 结论 | 动作 |
|---|---|---|
| 镜像删了磁盘没降 | 它用过的层还被引用,或已成为无引用层 | 用只读引用视图看"层被谁引用",再决定清哪一层 |
| 同一基础层被反复下载 | 引用没固定到内容身份,或本地层没被保留下来 | 用 sha256: 摘要核对"是不是同一份内容",并把引用方式固定下来 |
| 不敢清、怕删掉在跑的 | 缺"谁在引用"这一路证据 | 先按引用视图确认无实例依赖,再清 |
| 该保留几代 | 取决于回滚需求 | 写下"要能回滚几代",只清超出代际的 |
du 与 df 对不上 |
口径差(含已删除但被持有的文件) | 用同口径前后对比,别把差额全算给容器 |
改完怎么验证:每项一条判据
du -s --block-size=1 <路径>的占用确实下降;- 没有实例依赖被误删(在跑的仍正常);
- 保留代际与写下的策略一致;
df -h的可用量变化与du的口径差可解释;- 有回滚形态(卸载与清理前的原状态有记录)。
这篇不解决什么
- 讲部署前先固化产物的那篇是构建产物的摘要与版本对应;本篇是运行时的层与清理——两篇都用"摘要才是身份",但对象与动作不同。
- 讲磁盘还有空间却写不进去的那篇是空间与 inode 耗尽的分流;本篇是容器层占用的清理,是它的一个具体来源。
- 讲新文件为什么别人改不了的那篇是权限与继承;本篇不涉及权限——若清理失败,先排除权限与视图。
踩坑清单
- 拿"镜像还在不在列表"当判据:要看的层被谁引用。
- 直接清而不先看在跑的实例:把依赖它的东西一起删掉。
- 按剩余空间清理:下次回滚时那一代没了。
- 把引用挂在会变的标签上:说不清"这份内容对应哪个摘要"。
- 把标签当内容身份:标签会重新指向别的摘要。
- 把
du与df的差当成"都被容器吃了":里面还有已删除但被持有的文件。 - 不看
whiteout就以为删除不占空间:它是上层的一个标记。 - 忘了写时复制会让同一份内容多占一份。
- 在 guest 里以为占用与自己无关:跑在 guest 里也算你付的钱。
- 清完不读回:不知道占用到底降了没有。
- 忽略挂载命名空间的差异:在错误的视图里找占用。
- 卸载前不留回滚点:卸载之后回不去。
- 点赞
- 收藏
- 关注作者
评论(0)