docker 镜像存储分析

镜像在远端镜像仓库和本地的存储方式不同。在镜像仓库中,按层压缩后存储,因为需要考虑拉取、推送的效率;在本地是解压后存储的,因为需要考虑快速起容器,通过联合挂载的方式构造rootfs(联合文件系统UnionFS)。下面对两种存储方式分别进行介绍。

几个名词解释

首先对后面要用到的几个名词做个简单解释。

  • digest: 摘要信息,通常是文件的SHA256哈希值。

  • tag: 镜像的标签,通常用来表示镜像的一个版本。

  • Image ID: 镜像配置文件(config文件)的digest值。docker images时显示的镜像ID,本地保存在/var/lib/docker/image/overlay2/repositories.json文件中,同一个镜像可以打多个不同的tag,但image ID都相同。

  • config文件:镜像的配置文件,保存的是镜像的详细描述信息,包括根文件系统,容器运行时使用的执行参数及镜像的元数据。还有容器运行需要的相关信息,如arch、OS等。保存在/var/lib/docker/image/overlay2/imagedb/content/sha256/\${image_ID}中。sha256sum /var/lib/docker/image/overlay2/imagedb/content/sha256/${image_ID}结果就是image ID的值。

  • layer: 镜像的实际层,保存的是该层和上一层的差异部分,包括添加、更改和删除。保存在/var/lib/docker/image/overlay2/layerdb/sha256/\${diff_ID}目录下。

  • manifest:镜像清单文件,保存的是layer和config文件的digest。该文件保存在远端仓库中。

  • blob:镜像在远程仓库的基本存储单元,包含layer,config,manifest等数据。

  • 镜像索引(image index):指向一组支持不同架构的镜像。
    几个概念之间的关系示意图如下所示:


    image.png

镜像在远端仓库存储

在本地起一个registry服务,然后推送三个镜像到镜像仓库。可以得到registry中的文件内容如下所示。registry中包含三个镜像: xxx/library/debian:latest,xxx/repo:tag和xxx/busybox:v1

└── registry
    └── v2
        ├── blobs
        │   └── sha256
        │       ├── 0d
        │       │   └── 0d96da54f60b86a4d869d44b44cfca69d71c4776b81d361bc057d6666ec0d878
        │       │       └── data
        │       ├── 34
        │       │   └── 34efe68cca33507682b1673c851700ec66839ecf94d19b928176e20d20e02413
        │       │       └── data
        │       ...
        └── repositories
            ├── busybox
            │   ├── _layers
            │   │   └── sha256
            │   │       ├── 7138284460ffa3bb6ee087344f5b051468b3f8697e2d1427bac1a20c8d168b14
            │   │       │   └── link
            │   │       └── e685c5c858e36338a47c627763b50dfe6035b547f1f75f0d39753db71e319016
            │   │           └── link
            │   ├── _manifests
            │   │   ├── revisions
            │   │   │   └── sha256
            │   │   │       └── 34efe68cca33507682b1673c851700ec66839ecf94d19b928176e20d20e02413
            │   │   │           └── link
            │   │   └── tags
            │   │       └── v1
            │   │           ├── current
            │   │           │   └── link
            │   │           └── index
            │   │               └── sha256
            │   │                   └── 34efe68cca33507682b1673c851700ec66839ecf94d19b928176e20d20e02413
            │   │                       └── link
            │   └── _uploads
            ├── library
            │   └── debian
            │       ├── _layers
            │       │   └── sha256
            │       │       ├── 41c22baa66ecf728c1ea0c5405ebe72c5b2606ef66b4565a209e23e1ab05fe80
            │       │       │   └── link
            │       │       ├── 67283bbdd4a0dd32f555b4279fd546b3c69251342f0c6715b075cc72049d28a1
            │       │       │   └── link
            │       │       ...
            │       ├── _manifests
            │       │   ├── revisions
            │       │   │   └── sha256
            │       │   │       └── 57c1e4ff150e2782a25c8cebb80b574f81f06b74944caf972f27e21b76074194
            │       │   │           └── link
            │       │   └── tags
            │       │       └── latest
            │       │           ├── current
            │       │           │   └── link
            │       │           └── index
            │       │               └── sha256
            │       │                   └── 57c1e4ff150e2782a25c8cebb80b574f81f06b74944caf972f27e21b76074194
            │       │                       └── link
            │       └── _uploads
            └── repo
                ├── _layers
                │   └── sha256
                │       ├── 0d96da54f60b86a4d869d44b44cfca69d71c4776b81d361bc057d6666ec0d878
                │       │   └── link
                │       ├── 3790aef225b922bc97aaba099fe762f7b115aec55a0083824b548a6a1e610719
                │       │   └── link
                │       ...
                ├── _manifests
                │   ├── revisions
                │   │   └── sha256
                │   │       └── 36cb5b157911061fb610d8884dc09e0b0300a767a350563cbfd88b4b85324ce4
                │   │           └── link
                │   └── tags
                │       └── tag
                │           ├── current
                │           │   └── link
                │           └── index
                │               └── sha256
                │                   └── 36cb5b157911061fb610d8884dc09e0b0300a767a350563cbfd88b4b85324ce4
                │                       └── link
                └── _uploads

将上面的结构稍加整理,可以得到如下图所示结构

image.png

registry有两个目录,分别为blobs和repositories,其中blobs保存的是镜像的manifest文件、config文件和layer文件内容,文件名字均为data,每个文件可能是manifest、config、layer中的一种。repositories保存的是镜像的repo、tag、layer摘要等信息。其中的_manifests文件夹下包含着镜像的 tags 和 revisions 信息,每一个镜像的每一个 tag 对应 tag 名相同的目录。每个 tag名目录下面有 current 目录和 index 目录, current 目录下的 link 文件保存了该 tag 目前的 manifest 文件的 sha256 编码,对应在 blobs 中的 sha256 目录下的 data 文件,而 index 目录则列出了该 tag 历史上传的所有版本的 sha256 编码信息。_revisions 目录里存放了该 repository 历史上上传版本的所有 sha256 编码信息。

下面通过例子来说明下几个文件的关系。

  • manifest文件
    查看busybox:v1文件的manifest信息
cat docker/registry/docker/registry/v2/repositories/busybox/_manifests/tags/v1/current/link
sha256:34efe68cca33507682b1673c851700ec66839ecf94d19b928176e20d20e02413

可以看到link中返回的是一个digest值。
根据该digest值,我们到blobs中查看其中保存的数据:

cat  docker/registry/docker/registry/v2/blobs/sha256/34/34efe68cca33507682b1673c851700ec66839ecf94d19b928176e20d20e02413/data
{
   "schemaVersion": 2,
   "mediaType": "application/vnd.docker.distribution.manifest.v2+json",
   "config": {
      "mediaType": "application/vnd.docker.container.image.v1+json",
      "size": 1456,
      "digest": "sha256:7138284460ffa3bb6ee087344f5b051468b3f8697e2d1427bac1a20c8d168b14"
   },
   "layers": [
      {
         "mediaType": "application/vnd.docker.image.rootfs.diff.tar.gzip",
         "size": 772792,
         "digest": "sha256:e685c5c858e36338a47c627763b50dfe6035b547f1f75f0d39753db71e319016"
      }
   ]

可以看出来,这是一个manifest文件,里面包含了config和layer的digest值,且config文件的digest值就是执行docker images看到的镜像的image ID。下面分别查看两个文件的内容。
首先是config文件:

 cat docker/registry/docker/registry/v2/blobs/sha256/71/7138284460ffa3bb6ee087344f5b051468b3f8697e2d1427bac1a20c8d168b14/d
{
  "architecture": "amd64",
  "config": {
    "Hostname": "",
    "Domainname": "",
    "User": "",
    "AttachStdin": false,
    "AttachStdout": false,
    "AttachStderr": false,
    "Tty": false,
    "OpenStdin": false,
    "StdinOnce": false,
    "Env": [
      "PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin"
    ],
    "Cmd": [
      "sh"
    ],
    "Image": "sha256:d39a5c18a94ca076b3f9fad5b104d1b5555697280b61cbabd1eec6d89908b1b6",
    "Volumes": null,
    "WorkingDir": "",
    "Entrypoint": null,
    "OnBuild": null,
    "Labels": null
  },
  "container": "8afe392526b6fa99a3498001c95812b187123968e5a14802c9e837e1cd06d02b",
  "container_config": {
    "Hostname": "8afe392526b6",
    "Domainname": "",
    "User": "",
    "AttachStdin": false,
    "AttachStdout": false,
    "AttachStderr": false,
    "Tty": false,
    "OpenStdin": false,
    "StdinOnce": false,
    "Env": [
      "PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin"
    ],
    "Cmd": [
      "/bin/sh",
      "-c",
      "#(nop) ",
      "CMD [\"sh\"]"
    ],
    "Image": "sha256:d39a5c18a94ca076b3f9fad5b104d1b5555697280b61cbabd1eec6d89908b1b6",
    "Volumes": null,
    "WorkingDir": "",
    "Entrypoint": null,
    "OnBuild": null,
    "Labels": {}
  },
  "created": "2021-11-11T19:19:37.862545075Z",
  "docker_version": "20.10.7",
  "history": [
    {
      "created": "2021-11-11T19:19:37.680254655Z",
      "created_by": "/bin/sh -c #(nop) ADD file:10aef872700b72808327a02dd1b22ca1ac9d3e1058cb35cfec1fcfcd1b465ab4 in / "
    },
    {
      "created": "2021-11-11T19:19:37.862545075Z",
      "created_by": "/bin/sh -c #(nop)  CMD [\"sh\"]",
      "empty_layer": true
    }
  ],
  "os": "linux",
  "rootfs": {
    "type": "layers",
    "diff_ids": [
   "sha256:d94c78be13527d00673093f9677f9b43d7e3a02ae6fa0ec74d3d98243b5b40e4"
    ]
  }
}

可以看出,其中包含了容器的镜像的架构、默认配置,启动的容器,镜像构建命令,操作系统、diff_ids等信息。其中的diff_ids是镜像每一层解压后的digest值,在拉取镜像时,可以用来校验本地是否已经存在该层。镜像层本地保存路径为/var/lib/docker/image/overlay2/layerdb/sha256/\${diff_id}
最后看下镜像的layer文件:

 file docker/registry/docker/registry/v2/blobs/sha256/e6/e685c5c858e36338a47c627763b50dfe6035b547f1f75f0d39753db71e319016/data
docker/registry/docker/registry/v2/blobs/sha256/e6/e685c5c858e36338a47c627763b50dfe6035b547f1f75f0d39753db71e319016/data: gzip compressed data

该文件是一个gzip的压缩包,从前面的manifest文件中可以知道文件类型为:application/vnd.docker.image.rootfs.diff.tar.gzip。

镜像下载流程

最后,从上面的分析中,我们可以推测到镜像拉取的大致流程为:

  1. docker client发送镜像的tag到registry。
  2. registry根据镜像tag,得到镜像的manifest文件,返回给docker client。
  3. docker client拿到manifest文件后,根据其中的config的digest,也就是image ID,检查下镜像在本地是否存在。
  4. 如果镜像不存在,则下载config文件,并根据config文件中的diff_ids得到镜像每一层解压后的digest。
  5. 然后根据每层解压后的digest文件,检查本地是否存在,如果不存在,则通过manifest文件中的layer的digest下载该层并解压,然后校验解压后digest是否匹配。
  6. 下载完所有层后,镜像就下载完毕。

镜像推送流程

镜像下载流程和推送过程正好相反。个人总结,仅供参考。

  1. docker client首先发送HEAD请求到registry,检查registry中是否存在manifest,若存在则上传tag,镜像上传完毕;若不存在,则进入下一步。
  2. 逐层上传镜像层。docker client发送HEAD请求(某一层的digest)到registry,检查该层是否已经在registry中存在,若存在,则继续步骤2检查下一层,若不存在,则进入下一步。
  3. docker client发送PATCH请求到registry,上传该层数据。上传完毕后,继续步骤2,直到所有镜像层传输完毕后,进入下一步。
  4. docker client发送PUT请求到registry,将镜像manifest发送给registry,并上传镜像tag。至此,镜像上传完毕。

本地镜像存储

我使用的存储驱动时overlay2,镜像在本地存储目录为/var/lib/docker/image/overlay2,查看下面的文件结构,得到结果如下:

tree -L 4 /var/lib/docker/image/overlay2/
/var/lib/docker/image/overlay2/
├── distribution
│   ├── diffid-by-digest
│   │   └── sha256
│   │       ├── 0240c3db9dedbfe40ec02d465375aa5b059bf8ac78dc249d1f1c91b9429fce44
│   │       ├── 41c22baa66ecf728c1ea0c5405ebe72c5b2606ef66b4565a209e23e1ab05fe80
│   │       ├── 4cdd12619cf5ed0ae43b41cd51f26fbdbd1f5ded860e4188822ec29158218263
│   │       ├── ...
│   └── v2metadata-by-diffid
│       └── sha256
│           ├── 00188c48b6d80656e2344142a77bccf6927123e7492baf43df68e280b2baf7f2
│           ├── 04fefa2a1a8fefaafde3b966f11d547e3bbaa2bb36bf90c58e33c1d305052fa9
│           ├── ...
├── imagedb
│   ├── content
│   │   └── sha256
│   │       ├── 7138284460ffa3bb6ee087344f5b051468b3f8697e2d1427bac1a20c8d168b14
│   │       ├── ...
│   └── metadata
│       └── sha256
│           ├── b8604a3fe8543c9e6afc29550de05b36cd162a97aa9b2833864ea8a5be11f3e2
│           └── dabbfbe0c57b6e5cd4bc089818d3f664acfad496dc741c9a501e72d15e803b34
├── layerdb
│   ├── mounts
│   │   ├── 2d534be7517fb3efd9c14248eefdb4781924095fe304f5aa0c848f2e76c6bf08
│   │   │   ├── init-id
│   │   │   ├── mount-id
│   │   │   └── parent
│   │   ├──...
│   ├── sha256
│   │   ├── 0e16a5a61bcb4e6b2bb2d746c2d6789d6c0b66198208b831f74b52198d744189
│   │   │   ├── cache-id
│   │   │   ├── diff
│   │   │   ├── parent
│   │   │   ├── size
│   │   │   └── tar-split.json.gz
│   │   ├── 0ee0aa554b8be64c963aaaf162df152784d868d21a7414146cb819a93e4bdb9e
│   │   │   ├── cache-id
│   │   │   ├── diff
│   │   │   ├── parent
│   │   │   ├── size
│   │   │   └── tar-split.json.gz
│   │   ├── ...
│   └── tmp
└── repositories.json

对上面的文件结构进行整理,可以得到如下图所示的结构:


image.png

此处我们主要关心imagedb、layerdb和repositories中的内容。

  • repositories.json
    该json文件部分内容如下:
 cat /var/lib/docker/image/overlay2/repositories.json  | jq
{
    "Repositories":{
        "registry/busybox":{
            "registry/busybox:v1":"sha256:7138284460ffa3bb6ee087344f5b051468b3f8697e2d1427bac1a20c8d168b14",
            "registry/busybox@sha256:34efe68cca33507682b1673c851700ec66839ecf94d19b928176e20d20e02413":"sha256:7138284460ffa3bb6ee087344f5b051468b3f8697e2d1427bac1a20c8d168b14"
        }
        }
    }
}

可以看出repositories.json文件中保存的是镜像tag和镜像ID的对应关系,以及镜像manifest的digest值和镜像ID的对应关系。其实我们除了通过镜像tag拉取镜像外,也可以直接使用manifest的digest拉取镜像,如下:

docker pull registry/busybox@sha256:34efe68cca33507682b1673c851700ec66839ecf94d19b928176e20d20e02413
  • imagedb
    imagedb下的content保存的是镜像的config文件
    metadata目录下保存的是元信息,如镜像的最近更新时间等
  • layerdb
    layerdb下面的mounts目录保存的信息暂不清楚是做什么用的。。。。
    sha256目录下保存的是镜像每一层的实际内容,包括parent、diff等。因为镜像是按层构建的,需要记录每一层的上一层是什么,与上一层的差异点等。

镜像索引

最后简单介绍下镜像索引。
从前面的config文件中可以知道,一个镜像只能在指定架构的机器上执行,如果要在不同架构的机器上运行,则需要拉取不同架构的镜像。以前我们通过uname -m命令获取机器架构信息,然后拉取不同架构的镜像,非常麻烦。因此,OCI推出了镜像索引,通过镜像索引,可以根据本地机器的架构,自动拉取对应架构的镜像。

image.png

如图所示,镜像索引包含了不同架构下镜像的manifest的digest。在拉取镜像的时候,就可以按照不同的OS架构拉取不同的镜像了。

你可能感兴趣的:(docker 镜像存储分析)