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。HEAD /v2/<namespace>/<repo>/manifest/<tag>(docker1.19之前不发HEAD,直接发GET请求)
  2. docker client发送GET请求到registry,下载Manifest文件。
  3. registry根据镜像tag,得到镜像的manifest文件,返回给docker client。
  4. docker client拿到manifest文件后,根据其中的config的digest,也就是image ID,检查下镜像在本地是否存在。
  5. 如果镜像不存在,则下载config文件,并根据config文件中的diff_ids得到镜像每一层解压后的digest。
  6. 然后根据每层解压后的digest文件,检查本地是否存在,如果不存在,则通过manifest文件中的layer的digest下载该层并解压,然后校验解压后digest是否匹配。GET /v2/<namespace>/<repo>/blobs/<sha256>
  7. 下载完所有层后,镜像就下载完毕。

镜像推送流程

镜像下载流程和推送过程正好相反。

  1. 先发送POST请求,根据响应header的Location获取blob需要上传到哪个位置。POST /v2/<namespace>/<repo>/blobs/uploads
  2. 发送HEAD请求,检查每一层再镜像仓库中是否存在。返回404表示不存在,200表示已存在,307表示重定向到镜像存储位置确认,如对象存储中确认。HEAD /v2/<namespace/<repo>/blobs/<digest>
  3. 并发上传镜像层。docker client发送PATCH请求到registry,上传该层数据。上传完毕后,继续步骤2,直到所有镜像层传输完毕后,进入下一步。PATCH /v2/<namespace>/<repo>/blobs/uploads/<id>
  4. docker client发送PUT请求到registry,将镜像manifest发送给registry,并上传镜像tag。至此,镜像上传完毕。PUT /v2/<namespace>/<repo>/manifest/<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架构拉取不同的镜像了。

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 213,014评论 6 492
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 90,796评论 3 386
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 158,484评论 0 348
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 56,830评论 1 285
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 65,946评论 6 386
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 50,114评论 1 292
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 39,182评论 3 412
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 37,927评论 0 268
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 44,369评论 1 303
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 36,678评论 2 327
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 38,832评论 1 341
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 34,533评论 4 335
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 40,166评论 3 317
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,885评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,128评论 1 267
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 46,659评论 2 362
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 43,738评论 2 351

推荐阅读更多精彩内容