Docker 文件系统深度解析:从镜像分层到 OverlayFS

> 发布日期:2026-03-31 | 作者:Tony @ Jay's Lab

>

> 完整解析 Docker 的文件系统架构:镜像如何分层存储、容器如何读写文件、OverlayFS 的工作原理、数据持久化方案,以及性能调优实践。

1. 核心概念:一切皆分层

Docker 的文件系统建立在一个核心设计上:联合文件系统(Union Filesystem)——把多个目录"叠"在一起,呈现为一个统一的文件系统。


用户看到的容器文件系统(合并视图)
┌──────────────────────────┐
│  /app/server.js (修改过)   │  ← 容器可写层
│  /var/log/app.log (新文件)  │
├──────────────────────────┤
│  /app/server.js (原始)     │  ← 镜像层 3(COPY . /app)
│  /app/package.json         │
├──────────────────────────┤
│  /usr/bin/node             │  ← 镜像层 2(RUN apt install nodejs)
│  /usr/lib/libnode.so       │
├──────────────────────────┤
│  /bin/bash                 │  ← 镜像层 1(基础镜像 ubuntu:22.04)
│  /lib/x86_64-linux-gnu/    │
│  /etc/apt/sources.list     │
└──────────────────────────┘

关键规则:

2. 镜像分层:Dockerfile 的每一行都是一层

2.1 层是怎么产生的


FROM ubuntu:22.04          # 层 1:基础镜像(~77MB)
RUN apt update && \
    apt install -y nodejs  # 层 2:安装软件(~120MB)
COPY . /app                # 层 3:复制代码(~5MB)
CMD ["node", "/app/server.js"]  # 不产生新层(元数据指令)

产生新层的指令RUNCOPYADD

不产生新层的指令CMDENVEXPOSEWORKDIRENTRYPOINT(只修改元数据)

2.2 查看镜像分层


# 查看层信息
docker history my-app:latest

IMAGE          CREATED       SIZE    COMMENT
a1b2c3d4e5f6   2 min ago    5.2MB   COPY . /app
f6e5d4c3b2a1   5 min ago    120MB   RUN apt update && apt install -y nodejs
ubuntu:22.04   3 weeks ago  77.8MB  base image

# 详细层信息(含 diff_id)
docker inspect my-app:latest | jq '.[0].RootFS'
{
  "Type": "layers",
  "Layers": [
    "sha256:aaa...",  # 层 1
    "sha256:bbb...",  # 层 2
    "sha256:ccc..."   # 层 3
  ]
}

2.3 层共享节省空间


镜像 A (node-app)          镜像 B (python-app)
┌──────────────┐          ┌──────────────┐
│ COPY . /app  │ 5MB      │ COPY . /app  │ 8MB
├──────────────┤          ├──────────────┤
│ RUN npm i    │ 80MB     │ RUN pip i    │ 150MB
├──────────────┤          ├──────────────┤
│              ubuntu:22.04              │ ← 共享!只存一份
│              77MB(磁盘上只占一次)       │
└──────────────────────────────────────┘

# 查看实际磁盘占用(含共享层信息)
docker system df -v

TYPE        TOTAL   ACTIVE  SIZE     RECLAIMABLE
Images      5       3       1.2GB    450MB (37%)
Containers  3       2       125MB    50MB (40%)
Volumes     4       3       890MB    200MB (22%)

3. OverlayFS:Docker 的默认存储驱动

3.1 什么是 OverlayFS

OverlayFS 是 Linux 内核(3.18+)内置的联合文件系统,也是 Docker 的默认存储驱动(overlay2)。

它把多个目录合并为一个:


merged/     ← 容器看到的统一视图(mount point)
  │
  ├── upperdir/  ← 容器可写层(所有修改写这里)
  ├── lowerdir/  ← 镜像只读层(可以有多层,用 : 分隔)
  └── workdir/   ← OverlayFS 内部临时目录(原子操作用)

3.2 手动体验 OverlayFS

不用 Docker,直接用 mount 感受:


# 创建目录
mkdir -p /tmp/overlay/{lower,upper,work,merged}

# 在 lower(只读层)放文件
echo "original content" > /tmp/overlay/lower/file.txt
echo "read-only file" > /tmp/overlay/lower/readonly.txt

# 挂载 OverlayFS
sudo mount -t overlay overlay \
  -o lowerdir=/tmp/overlay/lower,\
upperdir=/tmp/overlay/upper,\
workdir=/tmp/overlay/work \
  /tmp/overlay/merged

# 在 merged 视图里可以看到 lower 的文件
cat /tmp/overlay/merged/file.txt
# → "original content"

# 修改文件
echo "modified!" > /tmp/overlay/merged/file.txt

# lower 没变!修改写到了 upper
cat /tmp/overlay/lower/file.txt
# → "original content"(不变)
cat /tmp/overlay/upper/file.txt
# → "modified!"(Copy-on-Write)

# 删除文件
rm /tmp/overlay/merged/readonly.txt

# lower 里文件还在,upper 里产生了一个"白障"文件
ls -la /tmp/overlay/upper/
# → readonly.txt 变成了 character device (0,0) — whiteout 标记

# 清理
sudo umount /tmp/overlay/merged

3.3 OverlayFS 的三个核心操作

读取(Read)


读 /app/server.js
  │
  ▼ 先查 upperdir(容器层)
  │   找到了?→ 返回
  │   没找到?↓
  ▼ 再查 lowerdir(镜像层,从上到下)
      找到了?→ 返回
      没找到?→ 文件不存在

性能:读未修改的文件直接从 lowerdir 读,零拷贝,和宿主机直接读文件一样快。

写入 / 修改(Copy-on-Write)


写 /app/server.js(已存在于 lowerdir)
  │
  ▼ 第一次修改?
  │   是 → 整个文件从 lowerdir 复制到 upperdir(Copy-Up)
  │        然后在 upperdir 的副本上修改
  │   否 → 直接修改 upperdir 的副本

Copy-on-Write 的代价


# 查看容器层的实际写入量
docker diff <container>

A /var/log/app.log        # Added(新增)
C /app/server.js          # Changed(修改,触发了 copy-up)
D /tmp/cache.tmp          # Deleted(删除,产生 whiteout)

删除(Whiteout)


删除 /app/old-file.txt(存在于 lowerdir)
  │
  ▼ 不能真删 lowerdir 的文件(只读)
  │
  ▼ 在 upperdir 创建 "whiteout" 标记文件
    → character device (0, 0)
    → OverlayFS 看到标记后,在 merged 视图中隐藏该文件

删除目录用 opaque whiteout.wh..wh..opq 文件),表示"忽略 lowerdir 中该目录的所有内容"。

3.4 Docker 中的 OverlayFS 实际结构


# 查看 Docker 使用的存储驱动
docker info | grep "Storage Driver"
# → Storage Driver: overlay2

# 查看容器的 overlay 挂载
docker inspect <container> | jq '.[0].GraphDriver'
{
  "Data": {
    "LowerDir": "/var/lib/docker/overlay2/abc123/diff:
                 /var/lib/docker/overlay2/def456/diff:
                 /var/lib/docker/overlay2/ghi789/diff",
    "MergedDir": "/var/lib/docker/overlay2/xyz000/merged",
    "UpperDir": "/var/lib/docker/overlay2/xyz000/diff",
    "WorkDir": "/var/lib/docker/overlay2/xyz000/work"
  },
  "Name": "overlay2"
}

# 直接查看容器的可写层
ls /var/lib/docker/overlay2/xyz000/diff/

4. 存储驱动对比

4.1 主流存储驱动

驱动内核要求后端状态适用场景
**overlay2**4.0+OverlayFS✅ 默认推荐所有场景
**btrfs**Btrfs 文件系统Btrfs 用户
**zfs**ZFS 文件系统ZFS 用户
**fuse-overlayfs**FUSErootless Docker
~~aufs~~AuFS❌ 已弃用
~~devicemapper~~Device Mapper❌ 已弃用

4.2 overlay2 vs 其他


# 检查当前驱动
docker info --format '{{.Driver}}'

# 切换存储驱动(需要重建所有镜像!)
# /etc/docker/daemon.json
{
  "storage-driver": "overlay2"
}

overlay2 的优势

5. 数据持久化:容器层之外的存储

容器层的数据在容器删除后就没了。持久化方案:

5.1 三种挂载方式


Docker Host
┌─────────────────────────────────────────┐
│                                         │
│  /var/lib/docker/volumes/mydata/_data   │ ← Volume
│  /home/jay/project                      │ ← Bind Mount
│  (tmpfs in memory)                      │ ← tmpfs
│                                         │
└────────────┬──────────┬──────────┬──────┘
             │          │          │
             ▼          ▼          ▼
         Container: /data    /app     /tmp

5.2 Volume(推荐)


# 创建
docker volume create mydata

# 使用
docker run -v mydata:/data my-app

# 查看
docker volume inspect mydata
{
  "Mountpoint": "/var/lib/docker/volumes/mydata/_data",
  "Driver": "local"
}

# Volume 存在于 Docker 管理的目录,容器删了 volume 还在
docker rm my-container  # 容器没了
docker volume ls        # volume 还在

5.3 Bind Mount(开发用)


# 把宿主机目录挂进容器
docker run -v /home/jay/project:/app my-app

# 新语法(更明确)
docker run --mount type=bind,source=/home/jay/project,target=/app my-app

# 只读挂载
docker run -v /home/jay/config:/etc/myapp:ro my-app

5.4 tmpfs(内存文件系统)


# 敏感数据(密码、密钥)不落盘
docker run --tmpfs /tmp:size=100m my-app

# 高性能临时目录
docker run --mount type=tmpfs,destination=/cache,tmpfs-size=256m my-app

5.5 对比

VolumeBind Mounttmpfs
管理方Docker用户内核
位置/var/lib/docker/volumes/任意路径内存
容器删除后**保留****保留****丢失**
多容器共享
适用场景数据库、持久数据开发(代码热更新)临时/敏感数据
备份docker volume 命令直接 cp不需要
性能接近原生**原生****最快**(内存)

6. 容器文件系统的大小控制

6.1 查看容器写了多少数据


# 查看每个容器的文件系统使用
docker ps -s

CONTAINER ID   IMAGE    SIZE
a1b2c3d4e5f6   my-app   125MB (virtual 330MB)
#                        ↑ 容器层    ↑ 含镜像层总大小

# 详细 diff
docker diff <container>

6.2 限制容器写入量


# overlay2 不直接支持容器存储配额
# 但可以用 --storage-opt(需要特定文件系统支持)
docker run --storage-opt size=10G my-app

# 更常见的做法:用 tmpfs 限制临时目录
docker run --tmpfs /tmp:size=100m my-app

6.3 减小镜像体积的技巧


# ❌ 坏:每层都有残留
RUN apt update
RUN apt install -y nodejs
RUN apt clean

# ✅ 好:单层完成,清理不占空间
RUN apt update && \
    apt install -y nodejs && \
    apt clean && \
    rm -rf /var/lib/apt/lists/*

# ✅ 更好:多阶段构建
FROM node:20 AS builder
COPY . /app
RUN npm ci && npm run build

FROM node:20-slim          # 更小的基础镜像
COPY --from=builder /app/dist /app
CMD ["node", "/app/server.js"]
# builder 阶段的 node_modules 不会出现在最终镜像

7. /var/lib/docker 目录结构


/var/lib/docker/
├── overlay2/                    # 存储驱动数据
│   ├── abc123.../               # 每个层一个目录
│   │   ├── diff/                # 该层的文件内容
│   │   ├── link                 # 短标识符(用于构建 mount 参数)
│   │   ├── lower                # 指向下层的链接
│   │   ├── merged/              # 合并视图(容器运行时才有)
│   │   └── work/                # OverlayFS 工作目录
│   └── l/                       # 短链接目录(避免 mount 参数过长)
│       ├── ABC123 -> ../abc123/diff
│       └── DEF456 -> ../def456/diff
├── image/                       # 镜像元数据
│   └── overlay2/
│       ├── imagedb/             # 镜像配置
│       ├── layerdb/             # 层元数据
│       └── repositories.json    # 镜像名 → ID 映射
├── containers/                  # 容器元数据
│   └── <container-id>/
│       ├── config.v2.json       # 容器配置
│       ├── hostname             # 主机名
│       ├── resolv.conf          # DNS
│       └── hosts                # hosts 文件
├── volumes/                     # Volume 数据
│   └── <volume-name>/
│       └── _data/               # 实际数据
├── network/                     # 网络配置
├── plugins/                     # 插件
└── tmp/                         # 临时文件

# 查看总占用
du -sh /var/lib/docker/
# → 15G

# 清理未使用的资源
docker system prune -a --volumes
# ⚠️ 会删除所有未运行容器的镜像、所有停止的容器、所有未使用的 volume

8. 高级话题

8.1 层的内容寻址(Content-Addressable)

Docker 用 SHA256 哈希标识每一层:


Layer = sha256(该层所有文件的 tar 包)

好处


# 查看镜像的层 hash
docker inspect ubuntu:22.04 | jq '.[0].RootFS.Layers'
[
  "sha256:a8b5423f...",  # 每个 hash 对应一个层
  "sha256:c4d6e7f8..."
]

# 在 registry 里的存储
# registry.example.com/v2/<repo>/blobs/sha256:a8b5423f...

8.2 Lazy Pulling(按需拉取)

传统 pull 要下载所有层。新方案:


# eStargz / Nydus / OverlayBD
# 只下载实际访问的文件,容器秒启动

# containerd + stargz-snapshotter
ctr images rpull --stargz ghcr.io/my-app:latest
# 只下载元数据(几 MB),文件按需从 registry 拉取

8.3 只读容器


# 整个容器文件系统只读
docker run --read-only my-app

# 只允许写 /tmp
docker run --read-only --tmpfs /tmp:size=50m my-app

# 安全加固:防止恶意程序写入文件系统

8.4 Docker 内的文件系统隔离


容器内的 mount namespace:
/         ← overlay mount(merged view)
/proc     ← procfs(进程信息,部分屏蔽)
/sys      ← sysfs(部分只读)
/dev      ← devtmpfs(受限设备访问)
/dev/shm  ← tmpfs(共享内存,默认 64MB)
/etc/resolv.conf  ← bind mount from host
/etc/hostname     ← bind mount from host
/etc/hosts        ← bind mount from host

9. 性能调优

9.1 I/O 性能基准

操作overlay2bind mounttmpfs原生 ext4
顺序读~95%**100%**N/A100%
顺序写~90%**100%**最快100%
随机读(首次)~85%**100%**N/A100%
小文件大量创建~70%**100%**最快100%
大文件首次修改**~50%**100%N/A100%

最大性能瓶颈:Copy-on-Write 首次修改大文件。

9.2 优化建议


# 1. 数据库文件用 Volume,不要放在容器层
docker run -v pgdata:/var/lib/postgresql/data postgres

# 2. 频繁写入的日志用 tmpfs 或 Volume
docker run --tmpfs /var/log:size=200m my-app

# 3. 减少层数(减少 lowerdir 查找深度)
# 合并 RUN 命令

# 4. 使用 .dockerignore 减少 COPY 上下文
echo "node_modules\n.git\n*.log" > .dockerignore

# 5. 监控容器层增长
docker ps -s --format "table {{.Names}}\t{{.Size}}"

10. 总结


Docker 文件系统 = 镜像层(只读)+ 容器层(可写)

镜像层:Dockerfile 每条 RUN/COPY/ADD 产生一层
        内容寻址(SHA256),跨镜像共享
        存储在 /var/lib/docker/overlay2/

容器层:OverlayFS upperdir
        Copy-on-Write:修改时拷贝,删除用 whiteout
        容器删除后丢失

持久化:Volume > Bind Mount > tmpfs
        数据库、重要数据必须用 Volume

性能:  读操作接近原生
        写操作有 CoW 开销(首次修改大文件最慢)
        高 I/O 场景用 Volume 或 tmpfs

参考链接