这个问题其实触及了容器化技术最核心的概念。在 Docker 的语境下,镜像(Image)和实例(Container/Instance)的关系,本质上就是面向对象编程中“类(Class)”与“对象(Object)”的关系,或者更通俗地说,是“蓝图”与“房子”的区别。
我们可以从以下几个维度来拆解它们的本质区别:
1. 静态 vs 动态
-
镜像(Image)是静态的只读模板。
当你执行docker build或docker pull时,你得到的是一个分层存储的文件集合。它包含了运行应用所需的所有东西:代码、运行时环境(如 JRE、Node.js)、系统库、配置文件等。镜像一旦构建完成,其内部内容就固定不变了。你可以把镜像理解为一个“压缩包”或者一个“快照”,它是不可变的(Immutable)。 -
实例(Container)是动态运行的进程。
当你执行docker run命令基于某个镜像启动容器时,Docker 会在该镜像的最上层添加一个可写层(Writable Layer)。这个可写层允许容器在运行时产生数据、修改文件状态、记录日志。更重要的是,容器不仅仅是一堆文件,它是一个正在执行的进程,拥有独立的 PID、网络栈、文件系统视图等资源隔离空间。
2. 存储结构差异
这是理解两者区别的关键技术点。Docker 采用 AUFS、Overlay2 等联合文件系统。
-
镜像由多个只读层组成。
比如一个 Nginx 镜像可能由基础 OS 层、Nginx 二进制层、配置层等组成。这些层在磁盘上是共享的。如果你拉取了 10 个不同的镜像,它们底层的基础操作系统层(如 Ubuntu 22.04)在物理磁盘上只存了一份,节省了存储空间。 -
实例在最顶层有一个可写层。
当容器启动时,Docker 会挂载一个新的可写层到镜像之上。容器内产生的任何写入操作(如/var/log/nginx/access.log的增长,或者临时文件的创建)都发生在这个可写层中。一旦容器被删除(docker rm),这个可写层及其中的所有数据通常也会随之消失(除非挂载了 Volume)。
3. 生命周期与复用性
-
镜像用于分发和标准化。
镜像是交付物。开发人员在本地构建好镜像,推送到 Harbor 或阿里云 ACR,然后在测试、预发、生产环境中拉取同一个镜像。这保证了“一次构建,到处运行”的一致性。镜像本身不消耗 CPU 和内存资源,只有在你尝试运行它时才涉及资源调度。 -
实例是资源的占用者。
实例是工作负载。每一个实例都会占用宿主机的 CPU、内存、I/O 和网络带宽。你可以基于同一个镜像启动 100 个实例(横向扩展),每个实例都是独立的,互不影响。如果其中一个实例崩溃,其他实例依然正常运行,这正是微服务架构高可用的基础。
4. 实际场景中的类比
为了更直观地理解,我们可以用几个常见的类比:
| 类比对象 | 镜像 (Image) | 实例 (Container) |
|---|---|---|
| 编程 | 类 (Class) | 对象 (Object) |
| 建筑 | 建筑设计图纸 | 按照图纸建成的具体房屋 |
| 食品 | 冷冻披萨包装(配方+原料) | 烤箱里加热好的披萨 |
| 车辆 | 汽车设计图 + 零件清单 | 路上行驶的具体某辆车 |
5. 常见误区澄清
-
“我改了容器里的文件,镜像变了吗?”
没有。你在容器里修改文件,只是修改了容器的可写层。如果你退出并重新基于原镜像启动一个新容器,之前的修改就没了。如果你想永久保存修改,需要使用docker commit(不推荐,因为破坏了镜像的分层理念)或者更好的方式:修改 Dockerfile 重新构建镜像。 -
“容器删除后数据还在吗?”
默认情况下,容器可写层中的数据会随着容器删除而丢失。这就是为什么在生产环境中,我们强烈建议使用 Volume(数据卷) 或 Bind Mount(绑定挂载) 将重要数据映射到宿主机或专用存储设备上,这样即使容器销毁重建,数据依然存在。
总结
简单来说:
- 镜像是你打包好的、标准化的、只读的应用程序包。
- 实例是这个包在服务器上运行时产生的、有状态的、可写的运行实体。
在实际运维和开发中,你的工作流通常是:编写代码 -> 构建镜像 -> 推送镜像 -> 部署镜像生成实例 -> 监控实例状态。理解这一流程,就能很好地把握 Docker 的核心逻辑。
云技术博客