镜像是什么,容器又是什么
一句话总结
镜像是由只读层堆叠而成的文件树,容器则是在其上添加一个可写层后运行的普通 Linux 进程。
为什么需要理解这一点
“容器是轻量级 VM”这种说法流传已久,但它在几乎所有实际问题中都会导致错误预测。在主机上执行 ps,可以直接看到容器内的 nginx。
48213 root nginx
48261 systemd+ nginx
从容器内查看同样的进程,会显示为 1 nginx、29 nginx。并不是有两套进程,而是从不同命名空间观察同一进程。查看内核版本会更加清楚:主机、alpine 容器和 ubuntu 容器报告的都是同一个内核。镜像提供的不是内核,而只是用户空间文件树。
工作原理
各层通过联合文件系统(overlayfs)叠加。
merged <- 컨테이너가 보는 뷰
↑ upperdir (쓰기 가능, 컨테이너 레이어)
↑ lowerdir (읽기 전용, 이미지 레이어들이 겹겹이)
- 读取:从上向下查找,最上层的文件优先。
- 写入:修改下层文件时,先将其**复制到上层(copy-up)**再修改。
- 删除:并不真正删除,而是在上层留下白化标记。
每一层都以内容的 SHA-256 哈希命名。相同内容会得到相同名称,因此无需重复存储,只需比较哈希便能验证完整性。镜像 ID 使用 sha256:... 也是同样的原因。
层形成时决定了什么
Dockerfile 的每条命令都会产生一层,而且**层不会被删除。**大多数实际陷阱都来自这两句话。
COPY secret.pem /tmp/secret.pem ← 레이어 3에 파일이 들어간다
RUN ./setup.sh && rm /tmp/secret.pem ← 레이어 4는 "지웠다" 는 기록일 뿐
最终镜像中看不到 /tmp/secret.pem,但它仍然完整存在于第 3 层。任何人都能通过 docker save 解包并取出。把秘密放进镜像后再删除,并不是真正删除。应使用构建秘密(RUN --mount=type=secret)或多阶段构建解决。
同理,镜像大小也不会减小。
RUN apt-get update && apt-get install -y build-essential # +400MB
RUN apt-get purge -y build-essential # 여전히 +400MB
必须在同一个 RUN 中安装并删除,才能只留下该层的最终状态。
删除容器后仍会保留什么
删除容器时,可写层会消失。因此,在容器内创建的文件会随 docker rm 一同消失。这就是需要卷的原因。
| 存储位置 | 删除容器后 | 写入性能 | 使用场景 |
|---|---|---|---|
| 可写层 | 消失 | 慢(copy-up) | 临时文件 |
| 匿名卷 | 保留(没有名称,难以查找) | 快 | 意外产生的卷 |
| 命名卷 | 保留 | 快 | DB 数据 |
| bind mount | 保持主机文件原样 | 快 | 开发期间的源码 |
“copy-up” 非常重要。在 overlayfs 中,修改下层文件时会把整个文件复制到上层。修改 1GB 文件中的一个字节,也会复制完整的 1GB。这就是数据库不能在没有卷的情况下运行于容器层的原因。
镜像名称与 Digest
标签是可变的。nginx:1.25 昨天和今天可能指向不同镜像。latest 更是如此——它并不表示“最新”,只是省略标签时使用的默认名称。
nginx:1.25 ← 움직인다
nginx@sha256:a1b2c3... ← 절대 안 움직인다
生产部署应固定到 Digest。这样,在排查“昨天能用、今天不能用”时,可以先确认“昨天与今天是否使用同一个镜像”。
生产现场中的表现
使用同一基础镜像的 100 个容器会完整共享 lowerdir,因此容器创建速度快,也能节省磁盘。反过来,由于 copy-up 成本,数据库文件这类大型写入必须放入卷中。
还有一点:混淆 docker save 与 docker export 会浪费一整天。save 导出的是包含层、标签和历史记录的镜像,export 导出的只是容器的单层文件系统。将 export 结果 import 后,ENTRYPOINT 和 ENV 都会消失。
下一项实验要做什么
亲自查看镜像列表,启动一个容器并检查日志,再通过镜像 ID 证明添加标签并不会创建新镜像。