4、Docker镜像原理:联合文件系统(UnionFS)、分层构建、写时复制、镜像仓库与标签
聊到Docker镜像,很多人第一反应就是「镜像嘛,就是个打包好的环境」。嗯,这话没错,但太笼统了。我刚开始用Docker那会儿,也以为镜像就是个ISO文件,后来踩了不少坑才明白——镜像的本质,是一套分层堆叠的文件系统快照。
说白了,Docker镜像不是一个大文件,而是一层一层叠起来的「千层饼」。每一层都只记录变化的部分。这个设计,才是Docker能火起来的核心原因之一。
4.1 联合文件系统(UnionFS)——镜像的底层基石
联合文件系统,英文叫UnionFS。它的作用很简单:把多个目录挂载到同一个挂载点,看起来像一个目录。
举个例子。你有两个目录:layer1/ 和 layer2/。layer1里有个 a.txt,layer2里有个 b.txt。用UnionFS把它们合并挂载到 /merged 下,你就能同时看到 a.txt 和 b.txt。
Docker用的就是这种机制。每个镜像层都是一个只读的目录,容器运行时再叠加上一个可写层。你想想看,这多省空间?
我在项目中遇到过一个问题:某个微服务镜像构建出来居然有2GB。后来一查,发现是每个构建步骤都复制了完整的JDK,没有利用分层缓存。嗯,这就是不懂UnionFS的代价。
4.2 分层构建——Dockerfile的精髓
Dockerfile里的每一行指令,都会生成一个新的镜像层。比如:
FROM ubuntu:20.04 # 层1:基础层
RUN apt-get update # 层2:更新包列表
RUN apt-get install -y nginx # 层3:安装nginx
COPY index.html /var/www/ # 层4:复制文件
CMD ["nginx", "-g", "daemon off;"] # 层5:元数据层
每一层都是只读的,并且会被缓存。如果你下次构建时,前三行没变,Docker就会直接复用缓存,只重新构建第四层和第五层。这速度,快得飞起。
但这里有个坑——层的顺序很重要。我习惯把「不常变」的指令放在前面,比如安装系统包;把「经常变」的指令放在后面,比如复制代码。这样能最大化利用缓存。
RUN apt-get install 这类稳定指令,再写 COPY 代码。如果反过来,每次改代码都要重新安装所有包,那构建时间就爆炸了。
4.3 写时复制(Copy-on-Write)——容器启动的秘密
你启动一个容器时,Docker会在镜像层之上,创建一个薄薄的可写层。这个可写层初始是空的。容器里任何写操作——创建文件、修改配置——都发生在这个可写层里。
这就是写时复制(CoW)。不写则不复制,写了才复制。
举个例子。你启动100个Nginx容器,它们共享同一个Nginx镜像。每个容器只拥有一个几KB的可写层。如果某个容器修改了 /etc/nginx/nginx.conf,Docker会把那个文件从镜像层复制到可写层,然后修改。其他容器不受影响。
我曾经犯过一个错误:在生产环境里,直接在容器里写日志文件。结果日志越写越大,可写层膨胀到几十GB,容器磁盘爆了。后来我改用挂载卷(volume)来存日志,才解决这个问题。
4.4 镜像仓库与标签——分发与版本管理
镜像构建好了,得有个地方存着,方便别人拉取。这就是镜像仓库(Registry)干的事。最常用的当然是Docker Hub,但企业内部一般用Harbor或Nexus。
镜像的完整名称格式是:
[仓库地址]/[命名空间]/[镜像名]:[标签]
比如:
docker.io/library/nginx:1.21
myregistry.com/myteam/myapp:v2.0.0
标签(Tag)就是版本号。我建议你不要用 latest 标签。为什么?因为 latest 是动态的,今天拉和明天拉可能不一样。生产环境里,版本必须锁定。
| 标签策略 | 说明 | 推荐场景 |
|---|---|---|
v1.0.0 |
语义化版本 | 正式发布 |
v1.0.0-rc.1 |
候选版本 | 测试环境 |
sha-xxxxxx |
Git提交哈希 | CI/CD自动构建 |
latest |
最新版(不推荐生产用) | 本地开发 |
我在团队里推行过一个规则:每个镜像必须打两个标签——一个语义化版本(如 v2.3.1),一个Git提交哈希(如 sha-a1b2c3d)。这样既能方便人类阅读,又能精确定位到代码版本。
4.5 镜像的存储结构——深入看一眼
如果你好奇镜像在磁盘上长什么样,可以看看 /var/lib/docker/overlay2/ 目录。里面是一堆哈希命名的目录,每个目录对应一个镜像层。
/var/lib/docker/overlay2/
├── l/ # 符号链接,指向各层
├── 3b8f7a2.../ # 层1
│ ├── diff/ # 该层的文件内容
│ └── link # 指向l/下的链接
├── 9c1e4d5.../ # 层2
│ ├── diff/
│ └── link
└── ...
每个 diff/ 目录里,只保存该层新增或修改的文件。这就是分层构建的物理体现。
我记得有一次排查磁盘空间问题,发现 /var/lib/docker 占了100多GB。用 docker system df 一看,发现大量悬空镜像(dangling images)——就是那些没有标签、没有被任何容器引用的中间层。执行 docker image prune 清理后,瞬间释放了40GB。
docker system df— 查看Docker磁盘使用情况docker image prune— 清理悬空镜像docker system prune -a— 清理所有未使用的资源(慎用)
4.6 总结一下
Docker镜像的原理,说白了就是三句话:
- UnionFS 让多层文件系统合并成一个视图
- 分层构建 让镜像可复用、可缓存、可增量
- 写时复制 让容器启动快、占用少
再加上镜像仓库和标签,你就有了一个完整的镜像生命周期管理体系。从构建、存储、分发到运行,每一步都有章可循。
嗯,下一章我们会聊聊Docker网络。到时候你会发现,网络这块比镜像复杂多了——但理解了原理,一切都不难。