当前位置: 首页 > news >正文

Docker镜像分层与写时复制原理详解

核心思想概述

可以把 Docker 的镜像和容器想象成一本书和它的读书笔记。

  • 镜像:就像一本印刷好的、不可更改的书。它由多个章节(分层)组成。
  • 容器:就像你拿到这本书后,在上面盖了一张透明的临摹纸。你可以在临摹纸上做笔记、画重点,而不会破坏原书。
  • 联合文件系统:就是让你能同时看到“原书的内容”和“透明纸上的笔记”的技术,形成一个完整的、可读写的视图。
  • 写时复制:当你想修改原书上的某个字时,你不能直接改书,而是先把那个字描到你的透明纸上,然后在透明纸上修改。这个“描”的动作就是复制。

一、原理详解

1. 镜像分层

为什么分层?
为了极致地实现共享、复用和节省空间

  • 每一层都是文件系统的一组变化diff),比如添加、修改或删除文件。每一层都有一个唯一的 ID。
  • 层是只读的。一旦创建,就无法修改。这保证了镜像的不可变性。
  • 基础层(Base Layer)通常是操作系统(如 Alpine, Ubuntu),上面的层是安装的软件、环境变量配置、应用代码等。

举个例子:构建一个 Node.js 应用镜像
假设你的 Dockerfile 如下:

FROM node:14-alpine        # 层 1:基础操作系统 + Node.js 环境
WORKDIR /app               # 层 2:创建并设置工作目录
COPY package*.json ./      # 层 3:复制 package.json 和 package-lock.json
RUN npm install            # 层 4:安装依赖,会生成 node_modules 目录
COPY . .                   # 层 5:复制应用源代码
CMD ["node", "index.js"]   # 层 6:设置容器启动命令

这个镜像最终由 6 个只读层堆叠而成。

分层的好处:

  • 如果你只修改了源代码(第5层),Docker 在构建新镜像时,会复用前面4层的缓存,只重新生成第5层和第6层。这极大地加速了构建过程。
  • 如果你的团队有10个服务都基于 node:14-alpine,那么宿主机上只存储一份 node:14-alpine 的层,所有镜像共享它。
2. 联合文件系统

它是如何把多层合并成一个统一视图的?

UFS 是一种文件系统服务,它能够将多个目录(层)透明地叠加在一起,形成一个单一、统一的文件系统视图。Docker 支持多种 UFS 实现,如 overlay2(目前最常用)、aufs 等。

overlay2 为例,它有几个关键目录:

  • lowerdir: 一个或多个只读层。对应镜像的层。
  • upperdir可写层。对应容器的读写层。
  • merged统一视图。将 lowerdirupperdir 合并后呈现给容器的最终目录。

当容器启动时,Docker 会:

  1. 将镜像的所有只读层作为 lowerdir
  2. 在宿主机上创建一个空目录作为 upperdir
  3. lowerdirupperdir 合并,挂载到 merged 目录。
  4. 容器进程看到的根文件系统就是这个 merged 目录。
3. 写时复制

这是发生在容器运行时的魔法。

当一个容器启动后(即在所有镜像层之上加了一个可写层),文件访问遵循以下规则:

  1. 读文件

    • 容器直接从 merged 视图读取文件。
    • 如果文件在镜像层(lowerdir)中存在,且从未被修改过,容器就直接读取它。多个容器可以安全地共享读取同一个文件。
  2. 第一次修改文件

    • 关键步骤来了! 容器不能直接修改只读的镜像层。
    • CoW 机制被触发:Docker 会将被修改的文件从镜像层(lowerdir)复制到容器的可写层(upperdir
    • 然后,所有后续的修改都作用于可写层中的这个副本
    • 从此,当容器再次读取这个文件时,UFS 会屏蔽镜像层中的旧文件,只呈现可写层中的新文件。
  3. 删除文件

    • 在 UFS 中,删除文件实际上是在可写层(upperdir)中创建一个特殊的“白障”文件,标记该文件在 merged 视图中不可见。

二、实现机制(以 overlay2 为例)

假设我们有一个镜像,包含两层:

  • Lower1: 包含文件 /a.txt, /dir/b.txt
  • Lower2: 包含文件 /a.txt(覆盖了Lower1的版本),新增 /c.txt

我们从这个镜像启动一个容器。

启动前的目录结构:

# 只读层 (lowerdir)
/lower1/a.txt    (内容: "Hello from Lower1")
/lower1/dir/b.txt
/lower2/a.txt    (内容: "Hello from Lower2") # 这个会覆盖lower1的a.txt
/lower2/c.txt# 可写层 (upperdir) - 初始为空
/upper/# 合并视图 (merged) - 呈现给容器
/merged/a.txt    -> 显示 "Hello from Lower2" (来自lower2)
/merged/dir/b.txt -> 显示 lower1 的内容
/merged/c.txt    -> 显示 lower2 的内容

容器内操作模拟:

  1. 读取 /c.txt:

    • 容器进程请求读取 /c.txt
    • UFS 在 merged 视图找到该文件,其数据来自 lower2
    • 无复制发生。
  2. 修改 /a.txt:

    • 容器试图修改 /a.txt
    • CoW 触发!Docker 发现 a.txtlower2 中。
    • Docker 将 a.txtlower2 复制/upper/a.txt
    • 容器对 /upper/a.txt 进行修改。
    • 现在,merged/a.txt 显示的是 /upper/a.txt 的内容。 lower2/a.txt 被“遮盖”了。

此时目录结构:

# 只读层 (lowerdir) - 未变
/lower1/...
/lower2/...# 可写层 (upperdir) - 新增了复制的文件
/upper/a.txt    (内容: "Modified by Container!")# 合并视图 (merged) - 已更新
/merged/a.txt    -> 现在显示 "Modified by Container!" (来自upper)
/merged/dir/b.txt -> 显示 lower1 的内容
/merged/c.txt    -> 显示 lower2 的内容
  1. 删除 /c.txt:
    • 容器删除 /c.txt
    • Docker 在可写层创建一个“白障”文件:/upper/c.txt(一个特殊字符设备文件,标记删除)。
    • 现在,在 merged 视图里,/c.txt 就“消失”了。

三、案例辅助介绍

案例一:高效部署多个Web应用

假设你要部署10个不同的Python Flask应用。

  • 你为每个应用编写一个 Dockerfile,都以 FROM python:3.9-slim 开始。
  • 在宿主机上,python:3.9-slim 的所有层只存储一份
  • 当你运行这10个容器时,它们都共享 python:3.9-slim 的只读层。
  • 每个容器只在需要修改系统文件或写入日志时,才使用自己的可写层。
  • 结果:节省了巨量的磁盘空间,并且启动容器极快,因为不需要复制整个基础镜像。
案例二:快速迭代开发

你在开发一个应用:

  1. 首次构建镜像,所有层都被创建。
  2. 你修改了一行源代码,然后重新 docker build
  3. Docker 发现 COPY . . 这一层之前的缓存都有效,于是跳过 npm install 等耗时操作,只复制新的代码并创建新层。
  4. 新镜像的构建时间可能从几分钟缩短到几秒钟。
  5. 你用新镜像启动容器,CoW 机制确保运行环境是全新的,但基础依赖没有被重复创建。
案例三:理解容器数据隔离
  • 你从同一个镜像 my-app:latest 启动了两个容器:Container-AContainer-B
  • 两个容器都读取镜像中的 /app/config.json它们读的是同一份物理文件。
  • 现在,Container-A 修改了 /app/config.json
    • CoW 触发,/app/config.json 被复制到 Container-A 的可写层并被修改。
    • Container-B 对此一无所知,它读取的仍然是镜像层中的原始文件。
  • 结果:两个容器实现了文件系统的完全隔离,互不影响。

总结

特性原理带来的好处
镜像分层将文件系统的变更记录为一组只读的、可复用的层。高效存储:共享层节省空间。
快速构建:利用缓存加速镜像构建。
快速分发:只需拉取缺失的层。
联合文件系统将多个只读层和一个可写层透明地合并成一个统一的文件系统视图。统一的运行时视图:容器看到的是一个完整的文件系统。
写时复制只有在需要修改文件时,才将文件从只读层复制到可写层。快速启动:无需复制整个镜像。
资源高效:避免不必要的复制,节省I/O和空间。
隔离性:容器间的修改互不干扰。

这三者结合,共同构成了 Docker 高效、轻量的基石。理解它们,对于更好地使用和优化 Docker 至关重要。

http://www.dtcms.com/a/502909.html

相关文章:

  • 药物靶点研究3天入门|Day1:从疾病差异里挖“潜力靶点”,两步锁定真目标
  • WebForms ArrayList详解
  • 达梦数据库性能调优总结
  • [JavaEE初阶]HTTPS-SSL传输过程中的加密
  • 单片机开发中裸机系统有哪些(轮询、时间片、前后台.....)
  • 一次线上MySQL分页事故,搞了半夜...
  • 医院网站建设思路深圳展览设计公司
  • C#WPF关键类
  • 从文件加密到数据料理台:两款主流加密工具(EncFSMP/CyberChef)技术特性解析与开发实战选型
  • 什么是uv和传统的区别
  • FastAPI之 处理HTTP请求
  • 【2025-系统规划与管理师】第十章:云原生系统规划
  • 求一个矩阵中的鞍点
  • 《计算机视觉度量:从特征描述到深度学习》-- 大模型应用开发基础RAG方案介绍
  • 【C++】list的使用及底层逻辑实现
  • 网站开发的整体职业规划购物网站多少钱
  • 【JVM】线上JVM堆内存报警,占用超90%
  • 【JVM系列】-第1章-JVM与Java体系结构
  • 鸿蒙NEXT Wear Engine穿戴侧应用开发完全指南
  • OpenHarmony 与 HarmonyOS 的 NAPI 开发实战对比:自上而下与自下而上的差异解析
  • openHarmony之DSoftBus分布式软总线智能链路切换算法
  • TensorFlow2 Python深度学习 - 循环神经网络(GRU)示例
  • TVM | Relay
  • 使用 Conda 安装 QGIS 也是很好的安装方式
  • 网站套餐到期什么意思抖音seo优化系统招商
  • 怎么看网站pr值衡水市住房和城乡建设局网站
  • 散点拟合圆:Matlab两种方法实现散点拟合圆
  • Kubernetes流量管理:从Ingress到GatewayAPI演进
  • 专做品牌网站西安做网站电话
  • “函数恒大于0”说明函数是可取各不同数值的变数(变量)——“函数是一种对应法则等”是非常明显的错误