overlayfs 是什么
overlayfs(叠加文件系统)是 Linux 内核 3.18+ 原生支持的 union mount 技术。它将多个目录"叠"在一起,形成统一视图:
| 角色 |
说明 |
| lowerdir(只读层) |
存放原始、不可变文件 |
| upperdir(可写层) |
存放修改和新增的文件 |
| mountpoint(合并视图) |
用户看到的统一目录树 |
内核自动处理同名文件优先显示上层、写操作触发 copy-up 等逻辑,对应用层完全透明。
环境准备 & 创建测试目录
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
|
# 1. 确认内核支持
$ grep overlay /proc/filesystems
nodev overlay # ← 输出说明支持
$ uname -r
# 2. 创建四件套目录(下层 + 上层 + 工作目录 + 挂载点)
mkdir -p ~/overlayfs_blog/{lowerdir,upperdir,workdir,mountpoint}
# 3. 在 lowerdir 放置原始文件
echo "I am in lowerdir - file1" > lowerdir/file1.txt
echo "I am in lowerdir - file2" > lowerdir/file2.txt
mkdir lowerdir/subdir && echo "I am in lowerdir subdir" > lowerdir/subdir/file3.txt
# 4. 在 upperdir 预先放一些修改
echo "Modified content - copied up" > upperdir/file1.txt
echo "I am created in upperdir" > upperdir/upper_only.txt
|
挂载前各目录状态:
1
2
3
4
5
6
7
8
9
10
11
12
|
~/overlayfs_blog/lowerdir/
├── file1.txt
├── file2.txt
└── subdir/
└── file3.txt
~/overlayfs_blog/upperdir/
├── file1.txt ← 已预先修改 file1
└── upper_only.txt ← 仅在上层存在的文件
~/overlayfs_blog/workdir/ (系统内部使用,空白)
~/overlayfs_blog/mountpoint/ (挂载点,挂载后才有内容)
|
挂载 & 验证合并视图
1
2
3
4
5
|
sudo mount -t overlay overlay \
-o lowerdir=/home/charles/overlayfs_blog/lowerdir,\
upperdir=/home/charles/overlayfs_blog/upperdir,\
workdir=/home/charles/overlayfs_blog/workdir \
/home/charles/overlayfs_blog/mountpoint
|
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
|
# 合并视图可见所有文件
$ ls /home/charles/overlayfs_blog/mountpoint/
file1.txt file2.txt subdir upper_only.txt
# 同名文件:upperdir 优先
$ cat mountpoint/file1.txt
Modified content - copied up # ← 优先显示 upperdir 版本
# lowerdir 独有文件正常显示
$ cat mountpoint/file2.txt
I am in lowerdir - file2
# upperdir 新增文件可见
$ cat mountpoint/upper_only.txt
I am created in upperdir
|
核心机制
Copy-on-Write(写时复制)
修改一个原本只存在于 lowerdir 的文件时,内核自动触发 copy-up:
1
2
3
4
5
6
7
8
9
10
11
12
|
# 在挂载点修改 file2(原本只在 lowerdir 存在)
$ echo "I was modified via overlay" > mountpoint/file2.txt
# upperdir 出现 copy-up 后的副本
$ ls /home/charles/overlayfs_blog/upperdir/
file1.txt file2.txt upper_only.txt # ← file2.txt 被 copy 上来了!
$ cat /home/charles/overlayfs_blog/upperdir/file2.txt
I was modified via overlay # ← upperdir 里的新版本
$ cat /home/charles/overlayfs_blog/lowerdir/file2.txt
I am in lowerdir - file2 # ← lowerdir 原文件未动
|
Whiteout(空洞标记)—— 删除文件
overlayfs 中删除文件不会真删 lowerdir,而是在 upperdir 创建 .wh. 前缀的特殊设备文件:
1
2
3
4
5
6
7
8
9
|
$ rm mountpoint/file2.txt
# upperdir 中出现 whiteout(字符设备 major=0, minor=0)
$ ls -la upperdir/
crw------- 1 root root 0, 0 ... .wh.file2 # ← .wh. 前缀 = 隐藏
# lowerdir 的 file2 完好无损,只是被 whiteout 隐藏
$ cat lowerdir/file2.txt
I am in lowerdir - file2
|
删除目录同理:
1
2
3
4
5
6
|
$ rm -rf mountpoint/subdir
$ ls -la upperdir/ | grep whiteout
crw------- 1 root root 0, 0 ... .wh.subdir # ← 隐藏整个目录
$ ls lowerdir/subdir/
file3.txt # ← 还在,只是被 whiteout 隐藏了
|
新增文件
1
2
3
4
5
6
7
8
|
$ echo "Brand new file" > mountpoint/new_file.txt
# upperdir 中出现新文件,lowerdir 无任何变化
$ ls upperdir/
file1.txt file2.txt upper_only.txt new_file.txt # ← 新增文件只落在 upperdir
$ ls lowerdir/
file1.txt file2.txt subdir # ← lowerdir 完全没动
|
workdir 的作用
workdir 是 overlayfs 的内部工作目录,不可省略,用途:
- copy-up 操作的临时中转区
- 实现原子性 rename(确保数据一致性)
- 管理 whiteout 文件的创建
约束:workdir 必须与 upperdir 位于同一文件系统上。
多 lowerdir 叠加
overlayfs 支持用 : 分隔多个 lowerdir,从右到左优先级递增:
1
2
3
4
5
|
sudo mount -t overlay overlay \
-o lowerdir=/layer3:/layer2:/layer1,\
upperdir=/upper,\
workdir=/workdir \
/mnt/merged
|
合并视图搜索顺序:upperdir → layer3 → layer2 → layer1,同名文件以优先级最高的为准.
实验目录清理
1
2
|
sudo umount /home/charles/overlayfs_blog/mountpoint
rm -rf /home/charles/overlayfs_blog
|
Docker 镜像 & OCI 规范
2013 年 Docker 发布时定义了私有的镜像格式,“Docker Image"一词由此而来。随着容器生态爆发,不同厂商各自的实现互不兼容,标准化呼声日益高涨。2015 年 Docker 联合 CoreOS 等公司发起成立 Open Container Initiative(OCI),将镜像格式、运行时规范开放为标准:
| 年份 |
事件 |
| 2015 |
OCI 成立,推动容器标准化 |
| 2016 |
发布 runtime-spec(即 runc 的实现基础) |
| 2017 |
发布 image-spec 1.0,统一镜像格式 |
| 2020 |
Docker Hub 停止接收旧的 schema 1 镜像 |
| 2023– |
Docker 25+ 默认使用 containerd image store,直接以 OCI 格式存储 |
现今 docker pull 拉下来的、docker build 构建出来的,本质上都是 OCI 镜像。Docker 只是 OCI 规范最流行的实现者之一。
什么是 Docker/OCI 镜像
一个镜像 = 只读的分层 rootfs(layers) + 描述层和运行参数的 config JSON。
- 每一层是一个 gzip 压缩的 tar 包(diff,而非完整快照)
RUN / COPY / ADD 产生新层,CMD / ENV / EXPOSE 只写进 config 不产生层
- 每层以 sha256 digest(内容寻址) 唯一标识——内容相同则 digest 相同,天然去重
磁盘布局:docker save 导出长什么样
1
2
3
4
5
6
7
8
9
10
11
12
13
|
$ docker save nginx:alpine -o nginx.tar && tar -xf nginx.tar
$ find . -maxdepth 3 -type f | sort
./index.json ← 多架构索引
./oci-layout ← {"imageLayoutVersion":"1.0.0"}
./manifest.json ← Docker 兼容格式(兼容旧工具)
./layer/ ← 各层(压缩 tar)
├── ...
└── blobs/sha256/ ← OCI 内容寻址 blob 存储
├── afff3924... ← image index(文件名 = sha256)
├── 796832... ← manifest
├── 4b0bc1... ← config
└── 3588d0... ← layer 1(压缩 tar.gz)
... (共 N+3 个:index + manifest + config + N 层)
|
读取链路:index.json → 找 manifest digest → 读 manifest → 找 config 和各层 digest → 逐层解压。blob 文件名就是它自己的 sha256——内容是地址,地址是内容。
核心对象的结构
index.json(多架构索引)
1
2
3
4
5
6
7
8
9
10
|
{
"schemaVersion": 2,
"mediaType": "application/vnd.oci.image.index.v1+json",
"manifests": [
{ "mediaType": "...manifest.v1+json", "digest": "sha256:796832...", "size": 7143,
"platform": { "architecture": "amd64", "os": "linux" } },
{ "mediaType": "...manifest.v1+json", "digest": "sha256:3588d0...", "size": 7200,
"platform": { "architecture": "arm64", "os": "linux" } }
]
}
|
manifest(单平台清单)
1
2
3
4
5
6
7
8
9
10
|
{
"schemaVersion": 2,
"mediaType": "application/vnd.oci.image.manifest.v1+json",
"config": { "mediaType": "...config.v1+json", "digest": "sha256:4b0bc1...", "size": 12322 },
"layers": [
{ "mediaType": "...layer.v1.tar+gzip", "digest": "sha256:55afa1...", "size": 3846391 },
{ "mediaType": "...layer.v1.tar+gzip", "digest": "sha256:d94291...", "size": 1902283 },
...
]
}
|
config(运行配置)
1
2
3
4
5
6
7
8
9
10
11
12
|
{
"architecture": "amd64", "os": "linux",
"config": { "Cmd": ["nginx", "-g", "daemon off;"],
"Entrypoint": ["/docker-entrypoint.sh"],
"ExposedPorts": { "80/tcp": {} } },
"rootfs": { "type": "layers",
"diff_ids": ["sha256:34884a...", "sha256:1c4d71...", ...] },
"history": [ { "created_by": "ADD alpine-minirootfs-3.24.1-x86_64.tar.gz /" },
{ "created_by": "RUN /bin/sh -c apk add nginx",
"empty_layer": true },
... ]
}
|
diff_ids = 每层解压后 tar 的 sha256(runtime 用,与 manifest 里压缩 blob 的 digest 不同)
history = Dockerfile 指令历史,对应每层的构建过程
- ImageID =
sha256(canonical JSON of config),内容决定身份
example:nginx:alpine 8 层解剖
1
2
3
4
5
6
7
8
9
10
|
$ docker pull nginx:alpine
55afa1ecc21d: Pull complete ← 每行 = 一层
0ea935727878: Pull complete
d94291c26261: Pull complete
1ed8e39a7434: Pull complete
0d62d88506ba: Pull complete
8cdfe4f23778: Pull complete
da4dea1b00af: Pull complete
fb597529c916: Pull complete
Digest: sha256:db35bfc6... # ← 镜像 ID(index digest)
|
1
2
|
$ docker save nginx:alpine -o nginx.tar && tar -xf nginx.tar
$ ls blobs/sha256/ | wc -l # 11 个:index + manifest + config + 8 层
|
| 层 |
大小 |
内容 |
| L1 |
~3.8 MB |
alpine 基础 rootfs |
| L2 |
~1.9 MB |
建 nginx 用户/组 + apk 签名密钥 |
| L3~L7 |
几百字节 |
各一个 shell 脚本 |
| L8 |
~20 MB |
apk add nginx 二进制和配置 |
层大小差异极大——每层只存 diff,不是完整快照。
运行时:overlayfs 把层叠起来
containerd 把每层展开为目录,再用 overlayfs 联合挂载:
1
2
3
4
5
|
$ docker run --rm nginx:alpine cat /proc/self/mountinfo | grep ' / '
... overlay rw,
lowerdir=.../snapshots/63/fs:.../39/fs:.../4/fs, # 8 个镜像层
upperdir=.../snapshots/64/fs, # 容器专属可写层
workdir=.../snapshots/64/work
|
- lowerdir:各层目录,从左到右优先级从高到低
- upperdir:容器可写层,删容器即消失
- 合并视图 = 容器里看到的
/
覆盖与删除在层内同样表现为 diff:
1
2
3
4
|
FROM alpine:3.20
RUN echo "v1" > /app/version.txt # 层2:新建
RUN echo "v2" > /app/version.txt # 层3:同名覆盖
RUN rm /app/version.txt # 层4:写入 .wh.version.txt(whiteout)
|
解包层 tar 可见:层3 有 app/version.txt(覆盖),层4 有 app/.wh.version.txt(隐藏)——这正是 overlayfs 合并语义在 OCI layer 里的表达。
Layer 的四大价值
| 价值 |
说明 |
| 省构建时间 |
缓存:只改最后一行,前面全部命中缓存 |
| 省磁盘 |
去重:内容相同的层 digest 相同,全机器只存一份 |
| 省流量 |
增量:registry 按 blob digest 逐层校验,已有层跳过 |
| 省心智 |
可审计:每条指令一层,层只读不可篡改 |
总结
overlayfs 通过只读下层 + 可写上层 + 统一合并视图的设计,实现了轻量级的文件叠加机制。其核心行为——Copy-on-Write 写时复制和 Whiteout 空洞标记——保证了下层数据不被破坏的同时,赋予上层灵活的读写能力。
这一机制是 Docker 容器和 OCI 镜像分层架构的底层基石:每一个镜像层是一个只读的 tar 快照,容器运行时由 overlayfs 将这些层与容器的可写层联合挂载,呈现出完整的根文件系统。
OCI 与容器镜像构建
从 Docker OverlayFS 到 OCI镜像格式