Post

Docker镜像文件的本地存储

本文描述了Docker镜像文件在本地的分层存储方式

Docker镜像文件的本地存储

众所周知,docker的image是按层存储的,关于layer的存储方式,做一个说明。 其整体上,layer有这些概念:

1
layerID -> diffID -> chainID -> cacheID
  • digest(layerID)就是 pull 镜像时的 hash ID,拉取是 镜像层文件是压缩文件,压缩态,tar.gz
  • diffID是 docker inspect 查看到的 镜像层 hash ID,此时 镜像层文件是解压缩的,解压缩态,tar
  • chainID用来在layerdb目录下保存具体的层信息
  • cacheID用来在overlay2目录下存储具体的层信息

docker的默认工作目录在 /var/lib/docker,我们重点关注imageoverlay2这两个文件夹,分别存放镜像信息和具体的layer文件。

/var/lib/docker/image/overlay2

1
2
3
4
5
6
root@linuc:/var/lib/docker/image/overlay2# ls -l
total 16
drwx------ 4 root root 4096 Nov 16 14:17 distribution
drwx------ 4 root root 4096 Nov 16 14:03 imagedb
drwx------ 5 root root 4096 Nov 16 14:20 layerdb
-rw------- 1 root root  873 Dec 22 10:13 repositories.json

repositories.json

保存了当前的docker daemon维护的镜像信息

1
2
3
4
5
6
root@linuc:/var/lib/docker/image/overlay2# jq . repositories.json 
……
"img": {
      "img:latest": "sha256:21adcd7be0556b5576e627f47c753b5113a8d91067aa94008769558462a482be",
      "img:tree": "sha256:4c68ba802076e7390e635e53e724179412b77aade7e36f072bf57ad0869101e8"
    },

结果显示,当前的docker daemon维护了两个img镜像,tag分别是latest和tree,我们以latest为例,记录此镜像的imageID 21adcd7be0556...

distribution 目录

diffid-by-digest保存了digest(layerID)->diffID的映射关系 v2metadata-by-diffid保存了diffid -> (digest,repository)的映射关系

imagedb 目录

保存具体的镜像元数据信息,进入/var/lib/docker/image/overlay2/imagedb/content/sha256下。 该目录下的文件都以sha256为名,我们可以看见上面找到的 img:latest-imageID: 21adcd7be0556...

1
2
3
4
5
6
root@linuc:/var/lib/docker/image/overlay2/imagedb/content/sha256# ls
21adcd7be0556b5576e627f47c753b5113a8d91067aa94008769558462a482be
4c68ba802076e7390e635e53e724179412b77aade7e36f072bf57ad0869101e8
62753ecb37c4e3c5bf7b6c8d02fe88b543f553e92492fca245cded98b0d364dd
e6a0117ec169eda93dc5ca978c6ac87580e36765a66097a6bfb6639a3bd4038a
f215ff3bfefbdfb038a0a1aaa35bf8f5dcb708a2116fdc1848c812ff9bdada7d

这些文件都是json文件,我们查看这些文件,实质上跟用docker inspect的效果是一样的。

1
2
3
4
5
6
7
8
9
10
11
12
root@linuc:/var/lib/docker/image/overlay2/imagedb/content/sha256# jq . 21adcd7be0556b5576e627f47c753b5113a8d91067aa94008769558462a482be 
……
 "rootfs": {
    "type": "layers",
    "diff_ids": [
      "sha256:20312b5745843a1d409495e32b3cb4aaf48fc5f02eba6ae34ec2dcee82a5886a",
      "sha256:4b96e5616c0fb8259bebd63045a9a61da52fa92d22b6bed78d91a02145a9ba73",
      "sha256:5f70bf18a086007016e948b04aed3b82103a36bea41755b6cddfaf10ace3c6ef",
      "sha256:99cef7be727bcfa4c8efe34747f1752b59d3fa39b64cdfbb9d17569799b816b7",
      "sha256:a2bd4f4d39eab016e1dedf197a88d659085c41d301bbf859144e583c6642be50"
    ]
  }

在rootfs字段中看到的diff_ids,是每一层的diffID。对每层文件,docker把起打包成tar包,对tar包计算sha256得到的就是diffID

layerdb 目录

layer具体的文件系统存储在/var/lib/docker/overlay2中,但并不是以上述提到的diffID直接存储的,而是使用其cacheID来存储的,layerdb目录描述了从diffID->cacheID的映射关系。 但是从diffID->cacheID也不是直接映射的,而是有chainID参与。

layerdb目录下所有文件夹的名称是chainID

具体而言,layer.ChainID只用本地,根据layer.DiffID计算,并用于layerdb的目录名称(cacheID)。 chainID唯一标识了一组(像糖葫芦一样的串的底层)diffID的hash值,包含了这一层和它的父层(底层),当然这个糖葫芦可以有一颗山楂,也就是chainID(layer0)==diffID(layer0);对于多颗山楂的糖葫芦,

ChainID(layerN) = SHA256hex(ChainID(layerN-1) + " " + DiffID(layerN))

再看:

1
2
3
4
5
6
7
    "diff_ids": [
      "sha256:20312b5745843a1d409495e32b3cb4aaf48fc5f02eba6ae34ec2dcee82a5886a",
      "sha256:4b96e5616c0fb8259bebd63045a9a61da52fa92d22b6bed78d91a02145a9ba73",
      "sha256:5f70bf18a086007016e948b04aed3b82103a36bea41755b6cddfaf10ace3c6ef",
      "sha256:99cef7be727bcfa4c8efe34747f1752b59d3fa39b64cdfbb9d17569799b816b7",
      "sha256:a2bd4f4d39eab016e1dedf197a88d659085c41d301bbf859144e583c6642be50"
    ]

这里面记录了每一层的diffID,第一个是最底层(其没有父层)。 这种情况下,其diffID=chainID.

1
2
3
4
5
6
7
# 第一层,其diffID就是chainID
root@linuc:/var/lib/docker/image/overlay2/layerdb/sha256# ls | grep 20312b5745*
20312b5745843a1d409495e32b3cb4aaf48fc5f02eba6ae34ec2dcee82a5886a

# 第二层,其diffID不是chainID,直接找diffID是找不到的,因为chainID不是直接diffID
root@linuc:/var/lib/docker/image/overlay2/layerdb/sha256# ls | grep 4b96e5616c*
root@linuc:/var/lib/docker/image/overlay2/layerdb/sha256# 

针对高层layer(其有父层)

ChainID(layerN) = SHA256hex(ChainID(layerN-1) + “ “ + DiffID(layerN))

我们用第一层的diffID=chainID和第二层diffID来计算第二层的chainID

1
2
3
4
root@linuc:/var/lib/docker/image/overlay2/layerdb/sha256# echo -n "sha256:20312b5745843a1d409495e32b3cb4aaf48fc5f02eba6ae34ec2dcee82a5886a sha256:4b96e5616c0fb8259bebd63045a9a61da52fa92d22b6bed78d91a02145a9ba73" | sha256sum | awk '{print $1}'
b4533429e3cf7b89e83d45befd513af96d2777564dcdeff3752786a193535a9c
root@linuc:/var/lib/docker/image/overlay2/layerdb/sha256# ls | grep b4533429e*
b4533429e3cf7b89e83d45befd513af96d2777564dcdeff3752786a193535a9c

可见我们计算第二层的chainID是正确的。 我们进入任意一层的chainID文件夹中,有如下文件:

1
2
3
4
5
6
7
root@linuc:/var/lib/docker/image/overlay2/layerdb/sha256/b4533429e3cf7b89e83d45befd513af96d2777564dcdeff3752786a193535a9c# tree
.
├── cache-id
├── diff
├── parent
├── size
└── tar-split.json.gz

其中,cache-id保存了这层的文件系统在 /var/lib/docker/overlay2下具体的位置。

脚本自动化探测

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
#!/bin/bash

# 检查是否输入了镜像名称和输出模式

if [ "$#" -ne 2 ]; then
    echo "使用方法: $0 <镜像名称> <输出模式>"
    echo "输出模式可以是 'class' 或 'layer'"
    exit 1
fi

# 获取镜像名称和输出模式

image_name=$1
output_mode=$2

# 执行 docker inspect 命令

output=$(docker inspect $image_name)

# 使用 jq 工具解析 JSON 输出,并获取 RootFS.Layers 数组中的所有值

layers=$(echo $output | jq -r '.[0].RootFS.Layers[]')

# 初始化 chainID 为第一层的 layerID

chainID=$(echo $layers | cut -d' ' -f1)

if [ "$output_mode" = "class" ]; then
    # 输出所有的 layerID
    echo "layerIDs:"
    index=1
    for layerID in $layers; do
        echo "   layer ${index}: $layerID"
        index=$((index+1))
    done

    # 输出所有的 chainID
    echo "chainIDs:"
    chainID=$(echo $layers | cut -d' ' -f1)
    echo -n "   layer 1: "
    echo $chainID
    
    index=2
    for layerID in $(echo $layers | cut -d' ' -f2-); do
        chainID=$(echo -n "$chainID $layerID" | sha256sum | awk '{print $1}')
        chainID="sha256:$chainID"
        echo -n "   layer ${index}: "
        echo $chainID
        index=$((index+1))
    done
    
    # 输出所有的 cache-id 文件的内容
    echo "cacheIDs:"
    chainID=$(echo $layers | cut -d' ' -f1)
    cache_id=$(cat /var/lib/docker/image/overlay2/layerdb/sha256/${chainID:7}/cache-id)
    echo "   layer 1: $cache_id"
    
    index=2
    for layerID in $(echo $layers | cut -d' ' -f2-); do
        chainID=$(echo -n "$chainID $layerID" | sha256sum | awk '{print $1}')
        chainID="sha256:$chainID"
        cache_id=$(cat /var/lib/docker/image/overlay2/layerdb/sha256/${chainID:7}/cache-id)
        echo "   layer ${index}: $cache_id"
        index=$((index+1))
    done

elif [ "$output_mode" = "layer" ]; then
    index=1
    for layerID in $layers; do
        echo "layer ${index}:"
        echo "   layerID: $layerID"
        if [ "$index" -eq 1 ]; then
            chainID=$layerID
        else
            chainID=$(echo -n "$chainID $layerID" | sha256sum | awk '{print $1}')
            chainID="sha256:$chainID"
        fi
        echo "   chainID: $chainID"
        cache_id=$(cat /var/lib/docker/image/overlay2/layerdb/sha256/${chainID:7}/cache-id)
        echo "   cacheID: $cache_id"
        index=$((index+1))
    done
else
    echo "无效的输出模式: $output_mode"
    echo "输出模式可以是 'class' 或 'layer'"
    exit 1
fi
This post is licensed under CC BY 4.0 by the author.