容器镜像分层机制的本质是“只读层叠加+写时复制”,存储复用发生在层级别,而不是整镜像级别。
理解了这一点,镜像构建为什么快、磁盘为什么省、推送拉取为什么只传差异,就都通了。
容器镜像分层机制是什么:一层层“贴纸”怎么变成应用底座
容器镜像不是一整块磁盘文件,它像一叠透明贴纸,每张贴纸记录一次文件系统变化,最下面是最基础的操作系统用户态文件,往上是软件包、环境变量、业务代码、启动配置,启动容器时,Docker会在这叠贴纸最上面再放一张可写贴纸,所有运行产生的修改都写在这张可写层里。
用一条命令就能看到这些层:
docker history nginx:latest
输出里每一行基本对应一层。CREATED BY列展示构建指令,SIZE列展示这一层带来的体积变化。
- 基础层:来自Linux发行版镜像,比如Debian、Alpine
- 中间层:安装依赖包、创建目录、修改配置
- 应用层:复制代码、设置启动命令
层一旦构建完成就是只读的,如果后续构建修改了某一层,Docker不会修改原层,而是生成一个新的层,这个规则看起来很死板,但它正是复用逻辑成立的前提。
docker镜像分层复用原理:同一台机器为何能省下大量磁盘
每一层都有一个由内容计算出来的唯一标识,通常叫层ID或者digest,只要内容相同,层ID就相同,Docker本地和镜像仓库都按这个标识判断层是否已存在。
比如你有两个镜像,一个跑Java服务,一个跑Python服务,但它们都基于同一个Ubuntu 22.04基础镜像,那么这两个镜像在本地只保存一份Ubuntu基础层,Java服务镜像只需要额外保存JDK层和应用层,Python服务镜像只需要额外保存Python层和应用层。
可以这样验证:
docker image inspect nginx:latest
找到GraphDriver字段,里面LowerDir列出的就是镜像只读层路径,UpperDir是容器可写层路径,MergedDir是最终合并后的视图。
在宿主机上还能直接查看overlay2层的软链:
ls -l /var/lib/docker/overlay2/l
每个软链名对应一个层,进入某个软链指向的目录,再查看lower文件:
cat /var/lib/docker/overlay2/l/XXXX/lower
里面用冒号分隔多个层路径,这个输出很直观地说明:一个容器运行时看到的完整文件系统,是很多个只读层加一个可写层拼出来的。
写时复制是另一个省钱逻辑,容器启动后,所有修改都写入自己的可写层,不会动到底下的镜像层,所以十个容器同时基于同一个基础镜像运行,基础层依然只读且干净,共享一份,真正新增的磁盘占用,只是每个容器可写层里产生的少量差异数据。
容器镜像和虚拟机镜像的区别:共享层让“搬家”更轻
虚拟机镜像通常是一个完整的磁盘文件,里面包含内核、驱动、系统服务、应用,一个基础虚拟机镜像动辄几个GB,复制十份就是十份完整磁盘,存储和迁移都很重。
容器镜像不包含操作系统内核,只保留用户态文件,更关键的是分层机制让多个容器可以共享相同基础层,测试环境里同时跑十个微服务,如果它们都基于同一个基础镜像,Docker只需要保存一份基础层和十个很小的差异层,虚拟机方案下,十个虚拟机很可能要占用十份操作系统磁盘空间。
行业共识认为,容器镜像的共享基础层在开发测试和CI/CD场景中能有效降低存储与分发成本。
但容器镜像和虚拟机镜像不是简单的替代关系,虚拟机镜像提供更强的系统隔离,容器镜像更适合进程级隔离和快速交付,选型时看的是隔离等级、启动速度、存储成本三者之间的取舍。
容器镜像仓库存储成本怎么算:按层计费不按镜像个数
镜像仓库的计费维度通常是存储容量、外网下行流量、请求次数,因为层去重机制的存在,仓库按层存储,而不是按镜像个数存储。
假如私有仓库里保存100个业务镜像,它们都基于同一个20GB基础镜像,实际占用不是100×20GB,而是基础层一份,再加上100个差异层,多数情况下,业务差异层只有几十MB到几百MB,这样总存储费用会被大幅拉低。
成本计算逻辑大致是:
- 未去重层总大小 × 单价 × 存储时长
- 外网下载流量 × 流量单价
- 不同地域的单价通常不同,华东、华北等国内地域与海外地域存在价差
- 内网流量在多数云厂商平台上免费或低价
镜像仓库里真正的成本大头,经常不是镜像数量,而是基础层体积和频繁的跨地域拉取流量,所以做镜像瘦身和选用同地域仓库,比单纯减少镜像个数更有效。
国内拉取容器镜像慢怎么解决:从镜像加速到构建缓存复用
国内拉取容器镜像慢怎么解决,先要搞清楚慢在哪里,默认的Docker Hub节点在海外,网络链路长,丢包和延迟明显,解决路径一般有两种:
- 配置国内镜像加速器
- 使用云厂商私有镜像仓库
配置加速器可以直接修改Docker守护进程配置:
sudo tee /etc/docker/daemon.json <<EOF
{
"registry-mirrors": ["https://你的镜像加速地址"]
}
EOF
sudo systemctl daemon-reload
sudo systemctl restart docker
配置完成后重新拉取:
docker pull nginx:1.25
如果本地或加速器已经缓存了部分层,输出里会出现Already exists,只下载缺失层,第二次拉取会明显快于第一次。
企业内网场景更适合用私有镜像仓库,把常用基础镜像先推送到同地域私有仓库,构建和部署都走内网地址,这样既绕开公网,又能利用层复用减少重复上传和下载。
利用分层机制做好镜像瘦身:实操层面的复用优化
层复用不只是省磁盘,还能直接影响构建速度和镜像分发效率,构建时如果某层缓存命中,Docker会直接复用,不再执行那条指令,所以镜像层设计得越合理,CI/CD流水线跑得越快。
多阶段构建是当前最常用的瘦身手段:
FROM golang:1.21 AS builder WORKDIR /app COPY . . RUN go build -o main . FROM alpine:3.19 WORKDIR /app COPY --from=builder /app/main . CMD ["./main"]
构建阶段用的Go基础镜像、依赖包、编译缓存都不会进入最终运行镜像,最终镜像只保留alpine基础层和应用二进制层,一个原本可能大几百MB的镜像,通常可以降到几十MB。
合并RUN指令也是有效做法:
RUN apt-get update
&& apt-get install -y --no-install-recommends curl
&& rm -rf /var/lib/apt/lists/
每条RUN都会产生一层,把更新、安装、清理合并到一条RUN里,可以避免把包缓存留在镜像层里,减少层数还能降低内核挂载层数,部分场景下对容器启动性能有好处。
清理无效层可以用:
docker image prune
它主要清理没有被任何镜像引用的悬空层,被正常镜像引用的层会继续保留,这也是层复用机制的一部分。
分层复用是容器镜像体系里最基础的省钱逻辑,镜像层像乐高积木,基础层反复利用,差异层按需拼装,构建、存储、分发三条链路同时被压缩,真正理解了层,就能理解容器镜像工程里大多数优化动作的起点。
容器镜像分层机制常见问题解答
容器镜像分层机制是什么时候生效?
构建镜像时每条产生文件变化的指令都会生成一个只读层,启动容器时在最上面追加一个可写层,容器运行期间的修改只发生在可写层,删除容器时只删除可写层,镜像层保持不变。
docker镜像分层复用原理对磁盘占用有什么影响?
同一台宿主机上,多个镜像只要共享相同基础层,磁盘只保存一份基础层,不同镜像各自有差异层,只占用差异部分空间,多个容器基于同一镜像运行时,也只保存一个镜像层集合和各自很小的可写层。
为什么删除容器后镜像层还在?
删除容器时,Docker默认只清理容器的可写层和元数据,不会删除镜像层,镜像层仍然被本地镜像引用,直到显式执行docker rmi删除镜像,或者用docker image prune清理未被引用的悬空层,被镜像引用的层会继续保留,以保证镜像可以再次启动容器。
首发原创文章,作者:王坚,如若转载,请注明出处:https://idctop.com/article/640750.html





