容器镜像与存储原理
在Docker Registry v2 中,镜像是以分层的形式存储的,采用了内容寻址和分层存储的机制。
各个结构的示意图如下:

假设存储一个名为 my-app:latest 的镜像,目录结构如下:
registry/
└── docker
└── registry
└── v2
├── blobs/
│ └── sha256/
│ ├── <digest-of-layer-1>/
│ ├── <digest-of-layer-2>/
│ ├── <digest-of-layer-3>/
│ └── <digest-of-config>/
└── repositories/
└── my-app/
└── _manifests/
└── tags/
└── latest/
└── current/
└── link下面分别介绍各个部分的结构.
image name
镜像名:镜像名是用户访问镜像的基础标识,通常以<仓库名>:<标签>的形式表示,例如:my-registry.com/my-app:latest.
tag
标签:镜像标签是对具体镜像版本的友好引用。标签本身并不包含实际数据,而是指向一个Manifest,多个标签可以指向同一个 manifest,实现镜像的版本管理和复用。
tags/latest/current/link 指向对应的 Manifest。
manifest
manifest是一个JSON 格式的文本文件,存储在 Docker Registry 中,用于描述一个镜像的组成部分,主要包括以下三部分:
镜像的各层(Layers)的 Digest。
镜像的配置(Config),包括启动命令、环境变量等。
层的媒体类型(Media Type)和大小。
manifest是一个JSON 格式的文本文件,存储在 Docker Registry 中,用于描述一个镜像的组成部分。存储在manifests文件夹下。
manifest结构举例如下:
{
"schemaVersion": 2,
"mediaType": "application/vnd.docker.distribution.manifest.v2+json",
"config": {
"mediaType": "application/vnd.docker.container.image.v1+json",
"size": 7023,
"digest": "sha256:c54a2cc56cbb2f0406b44acb6b5f5b3dbb21f19d50f8eedc5bba7d6a9dd67f58"
},
"layers": [
{
"mediaType": "application/vnd.docker.image.rootfs.diff.tar.gzip",
"size": 32654,
"digest": "sha256:ec4a7858f0b2561c830233f6e1f3b6a08efc1d7d706032d22b9a6f0d69a7c4cc"
},
{
"mediaType": "application/vnd.docker.image.rootfs.diff.tar.gzip",
"size": 16724,
"digest": "sha256:4cf1eda4d058d6915a4ea26453ed309b6937383c6e5b51a6da64e24835a5c303"
},
{
"mediaType": "application/vnd.docker.image.rootfs.diff.tar.gzip",
"size": 73109,
"digest": "sha256:8c662931926fae5a8fd0d7a3096f7b44c29b28a8e6a4b5e2a48b00601234abcd"
}
]
}layer
镜像是由多个层组成的,层是文件系统的增量变化记录,每一层存储新增修改或删除的文件.层之间是有序的,构成了镜像的完整文件系统。每一层在 Docker Registry 或本地存储中被保存为一个压缩的 tar 文件,包含该层的文件和目录结构。
添加文件。
修改文件。
删除文件。
镜像的每一层以内容哈希值(Digest, SHA256)为文件名存储在 blobs目录中。每一层的 Blob 存储在 blobs/sha256下。
分层的具体内容
每一层表示镜像构建过程中对文件系统的增量修改。这些修改通常包括新增文件、修改文件、删除文件、安装软件等操作。每一层只记录从上一层的增量变化,因此镜像存储高效且支持分层复用。
以下面这个构建镜像的 Dockerfile 举例
# 基础镜像
FROM ubuntu:20.04
# 设置工作目录
WORKDIR /app
# 安装软件
RUN apt-get update && apt-get install -y python3
# 复制本地文件到镜像
COPY app.py /app/app.py
# 设置默认启动命令
CMD ["python3", "app.py"]当这个 Dockerfile 被执行时,每条指令(Instruction)都会生成一层layer.
FROM ubuntu:20.04这一层使用了基础镜像 ubuntu:20.04,其内容包含该镜像的文件系统快照。
例如:
/bin/bash
/etc/apt/
/lib/
...这是整个镜像的第一个层,所有后续层都基于它。
WORKDIR /app这一层的内容是工作目录的变更。 实际上,这个层并不会在文件系统中创建新文件或目录,而是仅在元数据中记录 "工作目录为 /app" 的设置。
RUN apt-get update && apt-get install -y python3这一层包含执行命令后对文件系统的修改: 更新 apt-get 缓存(通常修改 /var/lib/apt/lists/)。 安装 Python3(添加文件到 /usr/bin/python3 和相关依赖到 /usr/lib/ 等)。 例如,新增的文件可能包括:
/usr/bin/python3
/usr/lib/python3/
/var/lib/dpkg/
/etc/apt/sources.list
...COPY app.py /app/app.py这一层的内容是将 app.py 文件从构建上下文复制到镜像内的 /app/ 目录。
CMD ["python3", "app.py"]这一层不改变文件系统,只更新镜像的元数据,记录默认的启动命令。 镜像中的 CMD 命令只是设置运行容器时的默认行为。
整个过程可以用一张图表表示:
blob
Blob是存储镜像层和配置的基本单元,每个 Blob 对应镜像的一个层或配置文件。
Blob 存储是无状态的,可以独立存放在不同的后端存储中(如本地文件系统、Amazon S3、Google Cloud Storage 等)。通过配置文件 config.yml 可以切换存储后端。
镜像层、可写层与 Copy-on-Write
镜像层 = 只读的"模板"(所有容器共享)
可写层 = 每个容器独有的"草稿纸"(容器删除就没了)
单个容器的文件系统结构
容器看到的文件系统 (rootfs)
↓
┌─────────────────┐
│ 可写层 │ ← 容器运行时的修改都在这
│ (Container) │ (读写,容器独有)
├─────────────────┤
│ 镜像层3 │ ← 只读,所有用这个镜像的
│ (Layer 3) │ 容器共享
├─────────────────┤
│ 镜像层2 │ ← 只读,共享
│ (Layer 2) │
├─────────────────┤
│ 镜像层1 │ ← 只读,共享
│ (Layer 1) │
└─────────────────┘
↓
物理存储位置
场景:运行3个Nginx容器
物理存储上:
/var/lib/containerd/
├── 镜像层存储/
│ ├── layer1: nginx基础层 (100MB) ← 只有1份
│ ├── layer2: nginx配置层 (10MB) ← 只有1份
│ └── layer3: nginx应用层 (50MB) ← 只有1份
│
└── 可写层存储/
├── 容器A的可写层 (5MB) ← 容器A独有
├── 容器B的可写层 (3MB) ← 容器B独有
└── 容器C的可写层 (8MB) ← 容器C独有
总占用 = 160MB(镜像层) + 16MB(可写层) = 176MB
如果没有分层,需要: 160MB × 3 = 480MB
容器内的操作
bash
# 在容器A中操作
docker exec -it 容器A bash
# 1. 读取文件 /usr/share/nginx/index.html
cat /usr/share/nginx/index.html
# → 从镜像层读取(所有容器看到的一样)
# 2. 修改文件
echo "Hello A" > /usr/share/nginx/index.html
# → 触发 Copy-on-Write (写时复制)
# → 文件从镜像层复制到容器A的可写层
# → 修改只在容器A的可写层
# 3. 新建文件
echo "test" > /tmp/myfile.txt
# → 直接写入容器A的可写层
其他容器不受影响
bash
# 在容器B中
cat /usr/share/nginx/index.html
# → 仍然读取镜像层的原始文件
# → 看不到容器A的修改
五、写时复制(Copy-on-Write)机制
工作原理
1. 容器要修改 /etc/nginx/nginx.conf
2. 检查文件位置
┌─────────────────────────┐
│ 文件在镜像层? │
│ Yes → 触发COW │
└──────────┬──────────────┘
↓
┌──────────────────────────┐
│ 复制文件到可写层 │
└──────────┬───────────────┘
↓
┌──────────────────────────┐
│ 在可写层修改文件 │
└──────────┬───────────────┘
↓
┌──────────────────────────┐
│ 容器看到的是可写层的版本 │
│ 镜像层的原文件被"遮住" │
└──────────────────────────┘Thin Pool 补充
Thin Pool 是 Linux 存储管理中的一个概念,主要用于 LVM (Logical Volume Manager) 和 Docker 等容器技术中。
简单说,thin pool 就是一个"按需分配"的存储池,先承诺给你很大空间,但实际只在你用的时候才真正占用物理磁盘。
允许分配比实际物理存储更多的逻辑空间
只在真正写入数据时才占用实际存储空间
类似"超卖"概念,提高存储利用率
Thin Pool 与 VG、PV、LV 的关系
物理磁盘 (/dev/sda, /dev/sdb)
↓
PV (Physical Volume - 物理卷)
↓
VG (Volume Group - 卷组)
↓
LV (Logical Volume - 逻辑卷)
├── 传统 LV (厚配置)
└── Thin Pool (精简池)
↓
Thin LV (精简逻辑卷)磁盘: /dev/sdb(100GB) /dev/sdc(100GB)
↓ ↓
PV: pv_sdb pv_sdc
└────────┬────────┘
↓
VG: vg_data (200GB)
↓
┌────────┴────────┐
↓ ↓
LV: 传统LV (50GB) Thin Pool (100GB) ← 这也是LV
↓
┌─────┼─────┐
↓ ↓ ↓
Thin LV: vol1 vol2 vol3
(50G) (50G) (50G)