镜像是怎么搬运的

当我们在本地构建完成一个镜像之后,如何传递给他人呢?这就涉及到镜像搬运的一些知识,搬运镜像就像我们在 GitHub 上搬运代码一样,Docker 也有类似于 git clone 和 git push 的搬运方式。docker push 就和我们使用 git push 一样,将本地的镜像推送到一个称之为 registry 的镜像仓库,这个 registry 镜像仓库就像 GitHub 用来存放公共/私有的镜像,一个中心化的镜像仓库方便大家来进行交流和搬运镜像。docker pull 就像我们使用 git pull 一样,将远程的镜像拉取本地。

docker pull

理解 docker pull 一个镜像的流程最好的办法是查看 OCI registry 规范中的这段文档 pulling-an-image ,在这里我结合大佬的博客简单梳理一下 pull 一个镜像的大致流程。下面这张图是从大佬博客借来的😂

win10 docker pull 拉下来的文件在哪 docker pull digest_客户端

docker pull 就和我们使用 git clone 一样效果,将远程的镜像仓库拉取到本地来给容器运行时使用,结合上图大致的流程如下:

  • 第一步应该是使用~/.docker/config.json 中的 auth 认证信息在 registry 那里进行鉴权授权,拿到一个 token,后面的所有的 HTTP 请求中都要包含着该 token 才能有权限进行操作。

win10 docker pull 拉下来的文件在哪 docker pull digest_客户端_02

  • dockerd 守护进程解析 Docker 客户端参数,由镜像名 + tag 向 registry 请求 Manifest 文件,HTTP 请求为GET /v2/manifests/。registry 中一个镜像有多个 tag 或者多个处理器体系架构的镜像,则根据这个 tag 来返回给客户端与之对应的 manifest 文件;

win10 docker pull 拉下来的文件在哪 docker pull digest_客户端_03

  • docker 守护进程解析这个 Manifest 文件获取镜像的 layer 的信息;
  • dockerd 守护进程并行下载各 layer ,HTTP 请求为GET /<name>v2/blobs/<digest>。
  • dockerd 起一个单独的进程 docker-untar 来 gzip 解压缩已经下载完成的 layer 文件;对于有些比较大的镜像(比如几十 GB 的镜像),往往镜像的 layer 已经下载完成了,但还没有解压完😂。

win10 docker pull 拉下来的文件在哪 docker pull digest_上传_04

  • 验证 image config 中的 RootFS.DiffIDs 是否与下载(解压后)hash 相同;
  • 解析 Manifest 获取镜像 Configuration,验证镜像是否正确。

docker push

push 推送一个镜像到远程的 registry 流程恰好和 pull 拉取镜像到本地的流程相反。我们 pull 一个镜像的时候往往需要先获取包含着镜像 layer 信息的 Manifest 文件,然后根据这个文件中的 layer 信息取 pull 相应的 layer。push 一个镜像,需要先将镜像的各个 layer 推送到 registry ,当所有的镜像 layer 上传完毕之后最后再 push Image Manifest 到 registry。大体的流程如下:

  • 第一步和 pull 一个镜像一样也是进行鉴权授权,拿到一个 token;
  • 向 registry 发送 POST /v2/
    /blobs/uploads/请求,registry 返回一个上传镜像 layer 时要应到的 URL;
  • 客户端通过 HEAD /v2/
    /blobs/请求检查 registry 中是否已经存在镜像的 layer。
  • 客户端通过URL 使用 POST 方法来实时上传 layer 数据,上传镜像 layer 分为 Monolithic Upload (整体上传)和Chunked Upload(分块上传)两种方式。

Monolithic Upload

win10 docker pull 拉下来的文件在哪 docker pull digest_客户端_05

Chunked Upload

win10 docker pull 拉下来的文件在哪 docker pull digest_docker_06

  • 镜像的 layer 上传完成之后,客户端需要向 registry 发送一个 PUT HTTP 请求告知该 layer 已经上传完毕。

win10 docker pull 拉下来的文件在哪 docker pull digest_客户端_07

  • 最后当所有的 layer 上传完之后,客户端再将 manifest 推送上去就完成了。

win10 docker pull 拉下来的文件在哪 docker pull digest_docker_08

Python docker-drag

这是一个很简单粗暴的 Python 脚本,使用 request 库请求 registry API 来从镜像仓库中拉取镜像,并保存为一个 tar 包,拉完之后使用 docker load 加载一下就能食用啦。该 Python 脚本简单到去掉空行和注释不到 200 行,如果把这个脚本源码读一遍的话就能大概知道 docker pull 和 skopeo copy 的一些原理,他们都是去调用 registry 的 API ,所以还是推荐去读一下这个它的源码。

食用起来也很简单直接 python3 docker_pull.py [image name],貌似只能拉取 docker.io 上的镜像。

win10 docker pull 拉下来的文件在哪 docker pull digest_上传_09

skopeo

这个工具是红帽子家的,是 Podman、Skopeo 和 Buildah (简称 PSB )下一代容器新架构中的一员,不过我觉着 Podman 想要取代 docker 和 containerd 容器运行时还有很长的路要走,虽然它符合 OCI 规范,但对于企业来讲,替换的成本并不值得他们去换到 PSB 上去。

其中的 skopeo 这个镜像搬运工具简直是个神器,尤其是在 CI/CD 流水线中搬运两个镜像仓库里的镜像简直爽的不得了。我曾经一个工作任务就是优化我们的 Jenkins 流水线中同步两个镜像仓库的过程,使用了 skopeo 替代 Docker 来同步两个镜像仓库中的镜像,将原来需要 2h 小时缩短到了 25min 😀。

在这里我讲两个最常用的功能。

skopeo copy

使用 skopeo copy 两个 registry 中的镜像时,skopeo 请求两个 registry API 直接 copy original blob 到另一个 registry ,这样免去了像 docker pull –> docker tag –> docker push 那样 pull 镜像对镜像进行解压缩,push 镜像进行压缩。尤其是在搬运一些较大的镜像(几 GB 或者几十 GB 的镜像,比如 nvidia/cuda ),使用 skopeo copy 的加速效果十分明显。

win10 docker pull 拉下来的文件在哪 docker pull digest_上传_10

skopeo inspect

用 skopeo inspect 命令可以很方便地通过 registry 的 API 来查看镜像的 manifest 文件,以前我都是用 curl 命令的,要 token 还要加一堆参数,所以比较麻烦,所以后来就用上了 skopeo inspect😀。

win10 docker pull 拉下来的文件在哪 docker pull digest_docker_11

 

 

镜像是怎么存放的(二)registry 存储

文章的开头我们提到过 OCI 规范中的镜像仓库规范 distribution-spec,该规范就定义着容器镜像如何存储在远端(即 registry)上。我们可以把 registry 看作镜像的仓库,使用该规范可以帮助我们把这些镜像按照约定俗成的格式来存放,目前实现该规范的 registry 就 Docker 家的 registry 使用的多一些。其他的 registry 比如 harbor ,quay.io 使用的也比较多。

registry (/registry/docker/v2)

想要分析镜像是如何存放在 registry 上的,我们在本地使用 docker run 运行 registry 的容器即可,我们仅仅是来分析 registry 中镜像时如何存储的,这种场景下不太适合用 harbor 这种重量级的 registry。

win10 docker pull 拉下来的文件在哪 docker pull digest_上传_12

启动完 registry 容器之后我们给之前已经构建好的镜像重新打上该 registry 的 tag 方便后续 push 到 registry 上。

win10 docker pull 拉下来的文件在哪 docker pull digest_docker_13

当我们在本地启动一个 registry 容器之后,容器内默认的存储位置为 /var/lib/registry ,所以我们在启动的时候加了参数 -v /var/lib/registry:/var/lib/registry 将本机的路径挂载到容器内。进入这里的路径我们使用 tree 命令查看一下这个目录的存储结构。

win10 docker pull 拉下来的文件在哪 docker pull digest_客户端_14

树形的结构看着不太直观,我画了一张层级结构的图:

win10 docker pull 拉下来的文件在哪 docker pull digest_上传_15

blobs 目录

之前我们向 registry 中推送了两个镜像,这两个镜像的 layer 相同但不是用一个镜像,在我们之前 push image 的时候也看到了 d1b85e6186f6: Layer already exists。也就可以证明,虽然两个镜像不同,但它们的 layer 在 registry 中存储的时候可能是相同的。

在 blobs/sha256 目录下一共有 5 个名为 data 的文件,我们可以推测一下最大的那个 [26M] 应该是镜像的 layer ,最小的 [529] 那个应该是 manifest,剩下的那个 [1.4K] 应该就是 image config 文件。

win10 docker pull 拉下来的文件在哪 docker pull digest_docker_16

在 registry 的存储目录下,blobs 目录用来存放镜像的三种文件:layer 的真实数据,镜像的 manifest 文件,镜像的 image config 文件。这些文件都是以 data 为名的文件存放在于该文件 sha256 相对应的目录下。使用以内容寻址的 sha256 散列存储方便索引文件,在 blob digest 目录下有一个名为 data的文件,对于 layer 来讲,这是个 data 文件的格式是 vnd.docker.image.rootfs.diff.tar.gzip ,我们可以使用 tar -xvf 命令将这个 layer 解开。当我们使用 docker pull 命令拉取镜像的时候,也是去下载这个 data 文件,下载完成之后会有一个 docker-untar的进程将这个 data文件解开存放在 /var/lib/docker/overlay2/${digest}/diff 目录下。

win10 docker pull 拉下来的文件在哪 docker pull digest_客户端_17

manifest 文件

其实就是一个普通的 json 文件,记录了一个镜像所包含的 layer 信息,当我们 pull 镜像的时候会使用到这个文件。

win10 docker pull 拉下来的文件在哪 docker pull digest_客户端_18

image config 文件

image config 文件里并没有包含镜像的 tag 信息。

win10 docker pull 拉下来的文件在哪 docker pull digest_客户端_19

_uploads 文件夹

_uploads 文件夹是个临时的文件夹,主要用来存放 push 镜像过程中的文件数据,当镜像 layer 上传完成之后会清空该文件夹。其中的 data 文件上传完毕后会移动到 blobs 目录下,根据该文件的 sha256 值来进行散列存储到相应的目录下。

上传过程中的目录结构:

win10 docker pull 拉下来的文件在哪 docker pull digest_上传_20

  • 上传完镜像之后,_uploads 文件夹就会被清空,正常情况下这个文件夹是空的。但也有异常的时候😂,比如网络抖动导致上传意外中断,该文件夹就可能不为空。

win10 docker pull 拉下来的文件在哪 docker pull digest_docker_21

_manifests 文件夹

_manifests 文件夹是镜像上传完成之后由 registry 来生成的,并且该目录下的文件都是一个名为 link 的文本文件,它的值指向 blobs 目录下与之对应的目录。

win10 docker pull 拉下来的文件在哪 docker pull digest_上传_22

_manifests 文件夹下包含着镜像的 tags 和 revisions 信息,每一个镜像的每一个 tag 对应着于 tag 名相同的目录。镜像的 tag 并不存储在 image config 中,而是以目录的形式来形成镜像的 tag,这一点比较奇妙,这和我们 Dockerfile 中并不包含镜像名和 tag 一个道理?

win10 docker pull 拉下来的文件在哪 docker pull digest_docker_23

镜像的 tag

每个 tag 名目录下面有 current 目录和 index 目录, current 目录下的 link 文件保存了该 tag 目前的 manifest 文件的 sha256 编码,对应在 blobs 中的 sha256 目录下的 data 文件,而 index 目录则列出了该 tag 历史上传的所有版本的 sha256 编码信息。_revisions 目录里存放了该 repository 历史上上传版本的所有 sha256 编码信息。

win10 docker pull 拉下来的文件在哪 docker pull digest_上传_24

当我们 pull 镜像的时候如果不指定镜像的 tag 名,默认就是 latest,registry 会从 HTTP 请求中解析到这个 tag 名,然后根据 tag 名目录下的 link 文件找到该镜像的 manifest 的位置返回给客户端,客户端接着去请求这个 manifest 文件,客户端根据这个 manifest 文件来 pull 相应的镜像 layer。

win10 docker pull 拉下来的文件在哪 docker pull digest_上传_25

最后再补充一点就是,同一个镜像在 registry 中存储的位置是相同的。

  • 通过 Registry API 获得的两个镜像仓库中相同镜像的 manifest 信息完全相同。
  • 两个镜像仓库中相同镜像的 manifest 信息的存储路径和内容完全相同。
  • 两个镜像仓库中相同镜像的 blob 信息的存储路径和内容完全相同。

从上面这三个结论中我们可以推断出 registry 存储目录里并不会存储与该 registry 相关的信息,比我们 push 镜像的时候需要给镜像加上 localhost:5000 这个前缀,这个前缀并不会存储在 registry 存储中。加入我要迁移一个很大的 registry 镜像仓库,镜像的数量在 5k 以上。最便捷的办法就是打包这个 registry 存储目录,将 tar 包 rsync 到另一台机器即可。需要强调一点,打包 registry 存储目录的时候不需要进行压缩,直接 tar -cvf 即可。因为 registry 存储的镜像 layer 已经是个 tar.gzip 格式的文件,再进行压缩的话效果甚微而且还浪费 CPU 时间,得不偿失。

docker-archive

原先我认为 docker save 出来的并不是一个镜像,而是一个 .tar 文件,但我仔细思考后,还是认为它是一个镜像,只不过存在的方式不同而已。与在 Docker 和 registry 中存放的方式不同,使用 docker save 出来的镜像是一个孤立的存在。就像是从蛋糕店里拿出来的蛋糕,外面肯定要有个精美的包装。它放在哪里都可以,使用的时候我们使用 docker load 拆开外包装(.tar)就可。比如我们离线部署 harbor 的时候就是使用官方的镜像 tar 包来进行加载镜像启动容器的。

 

 

镜像是怎么食用的

当我们拿到一个镜像之后,如果用它来启动一个容器呢?这里就涉及到了 OCI 规范中的另一个规范即运行时规范 runtime-spec 。容器运行时通过一个叫 OCI runtime filesytem bundle 的标准格式将 OCI 镜像通过工具转换为 bundle ,然后 OCI 容器引擎能够识别这个 bundle 来运行容器。

filesystem bundle 是个目录,用于给 runtime 提供启动容器必备的配置文件和文件系统。标准的容器 bundle 包含以下内容:

  • config.json: 该文件包含了容器运行的配置信息,该文件必须存在 bundle 的根目录,且名字必须为 config.json
  • 容器的根目录,可以由 config.json 中的 root.path 指定

win10 docker pull 拉下来的文件在哪 docker pull digest_上传_26

docker run

当我们启动一个容器之后使用 tree 命令来分析一下 overlay2 就会发现,较之前的目录,容器启动之后 overlay2 目录下多了一个 merged 的文件夹,该文件夹就是容器内看到的。Docker 通过 overlayfs 联合挂载的技术将镜像的多层 layer 挂载为一层,这层的内容就是容器里所看到的,也就是 merged 文件夹。

win10 docker pull 拉下来的文件在哪 docker pull digest_docker_27

win10 docker pull 拉下来的文件在哪 docker pull digest_客户端_28

这是 Docker 官方文档 Use the OverlayFS storage driver 里的介绍图:

win10 docker pull 拉下来的文件在哪 docker pull digest_上传_29

如果想对 Overlayfs 文件系统有详细的了解,可以参考 Linux 内核官网上的文档。