服务端阅读 06月6日 20:23
Docker 容器文件系统怎么工作?分层和 Copy-on-Write 机制
容器里写了一个文件,它到底存在哪里?删掉容器后文件去哪了?为什么镜像层是只读的但容器可以写入?理解 Docker 文件系统的工作原理,这些问题就都清楚了。从镜像到容器:分层文件系统Docker 镜像不是一个大文件——它是多层只读文件系统的堆叠。每个 Dockerfile 指令产生一层:FROM ubuntu:22.04 # 层 1:操作系统基础(~70MB)RUN apt-get update # 层 2:包索引更新RUN apt-get install -y python3 # 层 3:Python 运行时COPY . /app # 层 4:应用代码CMD ["python3", "/app/main.py"] # 不产生新层(只是元数据)┌────────────────────────┐│ 可写层(容器运行时添加) │ ← 容器启动后才有├────────────────────────┤│ 层 4:应用代码 │ ← COPY 产生├────────────────────────┤│ 层 3:Python 运行时 │ ← RUN 产生├────────────────────────┤│ 层 2:包索引 │ ← RUN 产生├────────────────────────┤│ 层 1:Ubuntu 基础 │ ← FROM 产生└────────────────────────┘关键:每个只读层都是不可变的。删除了上一层的文件,并不会真的删除——只是在新层里标记为"已删除"(whiteout 文件)。Copy-on-Write:容器写入的核心机制容器启动时,Docker 在所有只读层上面加一层可写层。容器内的写操作都走 CoW:修改已有文件1. 容器要修改 /etc/config.yml(存在于镜像层 2)2. Docker 从层 2 把 config.yml 复制到可写层3. 修改发生在可写层的副本上4. 后续读取这个文件时,可写层的版本"遮盖"了镜像层的原始版本新建文件直接写在可写层,不涉及复制。删除文件在可写层创建一个 whiteout 文件(标记为已删除),不真的从只读层删除。overlay2 的实现overlay2 是 Docker 默认的存储驱动,基于 Linux 内核的 OverlayFS:容器看到的是 merged 视图:┌──────────────┐│ merged │ = upperdir + lowerdir 合并后的视图├──────────────┤│ upperdir │ = 可写层(容器修改的文件)├──────────────┤│ lowerdir │ = 镜像的所有只读层└──────────────┘在宿主机上的实际位置# Docker 的存储根目录ls /var/lib/docker/overlay2/# 每个镜像层一个目录/var/lib/docker/overlay2/<layer-id>/ diff/ # 这一层的文件内容 link # 指向层的短链接 lower # 指向下层层的链接 merged/ # 合并视图(容器运行时才出现) work/ # OverlayFS 工作目录查看容器的 overlay2 层# 找到容器的 overlay2 路径docker inspect myapp --format '{{.GraphDriver.Data.MergedDir}}'# /var/lib/docker/overlay2/abc123/merged# 查看可写层的内容ls /var/lib/docker/overlay2/abc123/diff/diff/ 目录里就是容器内所有修改过的文件——和 docker diff 命令看到的一致。为什么容器删除后数据会丢1. docker run → 创建可写层 + 启动容器2. 容器运行中 → 数据写在可写层3. docker rm → 删除可写层 + 容器元数据可写层随容器一起删除,里面的数据也消失了。这就是为什么需要 Volume:services: postgres: image: postgres:16 volumes: - pg_data:/var/lib/postgresql/data # 数据存在 Volume,不在可写层Volume 的数据存在 /var/lib/docker/volumes/pg_data/,容器删除不影响它。镜像层的共享和节省空间多个镜像共享相同的基础层:镜像 A(Node.js 应用) 镜像 B(Python 应用)┌──────────────┐ ┌──────────────┐│ 层 3:App A │ │ 层 3:App B │ ← 不同├──────────────┤ ├──────────────┤│ 层 2:Node.js│ │ 层 2:Python │ ← 不同├──────────────┤ ├──────────────┤│ 层 1:Ubuntu │ │ 层 1:Ubuntu │ ← 共享!只存一份└──────────────┘ └──────────────┘# 查看 Docker 磁盘占用docker system df -v# SHARED SIZE 列显示被多个镜像共享的大小# UNIQUE SIZE 列显示该镜像独有的层大小镜像构建优化:减少层和大小合并 RUN 指令# 差:每条 RUN 一层RUN apt-get updateRUN apt-get install -y python3RUN apt-get install -y pip# 好:合并成一层RUN apt-get update && \ apt-get install -y python3 pip && \ rm -rf /var/lib/apt/lists/*合并的好处:层数减少rm -rf /var/lib/apt/lists/ 在同一层执行——删除操作真的释放了空间,而不是在下一层标记为已删除多阶段构建FROM node:20 AS builderWORKDIR /appCOPY . .RUN npm ci && npm run buildFROM node:20-alpineCOPY --from=builder /app/dist ./distCOPY --from=builder /app/node_modules ./node_modulesCMD ["node", "dist/index.js"]最终镜像只有 dist/ 和 node_modules/——没有源码、没有构建工具,体积缩小 60-80%。.dockerignore 减少构建上下文# .dockerignore.gitnode_modules.env*.mdtest/.dockerignore 排除的文件不会进入构建上下文——既加快构建,又防止敏感文件进镜像。常见文件系统问题镜像越来越大# 查看每层大小docker history myapp:latest# 找出最大的层docker history --format "{{.Size}}\t{{.CreatedBy}}" myapp:latest常见的膨胀原因:apt-get update 的缓存、npm 缓存、临时文件。在同一层里清理掉。容器写入慢可写层写大量小文件时性能差——特别是 Mac/Windows 上 Docker Desktop 的文件共享。解决方案:大量写入用 Volume。inode 耗尽大量小文件会消耗 inode。df -i 查看剩余 inode。overlay2 比 overlay 的 inode 使用更少——这也是推荐 overlay2 的原因之一。