Overlayfs 与 docker/oci Image

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

合并视图搜索顺序:upperdirlayer3layer2layer1,同名文件以优先级最高的为准.

实验目录清理

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镜像格式

Licensed under CC BY-NC-SA 4.0
使用 Hugo 构建
主题 StackJimmy 设计