惯性聚合 高效追踪和阅读你感兴趣的博客、新闻、科技资讯
阅读原文 在惯性聚合中打开

推荐订阅源

Vercel News
Vercel News
O
OpenAI News
Engineering at Meta
Engineering at Meta
让小产品的独立变现更简单 - ezindie.com
让小产品的独立变现更简单 - ezindie.com
月光博客
月光博客
freeCodeCamp Programming Tutorials: Python, JavaScript, Git & More
WordPress大学
WordPress大学
宝玉的分享
宝玉的分享
GbyAI
GbyAI
T
The Blog of Author Tim Ferriss
Google DeepMind News
Google DeepMind News
B
Blog RSS Feed
CTFtime.org: upcoming CTF events
CTFtime.org: upcoming CTF events
云风的 BLOG
云风的 BLOG
罗磊的独立博客
S
SegmentFault 最新的问题
The Register - Security
The Register - Security
Hugging Face - Blog
Hugging Face - Blog
D
DataBreaches.Net
U
Unit 42
奇客Solidot–传递最新科技情报
奇客Solidot–传递最新科技情报
B
Blog
阮一峰的网络日志
阮一峰的网络日志
P
Proofpoint News Feed
雷峰网
雷峰网
V
Visual Studio Blog
小众软件
小众软件
aimingoo的专栏
aimingoo的专栏
N
Netflix TechBlog - Medium
酷 壳 – CoolShell
酷 壳 – CoolShell
H
Hackread – Cybersecurity News, Data Breaches, AI and More
Y
Y Combinator Blog
博客园 - 【当耐特】
G
Google Developers Blog
L
LangChain Blog
Stack Overflow Blog
Stack Overflow Blog
I
InfoQ
Martin Fowler
Martin Fowler
F
Fortinet All Blogs
钛媒体:引领未来商业与生活新知
钛媒体:引领未来商业与生活新知
The Cloudflare Blog
AI
AI
Google Online Security Blog
Google Online Security Blog
Hacker News - Newest:
Hacker News - Newest: "LLM"
博客园 - Franky
Blog — PlanetScale
Blog — PlanetScale
Webroot Blog
Webroot Blog
PCI Perspectives
PCI Perspectives
爱范儿
爱范儿
cs.AI updates on arXiv.org
cs.AI updates on arXiv.org

文章列表 - 她和她的猫

使用小鹤双拼一个月 折腾 Hugo PaperMod 主题 去长鹿旅游休博园 1Password 自动化登录堡垒机 深入 Hyperf:Inject 注解是如何工作的? 使用 Rust 实现高性能预写日志(Write Ahead Log) 译|使用 Linux 命名空间隔离系统 深入 Hyperf:HTTP 服务是如何处理请求的? Linux 文件权限学习笔记 译|了解 Bitcask:基于日志结构的 KV 存储引擎 还原 SM2 压缩公钥的几种方法 深入 Hyperf:HTTP 服务启动时发生了什么? 一次 Hyperf 注解失效问题分析 将博客迁移到又拍云 译|PHP 7 新的 Hashtable 实现 排查 ES 查询问题:深入了解 json_encode() 函数 浅析 Redlock 分布式锁实现原理 Yar 源码阅读笔记:RPC 服务端 Yar 源码阅读笔记:客户端的并行调用 Yar 源码阅读笔记:客户端的同步调用 Yar 源码阅读笔记:数据传输模块 Yar 源码阅读笔记:消息编码模块 Yar 源码阅读笔记:RPC 通信协议 Yar 源码阅读笔记:开篇 PHP 8 新特性介绍 基于 OpenSSL 实现国密 SM4 加解密 PHP 源码阅读笔记:编译与调试 PHP 什么是惊群问题 大端模式和小端模式 基于 GitHub Actions 定时推送网址到百度站长平台 聊聊五种 I/O 模型 Workerman 源码分析:文件上传 浅入浅出 HTTP 协议 PHP 网络编程:构建 MySQL 蜜罐获取攻击者微信 ID 使用 Workerman 接入 Bilibili 直播弹幕协议 使用 GitHub Actions 自动部署 Hexo 什么是二进制安全 【转载】PHP 程序员进阶之路 优化 Workerman 检查主进程是否存活的逻辑 基于 Redis 实现分布式锁 《PHP 实现 Base64 编码/解码》笔记 PHP 多进程下载必应壁纸 基于 Redis 实现延迟队列 【转载】PHP 实现 Base64 编码/解码 使用 MySQL FIELD() 函数自定义排序规则 Laravel 的 Facades 实现原理 C语言单链表实现约瑟夫环 数据结构学习笔记:顺序栈和链栈(C语言) 使用 QueryList + Redis 下载壁纸 在 Laravel 登录/注册中使用 mews/captcha 扩展包 使用 mews/captcha 扩展包为 Laravel 应用添加图片验证码 给 YOURLS 短网址系统编写插件《批量生成短网址》 给 YOURLS 短网址系统编写插件《Hello World!》 PHP 7.1 使用 json_encode 函数造成浮点类型数据出现精度问题 Laravel 授权策略(Policy)的基本使用 Ubuntu 系统下 WiFi 频繁掉线解决方法 Laravel 应用部署到 Nginx 服务器上的第一个坑 Ubuntu 16.04 LST 安装 Redis 和 Composer PHP 采集逐浪小说章节列表 Nginx 环境 SSL 强制 HTTP 301 跳转到 HTTPS 【工作&生活】第一周 ServiceStack.Redis 与多线程 Count the number of Duplicates C# Base64 加密及解密 ASP.NET MVC 使用动态类型传递数据 Django 使用 order_by() 对数据进行排序操作 ASP.NET + jQuery + AJAX 实现用户登录 ASP.NET 从客户端中检测到有潜在危险的 Request.Form 值 ASP.NET 使用 FormData 实现 AJAX 上传图片 Python 使用 BeautifulSoup 抓取网页 贪心算法之字符串的完美度 一言不和就造轮子之 CookieHelper ASP.NET 上传图片 DBHelper 类 C# 捕获屏幕源码 用 JavaScript 做一些有趣的事情 第一篇文章 好久不见。 如何防止非法调用 PHP 文件 清明小记。 PHP 生成随机字符串 Z-Blog PHP 程序做 301 跳转 PHP 处理数组常用的几个函数 Sublime Text 3 快捷键总结 PHP 使用 file_get_contents() 函数实现采集网页 使用 C# 编写学生信息管理系统 终于到了。 zblog报错: magic_quotes_gpc 和 safe_mode 世事无常。 如何使用 PHP 上传文件 安卓手机编写和运行 PHP 程序的软件 使用 AJAX 刷新、验证 PHP 图片验证码 PHP 生成图片验证码 PHP 之 session 的使用方法 PHP 之 cookie 的使用方法 PHP 实现分页原理详解 Thinkphp 框架中系统常量在 Js 文件中不解析的解决方法
译|理解容器镜像层
她和她的猫 · 2024-07-28 · via 文章列表 - 她和她的猫

原文:Understanding Container Image Layers

容器非常神奇。它让简单的进程可以像虚拟机一样运行。在这背后的优雅设计中,有一套模式和实践使得一切可以正常运作。而设计的核心就是层(Layer)。层是存储和分发容器化文件系统内容的基本方式。这个设计出奇地简单又非常强大。在今天的文章中,我将解释什么是层,以及它们在概念上是如何工作的。

构建分层镜像

当你创建镜像时,通常会使用一个 Dockerfile 来定义容器的内容。Dockerfile 包含了一系列命令,例如:

FROM scratch
RUN echo "hello" > /work/message.txt
COPY content.txt /work/content.txt
RUN rm -rf /work/message.txt

在底层,容器引擎会按顺序执行这些命令,并为每个命令创建一个「层」。但是实际上发生了什么呢?最简单的理解是,将每一层看作是一个包含所有已修改文件的目录。

让我们通过一个示例来详细说明可能的实现方法。

  1. FROM scratch 表示此容器从空内容开始。这是第一层,可以表示为一个空目录 /img/layer1

  2. 创建第二个目录 /img/layer2,并将 /img/layer1 中的所有内容复制到其中。然后从 Dockerfile 执行下一条命令(写入数据到 /work/message.txt 文件)。这是第二层。

  3. 创建第三个目录 /img/layer3,并将 /img/layer2 中的所有内容复制到其中。接下来的一条 Dockerfile 命令需要将宿主机上的 content.txt 文件复制到该目录中。该文件会被写入到 /img/layer3/work/content.txt 文件。这是第三层。

  4. 最后,创建第四个目录 /img/layer4,并将 /img/layer3 中的所有内容复制到其中。接下来的一条命令会删除 /img/layer4/work/message.txt 文件。这是第四层。

为了共享这些层,最简单的方法是为每个目录创建一个 .tar.gz 压缩文件。为了减小总文件大小,任何未修改的、从前一层复制过来的文件都会被移除。为了明确地表示某个文件已被删除,可以使用一个删除标记文件作为占位符。该文件会在原文件名之前加上 .wh. 前缀。举个例子,在第四层,会使用一个名为 .wh.message.txt 的占位符文件代替被删除的文件。当解压某一层时,任何以 .wh. 开头的文件都会被删除。

继续我们的例子,压缩文件将包含:

文件内容
layer1.tar.gz空文件
layer2.tar.gz包含 /work/message.txt
layer3.tar.gz包含 /work/content.txt (因为 message.txt 未修改)
layer4.tar.gz包含 /work/.wh.message.txt (因为 message.txt 已删除)
content.txt 文件未修改,因此它不包含在内。

使用这种方式构建镜像会产生很多叫「layer1」的目录。为了确保名称唯一,压缩文件会使用内容的摘要作为名称。这有点类似于 Git 的工作方式。这样做的好处是可以识别相同的内容,同时在下载过程中可以识别出文件是否损坏。如果内容的摘要与文件名不匹配,文件就会被认为已损坏。

为了使结果可以复现,还需要做一件事情:创建一个解释层顺序的文件(清单)。清单会说明需要下载哪些文件以及解压它们的顺序。这使得可以重新创建目录结构。它还提供了一个重要的好处:层可以在不同的镜像之间重复使用和共享。这最小化了本地存储需求。

实际上,还有很多优化方法。例如,FROM scratch 其实意味着没有父层,所以我们的例子实际上是从 layer2 的内容开始的。引擎还可以检查构建过程中使用的文件,以确定是否需要重新创建某一层。这是层缓存的基础,它最小化了构建或重新创建层的需求。作为额外的优化,当不依赖前一层时,可以使用 COPY --link 来表明该层不需要删除或修改前一层的任何文件。这允许压缩的层文件与其它步骤并行创建。

快照

在容器运行之前,需要一个文件系统进行挂载。本质上,它需要一个包含所有可用文件的目录。压缩的层文件包含了文件系统的组件,但它们不能直接挂载和使用。相反,这些文件需要被解压并组织到一个文件系统中。这个解压后的目录被称为快照(好吧,这是为数不多叫这个名字的东西之一 😄)。

创建快照的过程与镜像构建相反。它先下载清单并生成要下载的层列表。对于每一层,都会创建一个包含该层父目录内容的目录。这个目录被称为活跃快照。接着,差异应用器负责解压压缩的层文件,并将更改应用到活跃快照。由此生成的目录称为已提交快照。最终,已提交快照将作为容器文件系统的挂载目录。

使用我们之前的例子:

  1. 初始层 FROM scratch 表示我们可以从下一层和一个空目录开始。它没有父级。

  2. 创建一个 layer2 的目录,这个空目录现在是一个活跃快照。文件 layer2.tar.gz 被下载,并通过将摘要与文件名进行比较来验证,然后被解压到该目录中。结果是一个包含 /work/message.txt 的目录。这是第一个已提交快照。

  3. 创建一个 layer3 的目录,并将 layer2 的内容复制到其中。这是一个新的活跃快照。文件 layer3.tar.gz 被下载、验证和解压。结果是一个包含 /work/message.txt 和 /work/content.txt 的目录。这个目录现在是第二个已提交快照。

  4. 创建一个 layer4 的目录,并将 layer3 的内容复制到其中。文件 layer4.tar.gz 被下载、验证和解压。差异应用器识别到删除标记文件 /work/.wh.message.txt 并删除 /work/message.txt,只剩下 /work/content.txt。这是第三个已提交快照。

  5. 由于 layer4 是最后一层,它是容器的基础。为了支持读写操作,会创建一个新的快照目录,并将 layer4 的内容复制到其中。该目录被挂载作为容器的文件系统。运行中的容器所做的任何更改都会发生在这个目录中。

如果这些目录中的任何一个已经存在,这表明另一个镜像具有同样的依赖。因此,引擎可以跳过下载和差异应用器,可以直接是使用现有的层。实际上,每个目录和文件都是根据内容的摘要命名的,以便于易于识别。举个例子,一组快照可能会像这样:

/var/path/to/snapshots/blobs
└─ sha256
   ├─ 635944d2044d0a54d01385271ebe96ec18b26791eb8b85790974da36a452cc5c
   ├─ 9de59f6b211510bd59d745a5e49d7aa0db263deedc822005ed388f8d55227fc1
   ├─ fb0624e7b7cb9c912f952dd30833fb2fe1109ffdbcc80d995781f47bd1b4017f
   └─ fb124ec4f943662ecf7aac45a43b096d316f1a6833548ec802226c7b406154e9

或者:

ImageParent
sha256:635944d2044d0a54d01385271ebe96ec18b26791eb8b85790974da36a452cc5c
sha256:9de59f6b211510bd59d745a5e49d7aa0db263deedc822005ed388f8d55227fc1sha256:635944d2044d0a54d01385271ebe96ec18b26791eb8b85790974da36a452cc5c
sha256:fb0624e7b7cb9c912f952dd30833fb2fe1109ffdbcc80d995781f47bd1b4017fsha256:9de59f6b211510bd59d745a5e49d7aa0db263deedc822005ed388f8d55227fc1
sha256:fb124ec4f943662ecf7aac45a43b096d316f1a6833548ec802226c7b406154e9sha256:fb0624e7b7cb9c912f952dd30833fb2fe1109ffdbcc80d995781f47bd1b4017f

真实的快照系统支持插件来改进其中的一些行为。例如,它可以允许对快照进行预组合和解压,从而加快处理速度。这样就可以远程存储快照。它还可以进行特殊优化,例如按需即时下载所需的文件和层。

覆盖层

虽然挂载很简单,但是我们刚介绍的快照方法会产生大量文件变化和重复文件。这会减慢容器首次启动的过程并浪费空间。幸运的是,这是容器化过程中许多可以由文件系统处理的方面之一。Linux 原生支持将目录作为覆盖层进行挂载,为我们实现了大部分过程。

在 Linux 中(以 --privileged--cap-add=SYS_ADMIN 运行的 Linux 容器中):

  1. 创建 tmpfs 挂载(基于内存的文件系统,将用于探索覆盖过程)
mkdir /tmp/overlay
mount -t tmpfs tmpfs /tmp/overlay
  1. 为实验创建一些目录。我们将会使用 lower 作为低层(父层),upper 作为高层(子层),work 作为文件系统的工作目录,以及 merged 用来包含的合并文件系统。
mkdir /tmp/overlay/{lower,upper,work,merged}
  1. 为实验创建一些文件。你可以随意地添加一些文件到 upper
cd /tmp/overlay
echo hello > lower/hello.txt
echo "I'm only here for a moment" > lower/delete-me.txt
echo message > upper/upper-message.txt
  1. 作为 overlay 类型文件系统挂载这些目录。这将创建一个新的文件系统在 merged 目录,该目录包含了 lowerupper 目录结合后的内容。work 目录会用来追踪文件系统的变更。
mount -t overlay overlay -o lowerdir=lower,upperdir=upper,workdir=work merged
  1. 探索文件系统。你会注意到 merged 包含 lowerupper 目录结合后的内容。然后,做一些改动:
rm -rf merged/delete-me.txt
echo "I'm new" > merged/new.txt
echo world >> merged/hello.txt
  1. 正如预期的那样,delete-me.txtmerged 中被移除和一个新文件,在相同目录中创建了 new.txt。如果你使用 tree 命令查看目录结构,会看到一些有趣的事情:
|-- lower
|   |-- delete-me.txt
|   `-- hello.txt
|-- merged
|   |-- hello.txt
|   |-- new.txt
|   `-- upper-message.txt
|-- upper
|   |-- delete-me.txt
|   |-- hello.txt
|   |-- new.txt
|   `-- upper-message.txt

运行 ls -l upper 显示:

total 12
c--------- 2 root root 0, 0 Jan 20 00:17 delete-me.txt
-rw-r--r-- 1 root root   12 Jan 20 00:20 hello.txt
-rw-r--r-- 1 root root    8 Jan 20 00:17 new.txt
-rw-r--r-- 1 root root    8 Jan 20 00:17 upper-message.txt

虽然 merged 显示了我们的更改效果,upper(作为父层)存储了类似于我们手工处理示例的更改。它包含了新文件 new.txt 和修改后的文件 hello.txt。还创建了一个删除标记文件。对于覆盖文件系统,这涉及用一个字符设备(以及设备号 0, 0)替换文件。简而言之,它拥有我们打包目录所需的一切!

你可以看到使用这种方法也可以实现一个快照系统。mount 命令本身可以接受一个用冒号(:)分隔的 lowerdir 路径列表,这些将被合并到一个单一的文件系统中。这是现代容器的本质之一 —— 容器是由本地操作系统特性组成的。

这就是创建一个基本系统的所有内容。实际上,Kubernetes(以及最近发布的 Docker Desktop 4.27.0 )使用的 containerd 运行时采用了一种类似的方法来构建和管理它们的镜像(更多细节可以参考 Content Flow)。希望这能帮助你揭开容器镜像工作方式的神秘面纱!