# 镜像存储

## 环境

* os: centos 7.3-1611
* kernel: 4.16.13
* docker-engine: 1.12.6
* backend-filesystem: xfs(ftype=1)
* storage-driver: overlay2

## 镜像准备

首先从docker官网拉取镜像 `library/registry:2.5.0`，然后用其搭建一个私有镜像仓库 `192.168.1.103:8021`，然后再把该镜像上传到私有镜像仓库中

## 目录树

我们在主机A上pull镜像`192.168.1.103:8021/library/registry:2.5.0`，接下来，我们看这台主机上镜像的存储结构。主机A上docker的安装目录为`/app/docker`。

`/app/docker`下有多个目录，与镜像相关的有两个：`image`与`overlay2`

```
$ tree -L 1 /app/docker
/app/docker
├── containers
├── image
├── network
├── overlay2
├── swarm
├── tmp
├── trust
└── volumes
```

* image的目录树如下：

```
$ tree image
image
└── overlay2
    ├── distribution
    │   ├── diffid-by-digest
    │   │   └── sha256
    │   │       ├── 06ba8e23299fcf9dd9efb3c5acd4c9d03badac5392953001c75d38197113a63a
    │   │       ├── 2ee5ed28ffa762104505295c3c256c52a87fe8af0114b9e0198e9036495e10b8
    │   │       ├── 802d2a9c64e8f556e510b4fe6c5378b9d49d8335a766d156ef21c7aeac64c9d6
    │   │       ├── d1562c23a8aa4913a2fc720a3c478121f45d26597b58bbf9a29238276ca420a7
    │   │       └── e110a4a1794126ef308a49f2d65785af2f25538f06700721aad8283b81fdfa58
    │   └── v2metadata-by-diffid
    │       └── sha256
    │           ├── 35039a507f7ae2cb74fd2405e6230036ee912588fcaac4d3c561774817590e97
    │           ├── 3bb5bc5ad373d4855414158babfedcd81a8e27cca04a861a5640c7ec9079bcfb
    │           ├── 4fe15f8d0ae69e169824f25f1d4da3015a48feeeeebb265cd2e328e15c6a869f
    │           ├── aa3a31ee27f3d041998258e135f623696d2c21a63ddf798ae206322c7d518247
    │           └── d00444e19d6513efe0e586094adb85fe5fc1c425d48e5b94263c65860a75d989
    ├── imagedb
    │   ├── content
    │   │   └── sha256
    │   │       └── c6c14b3960bdf9f5c50b672ff566f3dabd3e450b54ae5496f326898513362c98
    │   └── metadata
    │       └── sha256
    ├── layerdb
    │   ├── sha256
    │   │   ├── 273edac7c3ab13711e95ed35a4eb397e10ae9b69c896c9ad28b64cb9097be327
    │   │   │   ├── cache-id
    │   │   │   ├── diff
    │   │   │   ├── parent
    │   │   │   ├── size
    │   │   │   └── tar-split.json.gz
    │   │   ├── 3d9b8d55844ef4dc948d650855a2be52c6193502ba13b9afea9169495f254a03
    │   │   │   ├── cache-id
    │   │   │   ├── diff
    │   │   │   ├── parent
    │   │   │   ├── size
    │   │   │   └── tar-split.json.gz
    │   │   ├── 4fe15f8d0ae69e169824f25f1d4da3015a48feeeeebb265cd2e328e15c6a869f
    │   │   │   ├── cache-id
    │   │   │   ├── diff
    │   │   │   ├── size
    │   │   │   └── tar-split.json.gz
    │   │   ├── aff0ec55a7b1c314b647de027c36c25688f9784fee9ca34cbee0de56309fd5ea
    │   │   │   ├── cache-id
    │   │   │   ├── diff
    │   │   │   ├── parent
    │   │   │   ├── size
    │   │   │   └── tar-split.json.gz
    │   │   └── e553e3aa34103ab20e92e15af09af55aab8a3c8b1608a2f86c2ec3ee38b7ea45
    │   │       ├── cache-id
    │   │       ├── diff
    │   │       ├── parent
    │   │       ├── size
    │   │       └── tar-split.json.gz
    │   └── tmp
    └── repositories.json
```

* overlay2

overlay2的目录树如下（**注意：这里只显示两层**）

```
$ tree -L 2 overlay2
overlay2
├── 0f3f3124f1b247e8f20973ea11218589ec3f93e7acbc531acd9420029928dedf
│   ├── diff
│   ├── link
│   ├── lower
│   ├── merged
│   └── work
├── 15e3398435f9e280ce76c0006ebb9c01c717e1cf9c167d06cf9ea24fc6f6632d
│   ├── diff
│   ├── link
│   ├── lower
│   ├── merged
│   └── work
├── 3b14865a1ffd6e2926786e44da9176dfc0d98ce9ffffe23b8ed1f40993700960
│   ├── diff
│   ├── link
│   ├── lower
│   ├── merged
│   └── work
├── 9d8ccd3c87b4c56076c0e8924d8313ca77f5b2dcff5c8701bae7c91ee115e13c
│   ├── diff
│   ├── link
│   ├── lower
│   ├── merged
│   └── work
├── d388553d01e501540f5866e98bb3949ecfea24704bc900c01272e3fc74353753
│   ├── diff
│   └── link
└── l
    ├── IDKNGIJXKXKT55FYGI7Z6SMRI6 -> ../9d8ccd3c87b4c56076c0e8924d8313ca77f5b2dcff5c8701bae7c91ee115e13c/diff
    ├── KVYTHEQMEN2OKZWGWBCT5FSDUZ -> ../0f3f3124f1b247e8f20973ea11218589ec3f93e7acbc531acd9420029928dedf/diff
    ├── N62LB6PKCXTZOY7MNOFDZKVRL7 -> ../d388553d01e501540f5866e98bb3949ecfea24704bc900c01272e3fc74353753/diff
    ├── OBXTURUR4WON6PIOZ66VAOSWRP -> ../3b14865a1ffd6e2926786e44da9176dfc0d98ce9ffffe23b8ed1f40993700960/diff
    └── YOEFSN7RITY7NUQM33XC3ZIY5N -> ../15e3398435f9e280ce76c0006ebb9c01c717e1cf9c167d06cf9ea24fc6f6632d/diff
```

## repositories.json

repositories.json记录了本地repository相关的信息，主要是`repository:tag`到`iamgeID`的映射关系，如下：

其中`c6c14b3960bdf9f5c50b672ff566f3dabd3e450b54ae5496f326898513362c98`就是image-id，`51d8869caea35f58dd6a2309423ec5382f19c4e649b5d2c0e3898493f42289d6`是image-digest。如下：

```
$ docker images --digests
REPOSITORY                            TAG                 DIGEST                                                                    IMAGE ID            CREATED             SIZE
192.168.1.103:8021/library/registry   2.5.0               sha256:51d8869caea35f58dd6a2309423ec5382f19c4e649b5d2c0e3898493f42289d6   c6c14b3960bd        2 years ago         33.31 MB
```

## ImageConfig

每一个镜像（ImageID）都会有一个配置文件，比如上面的镜像`c6c14b3960bd`的配置文件就是`image/overlay2/imagedb/content/sha256/c6c14b3960bdf9f5c50b672ff566f3dabd3e450b54ae5496f326898513362c98`，查看该文件的内容如下：

在镜像的配置文件中，有该镜像所包含的所有layer，每一个layer用diff-id标识。diff\_ids数组中，`diff-ids[n]`是`diff-ids[n+1]`的parent，`diff-ids[0]`是最底层，它没有parent，`diff-ids[size-1]`表示最顶层。

image-id其实就是根据镜像的配置文件做哈希得到的，比如对上面的配置文件的内容做哈希，会发现哈希值就是image-id：

```
$ sha256sum image/overlay2/imagedb/content/sha256/c6c14b3960bdf9f5c50b672ff566f3dabd3e450b54ae5496f326898513362c98
c6c14b3960bdf9f5c50b672ff566f3dabd3e450b54ae5496f326898513362c98
```

注意：这里我们可以对imageConfig文件重命名然后再做哈希，得到的哈希值还是一样的，也就是说是其实是对文件内容做了哈希

## layer-diffid 与 layer-digest的映射关系

`image/overlay2/distribution`下存储了`layer-diffid`与`layer-digest`的映射关系，distribution的目录树如下

```
distribution
├── diffid-by-digest
│   └── sha256
│       ├── 06ba8e23299fcf9dd9efb3c5acd4c9d03badac5392953001c75d38197113a63a
│       ├── 2ee5ed28ffa762104505295c3c256c52a87fe8af0114b9e0198e9036495e10b8
│       ├── 802d2a9c64e8f556e510b4fe6c5378b9d49d8335a766d156ef21c7aeac64c9d6
│       ├── d1562c23a8aa4913a2fc720a3c478121f45d26597b58bbf9a29238276ca420a7
│       └── e110a4a1794126ef308a49f2d65785af2f25538f06700721aad8283b81fdfa58
└── v2metadata-by-diffid
    └── sha256
        ├── 35039a507f7ae2cb74fd2405e6230036ee912588fcaac4d3c561774817590e97
        ├── 3bb5bc5ad373d4855414158babfedcd81a8e27cca04a861a5640c7ec9079bcfb
        ├── 4fe15f8d0ae69e169824f25f1d4da3015a48feeeeebb265cd2e328e15c6a869f
        ├── aa3a31ee27f3d041998258e135f623696d2c21a63ddf798ae206322c7d518247
        └── d00444e19d6513efe0e586094adb85fe5fc1c425d48e5b94263c65860a75d989
```

* diffid-by-digest: 存放layer-digest到layer-diffid的映射关系
* v2metadata-by-diffid: 存放layer-diffid到layer-digest的映射关系

查看第一层的layer-digest与layer-diffid的映射关系

```
$ cat diffid-by-digest/sha256/e110a4a1794126ef308a49f2d65785af2f25538f06700721aad8283b81fdfa58
sha256:4fe15f8d0ae69e169824f25f1d4da3015a48feeeeebb265cd2e328e15c6a869f

$ jq . v2metadata-by-diffid/sha256/4fe15f8d0ae69e169824f25f1d4da3015a48feeeeebb265cd2e328e15c6a869f
[
  {
    "Digest": "sha256:e110a4a1794126ef308a49f2d65785af2f25538f06700721aad8283b81fdfa58",
    "SourceRepository": "192.168.1.103:8021/library/registry"
  }
]
```

## layer的元数据

layer的元数据都存储在`image/overlay2/layerdb`目录下，见目录树。每一个layer对应着一个sha256下的一个目录，目录的名字为layer的chainid。

第一层的chainid就为diffid，第N层的chainid的算法如下：

```
chainid(layerN) = sha256("chainid(layer1) chainid(layer2) ... chainid(layerN-1) diffid(layerN)")
```

我们来算一下第二层的chainid

```
$ echo -n "sha256:4fe15f8d0ae69e169824f25f1d4da3015a48feeeeebb265cd2e328e15c6a869f sha256:aa3a31ee27f3d041998258e135f623696d2c21a63ddf798ae206322c7d518247" | sha256sum
aff0ec55a7b1c314b647de027c36c25688f9784fee9ca34cbee0de56309fd5ea  -
```

在每一层对应的目录下有以下五个文件（第一层只有四个）：

* cache-id：docker下载layer的时候在本地生成的一个随机uuid
* diff：diff 文件存放layer的diffid
* parent：parent文件存放当前layer的父layer的diffid，注意第一层没有父layer故没有该文件
* size：该层的大小，单位为字节
* tar-split.json.gz：layer压缩包的split文件，通过这个文件可以还原layer的tar包，详情可参考 <https://github.com/vbatts/tar-split>

## layer的数据

所有layer的文件系统数据都存放在`/app/docker/overlay2`目录下，查看\[目录树]。每一个layer都对应着该目录下的一个目录，目录的名子就是layer的cache-id。

另外还有一个`l`目录，在本地有多少个layer，该目录下就有多少个符号链接文件。符号链接文件指向layer的文件系统目录。如下：

```
lrwxrwxrwx. 1 root root 72 Aug 27 16:34 IDKNGIJXKXKT55FYGI7Z6SMRI6 -> ../9d8ccd3c87b4c56076c0e8924d8313ca77f5b2dcff5c8701bae7c91ee115e13c/diff
lrwxrwxrwx. 1 root root 72 Aug 27 16:34 KVYTHEQMEN2OKZWGWBCT5FSDUZ -> ../0f3f3124f1b247e8f20973ea11218589ec3f93e7acbc531acd9420029928dedf/diff
lrwxrwxrwx. 1 root root 72 Aug 27 16:34 N62LB6PKCXTZOY7MNOFDZKVRL7 -> ../d388553d01e501540f5866e98bb3949ecfea24704bc900c01272e3fc74353753/diff
lrwxrwxrwx. 1 root root 72 Aug 27 16:34 OBXTURUR4WON6PIOZ66VAOSWRP -> ../3b14865a1ffd6e2926786e44da9176dfc0d98ce9ffffe23b8ed1f40993700960/diff
lrwxrwxrwx. 1 root root 72 Aug 27 16:34 YOEFSN7RITY7NUQM33XC3ZIY5N -> ../15e3398435f9e280ce76c0006ebb9c01c717e1cf9c167d06cf9ea24fc6f6632d/diff
```

接下来我们来看layer对应目录下的内容（第一层与其他层的内容会不一样，第一层只有diff目录与link文件）

我们看第二层目录下的内容

```
ll 15e3398435f9e280ce76c0006ebb9c01c717e1cf9c167d06cf9ea24fc6f6632d/
total 8
drwxr-xr-x. 6 root root 50 Aug 27 16:34 diff
-rw-r--r--. 1 root root 26 Aug 27 16:34 link
-rw-r--r--. 1 root root 28 Aug 27 16:34 lower
drwx------. 2 root root  6 Aug 27 16:34 merged
drwx------. 2 root root  6 Aug 27 16:34 work
```

* diff

diff目录下是该layer的文件系统，如下：

```
ll 15e3398435f9e280ce76c0006ebb9c01c717e1cf9c167d06cf9ea24fc6f6632d/diff
total 0
drwxr-xr-x. 5 root root 79 Jun 24  2016 etc
drwxr-xr-x. 3 root root 61 Jun 24  2016 lib
drwxr-xr-x. 7 root root 66 Jun 24  2016 usr
drwxr-xr-x. 3 root root 19 Jun 24  2016 var
```

* link

link文件的内容就是该layer的符号链接文件的名字：

```
$ cat 15e3398435f9e280ce76c0006ebb9c01c717e1cf9c167d06cf9ea24fc6f6632d/link 
YOEFSN7RITY7NUQM33XC3ZIY5N
```

* lower

该文件的内容是父layer的符号链接文件的名字，根据这个文件可以索引构建出整个镜像的层次结构

```
$ cat 15e3398435f9e280ce76c0006ebb9c01c717e1cf9c167d06cf9ea24fc6f6632d/lower
l/N62LB6PKCXTZOY7MNOFDZKVRL7
```

* merged和work 目录

每当启动一个容器时，会将 link 指向的镜像层目录以及 lower 指向的镜像层目录联合挂载到 merged 目录，因此，容器内的视角就是 merged 目录下的内容。

而 work 目录则是用来完成如 copy-on-write 的操作。

在没有启动容器的时候，这两个目录下都是空的。

## manifest

前面已经介绍了 config 文件和 layer 的存储位置，但唯独不见 manifest，去哪了呢？

manifest 里面包含的内容就是对 config 和 layer 的 sha256 + media type 描述，目的就是为了下载 config 和 layer，等 image 下载完成后，manifest 的使命就完成了，里面的信息对于 image 的本地管理来说没什么用，所以 docker 在本地没有单独的存储一份 manifest 文件与之对应。

不过我们可以看一下manifest文件长什么样。通过命令`curl -H "Accept: application/vnd.docker.distribution.manifest.v2+json" 192.168.1.103:8021/v2/library/registry/manfiests/2.5.0`下载`library/registry:2.5.0`的manifest，内容如下：

发现，`config.digest`就是镜像的image-digest。layers数组中就是各层的layer-digest；layers\[0]是第一层，layers\[N-1]是最顶层。

## 镜像的下载过程

* docker 发送 image 的名称+tag（或者 digest）给 registry 服务器，服务器根据收到的 image 的名称+tag（或者 digest），找到相应 image 的 manifest，然后将 manifest 返回给 docker
* docker 得到 manifest 后，读取里面 image 配置文件的 digest(sha256)，这个 sha256 码就是 image 的 ID
* 根据 ID 在本地找有没有存在同样 ID 的 image，有的话就不用继续下载了
* 如果没有，那么会给 registry 服务器发请求（里面包含配置文件的 sha256 和 media type），拿到 image 的配置文件（Image Config）
* 根据配置文件中的 diff\_ids（每个 diffid 对应一个 layer tar 包的 sha256，tar 包相当于 layer 的原始格式），在本地找对应的 layer 是否存在
* 如果 layer 不存在，则根据 manifest 里面 layer 的 sha256 和 media type 去服务器拿相应的 layer（相当去拿压缩格式的包）。
* 拿到后进行解压，并检查解压后 tar 包的 sha256 能否和配置文件（Image Config）中的 diff\_id 对的上，对不上说明有问题，下载失败
* 根据 docker 所用的后台文件系统类型，解压 tar 包并放到指定的目录
* 等所有的 layer 都下载完成后，整个 image 下载完成，就可以使用了

## 附录

接下来总结一下各种id

* image-id

  image-id是imageConfig的sha256的哈希值。
* image-digest

  image-digest是manifest的sha256的哈希值。
* layer-diffid

  layer-diffid是对layer的未压缩的tar包的内容做sha256得到的哈希值。

  我们可以通过registry的API下载到某个layer的压缩后的tar包`layer.tar.gzip`，但是实验中发现手动用`tar xzvf layer.tar.gzip -C layer/`先解压，然后再用`tar cvf layer.tar layer/*`打包，对layer.tar做`sha256sum layer.tar`，得到的哈希值并不是layer的diffid。猜测layer的解压与打包用shell的命令不对或打包时有些参数不对。
* layer-digest

  layer-digest是对layer的压缩后的tar包的内容做sha256得到的哈希值。

  我们可以通过registry的API `GET /v2/{repository}/blobs/{layer-digest}` 下载压缩后的layer，然后对该文件内容做sha256哈希，得到的哈希值就是layer-digest。

  比如，我们下载镜像`library/registry:2.5.0`的第一层：

  ```
  curl 192.168.1.103:8021/v2/library/registry/blobs/sha256:e110a4a1794126ef308a49f2d65785af2f25538f06700721aad8283b81fdfa58 -o layer1.tar.gzip
  ```

  下载得到的文件是经过gizp压缩算法压缩的，接下来我们对该layer做哈希（文件名可以随便取，没有影响）：

  ```
  $ sha256sum layer1.tar.gzip
  e110a4a1794126ef308a49f2d65785af2f25538f06700721aad8283b81fdfa58
  ```

  发现，哈希值就是第一层的layer-digest。

  不过，如果我们尝试用先用`tar xzvf layer1.tar.gzip -C layer1/`解压，然后再用命令`tar czvf layer1.tar.gizp layer1/*`压缩，然后再对我们自已压缩过的文件做哈希，得到的哈希值与layer-digest不一致。猜测是不能使用shell的tar命令或者参数不对。

## Reference

\[1] <https://www.yangcs.net/posts/how-manage-image/>

\[2] <https://segmentfault.com/a/1190000009309347>

\[3] <https://segmentfault.com/a/1190000009730986>

\[4] <https://yq.aliyun.com/articles/57752>

\[5] <https://windsock.io/explaining-docker-image-ids/>

\[6] <https://docs.docker.com/registry/spec/api/#pulling-an-image>


---

# Agent Instructions: Querying This Documentation

If you need additional information that is not directly available in this page, you can query the documentation dynamically by asking a question.

Perform an HTTP GET request on the current page URL with the `ask` query parameter:

```
GET https://pshizhsysu.gitbook.io/docker/jing-xiang-de-ben-di-cun-chu.md?ask=<question>
```

The question should be specific, self-contained, and written in natural language.
The response will contain a direct answer to the question and relevant excerpts and sources from the documentation.

Use this mechanism when the answer is not explicitly present in the current page, you need clarification or additional context, or you want to retrieve related documentation sections.
