随着 的早期访问版本,Docker Hub 终于开始可视化图像内部结构。这很棒!为什么 Hub 几年前没有这么做?因为它的商业模式。 Docker Scout Docker 在进一步讨论之前,我们先简要回顾一下 Docker 镜像的组成部分。 内容概述 Docker 镜像由层组成 为什么开发人员会关注 Docker 镜像的内部结构 开发人员过去如何查看 Docker 镜像的内部结构 Docker Scout:检查镜像内部的更好方法 是什么让他们花了这么长时间? 两侧平台 凡是过去,皆为序章 Docker 镜像由层组成 当您通过在命令行运行 下载 Docker 映像时,Docker CLI 会在拉取映像时显示每个层的下载进度。如果您曾经下载过 Docker 镜像,您可能已经见过它: docker pull 如果您以前使用过 Docker,您可能已经为自己的映像定义了其中一些层。开发人员在编写 Dockerfile 时隐式定义这些层。例如,此 Dockerfile 中的每一行都会生成一个层: FROM ubuntu:22.04 # install apt dependencies RUN apt-get update RUN apt-get install -y iputils-ping python3 python3-pip # install python dependencies RUN pip3 install numpy # cleanup apt lists RUN rm -rf /var/lib/apt/lists/* CMD ["/bin/bash"] 层是存档的 Linux 目录——它们是文件系统 tarball。 Docker 下载所有镜像层并将每个镜像层解压到一个单独的目录中。当您使用 命令从 Docker 映像启动容器时,Docker 守护程序会将映像层组合在一起以形成容器。 docker run Docker 作为产品的许多增值之处在于它抽象了这些细节,让用户无需考虑分层容器的工作原理即可获得分层容器的好处。但所有抽象都会泄漏,Docker 也不例外——有时,您需要拉开帷幕。 为什么开发人员会关注 Docker 镜像的内部结构 供应链检验 Docker 镜像层包含容器文件系统中存在的每个二进制文件的起源故事。 Docker 镜像中的第一行是“the FROM line”。它定义了 Dockerfile 构建于其上的 Docker 映像(以及映像层)。 通过检查当前 Dockerfile 中的层及其父映像链中的层,开发人员可以确定容器根文件系统中的每个文件来自何处。这是非常有价值的信息。它可以帮助开发人员: 遵守软件许可协议 更顺利通过合规审核 检测并避免安全漏洞 想象一下,单击可视化中的各个层来跟踪 Docker 映像版本之间的文件更改。当自动安全扫描识别出其中一张图像中的漏洞时,想象一下使用层检查工具来识别漏洞是如何引入的。 图像尺寸优化 过大的图像尺寸可能会让公司损失大量资金。许多 会为每个拉取请求拉取 Docker 镜像,因此冗长的 Docker 镜像下载会减慢管道速度,降低开发人员的效率并浪费 CPU 时间。由于组织通常按小时支付基础设施成本,因此浪费的每一个小时都是不必要的费用。 CI/CD 管道 除了浪费计算资源之外,臃肿的 Docker 镜像还可能导致网络传输成本过高。如果组织跨 AWS 区域下载 Docker 映像,或者从 AWS 下载到开放互联网,则 Docker 映像膨胀会直接转化为基础设施支出的浪费。这加起来很快。 很容易意外地在 Docker 镜像层中引入膨胀。前面描述的 Dockerfile 包含一个经典示例 - 第 4 行中的层在磁盘上保存了 40MB 的文件,这些文件稍后会被第 11 行中的层删除。由于 Docker 镜像的工作方式,该数据仍然是镜像的一部分,添加40MB 不必要的图像大小。 这是一个简单的示例——事实上,它直接来自 Docker 的 。在更复杂的 Dockerfile 中,这个错误可能更难发现。 文档 开发人员过去如何查看 Docker 镜像的内部结构 Docker 镜像层可能很难使用命令行进行交互,但在最近发布 Docker Scout 之前,命令行是您找到最先进技术的地方。这是两种基本方法。 启动一个容器并查看 这是简单的 DIY Unix 方法。您所需要的只是一台运行 Docker 守护进程的主机。这是一个简单的方法: 运行 从映像启动容器。 docker create 使用 查找新容器的层目录。 docker inspect 将这些层目录与 Docker 映像中的行相关联。 在命令行中 进入这些目录,并在头脑中推理出这些层。 cd 这就麻烦了。假设我们正在尝试追踪 Docker 镜像中的一些镜像膨胀情况,该镜像我们已经使用了几个月,但最近其大小显着增长。 首先,我们从图像创建一个容器: where-the@roadmap-ends ~ $ docker create --name example where-the-roadmap-ends 339b8905b681a1d4f7c56f564f6b1f5e63bb6602b62ba5a15d368ed785f44f55 然后, 告诉我们下载的图像的层目录最终在我们的文件系统上的位置: docker inspect where-the@roadmap-ends ~ $ docker inspect example | grep GraphDriver -A7 "GraphDriver": { "Data": { "LowerDir": "/var/lib/docker/overlay2/1c18bd289d9c3f9f0850e301bf86955395c312de3a64a70e0d0e6a5bed337d47-init/diff:/var/lib/docker/overlay2/wbugwbg23oefsf678r7anbn4f/diff:/var/lib/docker/overlay2/j0dekt7y8xgix11n0lturmf8t/diff:/var/lib/docker/overlay2/zd57mz6l4zrsjk9snc2crphfq/diff:/var/lib/docker/overlay2/83za1pmv9xri44tddzyju0usm/diff:/var/lib/docker/overlay2/8c639b22627e0ad91944a70822b442e5bff848968263a37715a293a15483c170/diff", "MergedDir": "/var/lib/docker/overlay2/1c18bd289d9c3f9f0850e301bf86955395c312de3a64a70e0d0e6a5bed337d47/merged", "UpperDir": "/var/lib/docker/overlay2/1c18bd289d9c3f9f0850e301bf86955395c312de3a64a70e0d0e6a5bed337d47/diff", "WorkDir": "/var/lib/docker/overlay2/1c18bd289d9c3f9f0850e301bf86955395c312de3a64a70e0d0e6a5bed337d47/work" }, "Name": "overlay2" 我们要在本次调查中查看的层是“LowerDir”目录列表。其他目录不是 Docker 镜像本身的一部分——我们可以忽略它们。 因此,我们在命令行解析出“LowerDir”目录列表: where-the@roadmap-ends ~ $ docker inspect example | grep GraphDriver -A7 | grep LowerDir | awk '{print $2}' | sed 's|"||g' | sed 's|,||g' | sed 's|:|\n|g' /var/lib/docker/overlay2/1c18bd289d9c3f9f0850e301bf86955395c312de3a64a70e0d0e6a5bed337d47-init/diff /var/lib/docker/overlay2/wbugwbg23oefsf678r7anbn4f/diff /var/lib/docker/overlay2/j0dekt7y8xgix11n0lturmf8t/diff /var/lib/docker/overlay2/zd57mz6l4zrsjk9snc2crphfq/diff /var/lib/docker/overlay2/83za1pmv9xri44tddzyju0usm/diff /var/lib/docker/overlay2/8c639b22627e0ad91944a70822b442e5bff848968263a37715a293a15483c170/diff 这些是图像中的层列表,按顺序排列,最低层在前。现在我们需要手动将这些层目录与生成它们的 Dockerfile 行关联起来。 不幸的是,Docker 没有为我们提供直接从 Docker 镜像中提取这些行的方法——这是我们使用 所能得到的最好的方法: docker history where-the@roadmap-ends ~ $ docker history where-the-roadmap-ends IMAGE CREATED CREATED BY SIZE COMMENT 6bbac081b2a7 2 hours ago CMD ["/bin/bash"] 0B buildkit.dockerfile.v0 <missing> 2 hours ago RUN /bin/sh -c rm -rf /var/lib/apt/lists/* #… 0B buildkit.dockerfile.v0 <missing> 2 hours ago RUN /bin/sh -c pip3 install numpy # buildkit 70MB buildkit.dockerfile.v0 <missing> 2 hours ago RUN /bin/sh -c apt-get install -y iputils-pi… 343MB buildkit.dockerfile.v0 <missing> 2 hours ago RUN /bin/sh -c apt-get update # buildkit 40.1MB buildkit.dockerfile.v0 <missing> 8 months ago /bin/sh -c #(nop) CMD ["bash"] 0B <missing> 8 months ago /bin/sh -c #(nop) ADD file:550e7da19f5f7cef5… 69.2MB 使用此输出,我们可以识别哪些层具有目录以及哪个 Dockerfile 命令创建了该层(如 CREATED BY 列所示)。 命令以与 列出层目录相同的顺序输出容器中的层。知道了这一点,我们可以手动将两个输出合并在一起,看看哪些层比其他层大、哪个 Dockerfile 命令创建了它们,以及哪个目录包含每个层。 docker history docker inspect 这是 A 层的内容,它执行 : apt-get update where-the@roadmap-ends ~ $ du -hs /var/lib/docker/overlay2/83za1pmv9xri44tddzyju0usm/diff/var/lib/apt/lists 38.2M /var/lib/docker/overlay2/83za1pmv9xri44tddzyju0usm/diff/var/lib/apt/lists 与B层的内容相比,删除A层留下的文件: where-the@roadmap-ends ~ $ du -hs /var/lib/docker/overlay2/wbugwbg23oefsf678r7anbn4f/diff/var/lib/apt/lists 4.0K /var/lib/docker/overlay2/wbugwbg23oefsf678r7anbn4f/diff/var/lib/apt/lists 目录在两层中都存在,但在 B 层中,该目录几乎不使用任何空间。 /var/lib/apt/lists 这是因为 B 层的目录包含“whiteout 文件”,Docker 使用这些文件来标记要从最终容器文件系统中排除的文件。 因此,尽管文件在 B 层中被“删除”,但它们仍然存在于 A 层中,从而增加了图像的整体大小 — 38.2 MB 的不必要的膨胀。 现在,这不是很容易吗? 😉 使用潜水 手动方法非常复杂且难以操作,以至于开源社区专门为此任务创建了一个工具 - 称为 。 Dive 是一个 CLI 工具,它以图像作为输入,解析其文件系统,并在终端中呈现基于文本的交互式 UI。它为您关联图像图层,让您更轻松地检查图层目录。 dive 当针对上面 Dockerfile 中的 Docker 映像运行时,它看起来像这样: ┃ ● Layers ┣━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ │ Aggregated Layer Contents ├─────────────────────────────────────────────────────────────────────────────── Cmp Size Command └── var 69 MB FROM acee8cf20a197c9 └── lib 40 MB RUN /bin/sh -c apt-get update # buildkit └── apt 343 MB RUN /bin/sh -c apt-get install -y iputils-ping python3 python3-pip # buildkit └── lists 70 MB RUN /bin/sh -c pip3 install numpy # buildkit ├── auxfiles 0 B RUN /bin/sh -c rm -rf /var/lib/apt/lists/* # buildkit ├── lock ├── partial │ Layer Details ├─────────────────────────────────────────────────────────────────────────────────────────── ├── ports.ubuntu.com_ubuntu-ports_dists_jammy-backports_InRelease ├── ports.ubuntu.com_ubuntu-ports_dists_jammy-backports_main_binary-arm64_Packages.lz4 Tags: (unavailable) ├── ports.ubuntu.com_ubuntu-ports_dists_jammy-backports_universe_binary-arm64_Packages.lz4 Id: 2bc27a99fd5750414948211814da00079804292360f8e2d7843589b9e7eb5eee ├── ports.ubuntu.com_ubuntu-ports_dists_jammy-security_InRelease Digest: sha256:6e6fb36e04f3abf90c7c87d52629fe154db4ea9aceab539a794d29bbc0919100 ├── ports.ubuntu.com_ubuntu-ports_dists_jammy-security_main_binary-arm64_Packages.lz4 Command: ├── ports.ubuntu.com_ubuntu-ports_dists_jammy-security_multiverse_binary-arm64_Packages.lz4 RUN /bin/sh -c apt-get update # buildkit ├── ports.ubuntu.com_ubuntu-ports_dists_jammy-security_restricted_binary-arm64_Packages.lz4 ├── ports.ubuntu.com_ubuntu-ports_dists_jammy-security_universe_binary-arm64_Packages.lz4 │ Image Details ├─────────────────────────────────────────────────────────────────────────────────────────── ├── ports.ubuntu.com_ubuntu-ports_dists_jammy-updates_InRelease ├── ports.ubuntu.com_ubuntu-ports_dists_jammy-updates_main_binary-arm64_Packages.lz4 Image name: where-the-roadmap-ends ├── ports.ubuntu.com_ubuntu-ports_dists_jammy-updates_multiverse_binary-arm64_Packages.lz4 Total Image size: 522 MB ├── ports.ubuntu.com_ubuntu-ports_dists_jammy-updates_restricted_binary-arm64_Packages.lz4 Potential wasted space: 62 MB ├── ports.ubuntu.com_ubuntu-ports_dists_jammy-updates_universe_binary-arm64_Packages.lz4 Image efficiency score: 90 % ├── ports.ubuntu.com_ubuntu-ports_dists_jammy_InRelease ├── ports.ubuntu.com_ubuntu-ports_dists_jammy_main_binary-arm64_Packages.lz4 Count Total Space Path ├── ports.ubuntu.com_ubuntu-ports_dists_jammy_multiverse_binary-arm64_Packages.lz4 2 28 MB /var/lib/apt/lists/ports.ubuntu.com_ubuntu-ports_dists_jammy_universe_binary-arm64_Pack ├── ports.ubuntu.com_ubuntu-ports_dists_jammy_restricted_binary-arm64_Packages.lz4 2 7.3 MB /usr/bin/perl └── ports.ubuntu.com_ubuntu-ports_dists_jammy_universe_binary-arm64_Packages.lz4 2 4.4 MB /usr/lib/aarch64-linux-gnu/libstdc++.so.6.0.30 2 2.9 MB /var/lib/apt/lists/ports.ubuntu.com_ubuntu-ports_dists_jammy_main_binary-arm64_Packages 2 2.0 MB /var/lib/apt/lists/ports.ubuntu.com_ubuntu-ports_dists_jammy-updates_main_binary-arm64_ 2 1.7 MB /var/lib/apt/lists/ports.ubuntu.com_ubuntu-ports_dists_jammy-updates_universe_binary-ar Dive 是一个很棒的工具,我很高兴它的存在,但有时它也有不足之处。基于文本的界面并不是最容易使用的——有时 Grafana 比 更好。 top 此外,大图像可能会淹没 Dive 的功能。当检查大图像时,Dive 会消耗大量内存——有时内核会在输出任何数据之前终止 Dive 进程。 Docker Scout:检查镜像内部的更好方法 从开发人员的角度来看,期望 Docker Hub 中存在 Docker 镜像层可视化始终是有意义的。毕竟,我们的 Docker 镜像数据已经存在于 Docker Hub 的后端中。 我经常想象在浏览器中检查 Docker 镜像的内部结构,使用如下所示的 UI: 通过 Docker Scout,Docker 作为一家公司似乎正在朝着这个方向前进。今天,如果我导航到 Docker Hub 中最新的 Postgres 镜像,我会看到以下内容: 作为一名开发人员,这是令人兴奋的。这个新的 UI 让我可以直观地浏览图像层详细信息,突出显示漏洞和图像大小问题,就像我想要的那样。 是什么让他们花了这么长时间? Docker Hub 刚推出时,有两类用户: 下载 Docker 镜像的 开发人员 上传 Docker 镜像的 软件发行商 Docker 必须以不同的方式对待这两类用户。 Docker 在 2013 年首次亮相后,开发人员立即想要使用他们的产品。 Docker 容器标准化了每个人软件的打包和部署。容器在软件开发人员社区中迅速流行起来。 然而,软件发行商则更加犹豫。为什么他们应该将软件上传到一个其核心功能是消除软件产品之间差异的平台? 为了使 Docker Hub 取得成功,Docker 必须赢得他们的支持。因此,Docker Hub 的设计和功能集并不是专注于吸引开发人员,而是迎合了软件发行商的需求。 对于出版商来说,Docker Hub 是一个营销网站。它为他们提供了一个宣传产品的地方。它不允许开发人员看到 Docker 镜像的内部细节,就像汽车经销商不允许你拆卸他们的发动机组一样。内部工程细节没有展示,而是隐藏起来,因此购物者将注意力集中在产品本身,而不是它们的制造方式。 Docker Hub 缺乏面向开发人员的图像大小优化和供应链自省功能,因为 Docker 已经赢得了没有这些功能的开发人员的青睐。事实上,这些功能往往会让软件发行商看起来很糟糕——而他们是 Docker Hub 仍需要赢得成功的用户。 这种为软件发行商和开发人员提供服务的动态使 Docker Hub 成为一个双向平台。 两侧平台 双面平台是指拥有两类不同用户的企业,需要他们都参与平台才能正常运行。通常,这是使用平台的动机将用户分为不同的组。 虽然两组用户可能不会直接进行交易,但双边平台就像市场一样,除非供应商出现出售且购物者出现购买,否则它们不会创造价值。 在科技行业,双面平台无处不在。如果成功,这些商业模式往往会产生网络效应,推动企业持续增长和盈利。在某一点之后,平台的规模和在空间中的既定地位会吸引新用户进入该平台。 一旦您知道自己在寻找什么,双面平台就很容易被发现。以下是三个例子。 领英 LinkedIn 上有两种类型的用户:员工和招聘经理。两个群体参与的原因不同——员工想要工作,而招聘经理想要雇用员工。 在另一组开始参与之前,任何一组都无法从该网站获得他们想要的东西。一旦每个团体中有足够多的人注册,它就成为每个团体新成员的默认地点,并且该网站的增长会持续下去,直到被 以 260 亿美元收购。 微软 YouTube 在 上,有内容创作者,也有观众。观众来到该网站观看视频——内容创作者在网站上发布内容是为了追求良好的氛围、名誉和财富。 YouTube YouTube 被誉为内容创作者可以取得成功的地方,之后,越来越多的内容创作者出现,随着他们创作的内容越来越多,前来访问的观众也越来越多。一旦平台发展到超过一定规模,内容创作者和观众别无选择,只能继续使用它——每个群体都需要对方,他们只能通过 YouTube 找到彼此。 码头工人中心 为了使 Docker Hub 具有相关性,它需要软件发行商上传图像,需要开发人员下载图像。一旦有足够多的发布者将镜像推送到 Docker Hub,它将成为开发人员获取镜像的默认位置。随着该平台的不断发展,Docker Hub 将巩固其作为统治所有平台的注册中心的主导地位。 凡是过去,皆为序章 至少,那是计划。事实上,Docker(该公司)衰落了,Docker Hub 从未成为“那个”。在构建双向平台时,初创公司只有一种产品,但他们必须两次找到产品与市场的契合点。对于 Docker 来说,这个负担实在是太难以承受了——最终,它 。 把 Docker 劈成了两半 现在,在 后,Docker 进行了自我改造,将其订阅服务专门针对开发人员及其雇主。 Docker Hub 不再是一个双向平台,它是简单的 SaaS——客户每月支付费用以换取推送和拉取图像的交换。 出售其企业业务 这改变了微积分。 Docker 不再需要取悦“发布者”用户角色——他们都在关注开发者。对于 Docker Hub 来说,这为 Docker Scout 的镜像层检查功能铺平了道路。对于我们这些从远处观看的人来说,它展示了初创公司的商业模式与其产品之间微妙的、基本的耦合。 也发布 在这里。