外观
项目 2:Docker 镜像底层原理与容器生命周期
约 2952 字大约 10 分钟
DockerImageContainerUnionFS
2026-05-22
项目目标
上一节课我们成功安装了 Docker 并运行了 hello-world。但在企业实战中,真正支撑起庞大微服务体系的是数以万计的“镜像”与“容器”。 本节课我们将深入 Docker 的底层文件系统 UnionFS,揭开“镜像”为何像千层糕一样的物理本质;同时梳理容器从生到死的状态流转,最终我们将手动“雕刻”出一个自定义镜像。
一、到底什么是“镜像”?
在 IT 行业里,我们经常说“去拉取一个 MySQL 镜像”、“把代码打包成镜像”。很多人以为,镜像就像是一个 .zip 或 .iso 压缩包,里面装了一个完整的操作系统和软件。 这种理解是完全错误的,而且会让你在后续学习 Dockerfile 时痛不欲生。
1. 镜像不是一个整体,而是一层层的“千层糕”
Docker 镜像采用了非常独特的 UnionFS(联合文件系统) 技术(在较新的 Docker 版本中通常底层实现为 overlay2)。 你可以把镜像想象成透明的幻灯片(或者千层糕)。每一个独立的操作(比如装个系统、装个 Python、拷贝一段代码),都会生成一张新的透明幻灯片,并轻轻盖在之前的幻灯片上面。
当你从最上面往下看的时候,所有的幻灯片重叠在一起,视觉上融合成了一副完整的画面。这就是你在容器里看到的“完整的文件系统”。
2. 只读与共享:省空间的终极魔法
镜像层有一个极其重要的物理特性:一旦构建完成,它就是绝对只读 (Read-Only) 的,永远无法被修改! 这带来了什么好处? 假设你下载了一个 Ubuntu 基础镜像(100MB),然后基于它做了 5 个不同的业务镜像。这 5 个镜像在你的硬盘上并不是占用 500MB,而是 依然只占用 100MB 左右的操作系统空间,加上一点点业务代码的空间! 因为底层的 Ubuntu 幻灯片是绝对只读的,所以 Docker 只需要在硬盘上存一份,让所有的容器共享这一层即可。
👇 亲自操作下方的“千层糕”推演动画,直观感受这种分层叠加和“写时复制 (CoW)”的覆盖魔术:
🥞 Docker 镜像千层糕 (UnionFS) 原理演示
点击下方按钮,观察基础镜像层如何一步步叠加成最终的运行环境
👁️ 容器视角
↓
等待构建镜像...
二、镜像管理与底层探秘
懂了理论,我们来看看在实际的命令行中,这种分层结构是如何体现的。
1. 拉取与查看镜像
官方提供了一个极其精简的 Linux 镜像,叫做 alpine。它仅仅包含最核心的系统文件,体积只有惊人的 5MB 左右!
# 从 Docker Hub 拉取 alpine 镜像的最新版本
docker pull alpine:latest下载完成后,我们要查看本地到底有哪些镜像。为了列出所有的镜像文件,Docker 专门提供了一个名为 images 的子命令。请结合所学,在下方的解锁框中输入用于查看镜像列表的子命令来继续实验:
问题:根据上面的阅读,查看所有镜像列表所使用的子命令是什么?(全小写,6个字母)
2. 探秘底层的 JSON 数据
只看列表是不够的,我们要扒开它的外衣。Docker 提供了 inspect 命令来查看 Docker 对象的底层元数据(以 JSON 格式呈现)。
docker image inspect alpine:latest执行后,请在长长的输出中找到 RootFS -> Layers 字段。你会看到一个形如 sha256:xxxxxx 的哈希值。这就是构成 alpine 镜像的那张最底层的“只读幻灯片”在硬盘上的真实身份证号!如果是更复杂的镜像(比如 nginx 或 mysql),你会在这里看到一长串的哈希值,代表它们是由很多层幻灯片叠加而成的。
三、容器的生死轮回
既然镜像是一堆只读的幻灯片,那我的程序运行时,往日志文件里写的数据,存到哪里去了呢?难道只读层被破坏了吗?
1. 容器 = 只读镜像层 + 读写层
这就是 Docker 最精妙的设计:所谓的容器,不过就是在镜像所有“只读幻灯片”的最顶端,临时加上了一张“可读写的幻灯片 (Container Layer)”!
当容器启动时,你对系统的所有修改(比如新建文件、修改配置、写入日志),统统都只发生在这个最顶层的读写层里。底层的镜像层毫发无损。 这就意味着,如果把最上面的读写层删掉,容器就彻底消失了,而镜像依然光洁如新,随时可以用来启动下一个完全一致的新容器。
2. 状态流转模型
因为容器本质上就是一个附带了读写层的进程,所以它拥有像软件一样的生命周期状态:
- Created (已创建):读写层分配好了,但进程还没开始跑。
- Running (运行中):进程正在 CPU 里狂奔。
- Paused (已暂停):进程被操作系统冻结,不再消耗 CPU,但内存和读写层还在。
- Exited (已停止/退出):进程已经彻底死掉(结束),但最顶端的读写层仍然保留在硬盘上。如果你想看崩溃前留下的日志,还能看得到。
- Deleted (已销毁):不仅进程没了,最顶端的读写层也被彻底删除了。容器在这个世界上存在过的痕迹被彻底抹除。
👇 通过下方的交互式控制台,亲自操纵容器的生死流转,并观察右侧“底层读写层”与“系统进程”的对应变化:
🔄 容器生命周期流转演示
观察容器如何在不同状态间切换,以及对应的底层读写层变化
📦 Created
docker start
▶
🏃 Running
docker pause ▶
◀ docker unpause
docker stop ▶
◀ docker start
⏸️ Paused
🛑 Exited
当前状态:None
底层读写层 (RW Layer):🔴 不存在 (已销毁或未创建)
系统进程 (PID):💤 进程已终止
四、操纵容器的生命周期
现在我们将刚才在动画里点按的按钮,转化为真实的 Linux 命令行操作。
1. 后台运行与状态查看
我们在后台(-d)启动一个会一直休眠的容器,用来模拟长期运行的服务器进程,并给它起个好记的名字(--name)。
docker run -d --name my-sleeper alpine:latest sleep 3600使用 docker ps(process status 的缩写)可以查看当前正在运行的容器:
docker ps你会看到 my-sleeper 的状态是 Up xx seconds。
2. 暂停与恢复 (验证内存驻留)
如果我们想临时冻结这个容器(比如备份数据时防止新数据写入),可以使用 pause:
docker pause my-sleeper
# 此时再执行 docker ps,你会发现状态变成了 (Paused)
docker ps然后我们再唤醒它:
docker unpause my-sleeper3. 进入容器 (开启异世界大门)
容器既然是一个独立的小世界,我们当然希望能“钻”进去看看。docker exec 允许我们在正在运行的容器里额外启动一个新的进程(通常是启动一个 Bash shell)。
# -it 表示开启交互式终端,sh 是我们要在这个 Alpine 容器里启动的命令
docker exec -it my-sleeper sh当你的终端提示符变成类似 / # 时,说明你已经成功进入了容器内部!在这个“异世界”里随便敲几个命令(如 ls, pwd, top),你会发现这里干净得就像一台刚装好的新电脑。体验完后,输入 exit 退出容器,回到你的宿主机。
4. 停止与彻底销毁
实验结束,我们需要清理战场。
# 友好地让容器停止(主进程终止,状态变为 Exited,但读写层保留)
docker stop my-sleeper
# 此时 docker ps 看不到它了,必须加 -a (all) 才能看到尸体
docker ps -a
# 彻底销毁它(抹除顶层的读写层,释放磁盘空间)
docker rm my-sleeper五、进阶任务 (Advanced Task):手工雕刻,提交你的专属镜像
在理论部分我们说过,容器的顶层是一个“可读写层”。既然能写,如果我在这个读写层里装上了一些很有用的软件,我能不能把这个读写层永久固化下来,变成一张新的“只读幻灯片”,从而生成一个我自己的专属镜像呢?
答案是绝对可以的。这个将读写层“转正”的操作,非常类似于 Git 代码管理中的提交机制。
任务步骤:
启动并进入纯净容器:
# 启动一个交互式的临时容器 docker run -it --name my-temp-alpine alpine:latest sh在容器内部搞点破坏(安装软件): 在容器的
sh界面中,执行以下命令,把本来没有 Python 的 Alpine 装上 Python 3。这一切的变更,此刻都暂时保存在了那层脆弱的可读写层里。apk add --no-cache python3 # 验证安装成功 python3 --version # 退出容器 exit见证奇迹的固化时刻: 现在容器已经 Exited 了。为了把刚才装了 Python 的读写层和底层的 Alpine 结合成一个新的镜像,我们需要执行类似于 Git 提交的动作。 请思考“提交”对应的英文单词是什么,在下方输入以获取最终的固化指令:
问题:把修改过的读写层“提交”并固化成新镜像,对应的英文命令是什么?(全小写,6个字母)
执行完命令后,你会发现本地多了一个属于你自己的、体积比原版 Alpine 大得多的新镜像! 💡 深度思考: 虽然手工 commit 很有成就感,但在企业中,这种做法被称为“黑盒镜像”,极不推荐!因为别人根本不知道你在这个镜像里敲过什么命令、有没有埋下木马。如何用“代码”把这个构建过程写得明明白白?这将是我们第四课(Dockerfile)要彻底解决的终极问题。
六、故障排查与验收标准
常见故障排查手册
| 现象 | 可能的原因 | 排查方案 |
|---|---|---|
Unable to find image xxx locally | 本地没有这个镜像的缓存。 | 这不是报错。Docker 会自动去云端拉取,如果此时报错 connection timeout,请检查上一课的镜像加速器配置。 |
docker exec 报错 Container is not running | 容器已经处于 Exited 状态。 | exec 只能进入存活的容器。请先用 docker start 容器名 把主进程跑起来。 |
docker rm 报错 You cannot remove a running container | 试图直接销毁正在运行的容器。 | 为了防止误删数据,Docker 拒绝销毁存活容器。先 docker stop,或者加暴力参数 docker rm -f。 |
| 容器名冲突报错 | 你试图用 --name 给新容器起一个已经被占用的名字。 | 使用 docker ps -a 找出占用名字的旧容器,将其删掉,或者换个新名字。 |
本课验收清单
完成本课的学习和实验后,你应当能够:
