Docker源码分析—镜像存储

基于Docker 17.05.x

Docker使用层存储来管理容器和镜像的层,那么如何知道哪些层属于某一个镜像?也就说如果现在系统上有很多个镜像,每个镜像又包括很多层,如果使用Overlay2,你可以在存储驱动目录下看到一大堆目录,如何知道哪些层是属于某个镜像?也就是说一个镜像如何和镜像层之间建立起关系

这篇文章主要通过分析Docker源码,介绍镜像存储的创建过程

Docker使用镜像存储来管理镜像,通过镜像存储可以查找系统当前所有镜像,以及每个镜像的镜像层信息。镜像存储主要通过镜像的元数据(imageMeta类型)来索引镜像的顶层镜像层,然后可以通过层存储来完成所有镜像层的查找等操作,关系结构如下:


创建镜像存储

Docker daemon启动过程中,首先创建层存储,然后创建镜像存储。创建镜像存储需要用到两个参数:一个是层存储,另外一个是存储后端。存储后端记录了镜像信息的存储根目录

1
2
3
4
5
6
7
8
9
10
11
12
13
//d是docker daemon对象
//1.首先创建层存储
d.layerStore = layer.NewStoreFromOptions(...)
...
//imageRoot:/var/lib/docker/image/graphDriver
imageRoot := filepath.Join(config.Root, "image", graphDriver)
...
//2.创建存储后端
//镜像存储后端根目录:imageRoot/imagedb
ifs, err := image.NewFSStoreBackend(filepath.Join(imageRoot, "imagedb"))
...
//3.创建镜像存储
d.imageStore, err = image.NewImageStore(ifs, d.layerStore)

创建存储后端

创建存储后端调用函数NewFSStoreBackend()创建,传入镜像存储的位置。如果使用overlay2作为存储驱动,则参数字符串为“/var/lib/docker/image/overlay2/imagedb”:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
func NewFSStoreBackend(root string) (StoreBackend, error) {
return newFSStore(root)
}

func newFSStore(root string) (*fs, error) {
//1.构造一个fs结构
s := &fs{
root: root,// /var/lib/docker/image/(graphdriver)/imagedb
}
/*
2.创建目录content和目录metadata
contentDirName和metadataDirName为全局变量:
contentDirName = "content"
metadataDirName = "metadata"
*/
if err := os.MkdirAll(filepath.Join(root, contentDirName, string(digest.Canonical)), 0700); err != nil {
return nil, errors.Wrap(err, "failed to create storage backend")
}
if err := os.MkdirAll(filepath.Join(root, metadataDirName, string(digest.Canonical)), 0700); err != nil {
return nil, errors.Wrap(err, "failed to create storage backend")
}
return s, nil
}

从上面的代码中可以看出,存储后端的创建主要包括2步:

  1. 构造一个存储后端的fs结构对象
  2. 在镜像存储后端根目录下创建目录content和目录metadata

content目录用于存放镜像的配置文件。metadata目录会存放镜像的父镜像信息,如果一个镜像使用另一个镜像作为父镜像,则metadata/sha256/目录下会为这个镜像创建一个目录,这个目录中包含一个parent文件,文件内容为“sha256:父镜像ID”

创建镜像存储

在创建镜像的存储后端之后,调用函数NewImageStore()创建镜像存储:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
/*
传入镜像的存储后端对象fs,以及层存储对象layerStore
StoreBackend和LayerGetReleaser是两个接口:
fs实现了StoreBackend接口
layerStore实现了LayerGetReleaser接口
*/
func NewImageStore(fs StoreBackend, ls LayerGetReleaser) (Store, error) {
//1.构造镜像存储结构
is := &store{
ls: ls,
images: make(map[ID]*imageMeta),
fs: fs,
digestSet: digestset.NewSet(),
}

//2.调用函数restore装载当前镜像
if err := is.restore(); err != nil {
return nil, err
}

return is, nil
}

镜像存储的创建逻辑很简单,也是只有2步:

  1. 构造镜像存储结构
  2. 调用restore()函数装载当前镜像

装载镜像需要用到镜像的配置文件,镜像的配置文件在存储后端根目录下的content目录中:

1
2
3
4
5
6
7
8
9
10
11
12
#只有1个ubuntu镜像,镜像ID为2a4cca...
root@8a7d95c22e2b:/var/lib/docker/image/overlay2/imagedb/content# docker images
REPOSITORY TAG IMAGE ID CREATED SIZE
ubuntu latest 2a4cca5ac898 3 days ago 111MB

#查看镜像的配置文件,文件名和镜像ID相同
root@8a7d95c22e2b:/var/lib/docker/image/overlay2/imagedb/content# pwd
/var/lib/docker/image/overlay2/imagedb/content
root@8a7d95c22e2b:/var/lib/docker/image/overlay2/imagedb/content# ls
sha256
root@8a7d95c22e2b:/var/lib/docker/image/overlay2/imagedb/content# ls sha256/
2a4cca5ac898476c2c47a8d6a17102e00241d6fa377fbe4d50787fe3d7a8d4d6

下面看看restore函数如何装载镜像

装载镜像

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
func (is *store) restore() error {
/*
读取/../content/sha256目录,
使用每一个镜像配置文件的文件名以及sha256构造一个digest.Digest对象
然后根据这个对象构造镜像元数据对象imageMeta
并将镜像ID和镜像元数据对象存入镜像存储的镜像元数据映射表中
*/
err := is.fs.Walk(func(dgst digest.Digest) error {
//1.读取镜像配置文件构造image对象img
img, err := is.Get(IDFromDigest(dgst))
if err != nil {
logrus.Errorf("invalid image %v, %v", dgst, err)
return nil
}
var l layer.Layer
//2.获取该镜像顶层镜像层的ChainID,
//根据ChainID到层存储的镜像层元数据映射表中获取顶层镜像层的元数据对象
if chainID := img.RootFS.ChainID(); chainID != "" {
l, err = is.ls.Get(chainID)
if err != nil {
return err
}
}
//3.将该镜像的digest.Digest对象dgst添加到digestSet中
if err := is.digestSet.Add(dgst); err != nil {
return err
}

//4.构造镜像元数据对象
imageMeta := &imageMeta{
layer: l,//镜像顶层元数据
children: make(map[ID]struct{}),
}

//5.将元数据信息添加到镜像存储的”镜像元数据映射表“中
is.images[IDFromDigest(dgst)] = imageMeta

return nil
})
if err != nil {
return err
}

// Second pass to fill in children maps
for id := range is.images {
if parent, err := is.GetParent(id); err == nil {
if parentMeta := is.images[parent]; parentMeta != nil {
parentMeta.children[id] = struct{}{}
}
}
}

return nil
}

装载镜像通过walk()函数来读取镜像的存储后端目录中,镜像配置文件所在的目录/../content/sha256,根据每个镜像名和sha256,构造一个digest.Digest对象dgst,然后根据dgst完成镜像的装载,主要包括5个步骤:

  1. 调用镜像存储实现的Get()函数读取镜像配置文件的内容,构造并返回一个image对象img
  2. 调用img对象RootFS成员的ChainID()函数获取镜像顶层的层ChainID镜像层的元数据目录就是根据层的ChainID来命名),可以根据这个ChainID调用层存储的Get()函数查找镜像层元数据映射表,获取顶层镜像层roLayer类型的元数据对象
  3. 将这个dgst添加到镜像存储的digestSet中
  4. 构造镜像的元数据对象
  5. 将镜像ID和镜像的元数据对象作为新的表项添加到镜像元数据映射表

实际上装载镜像就是构造镜像的元数据对象,然后将其添加到镜像存储的镜像元数据映射表中。以后就可以根据这个映射表查找镜像的元数据,然后进一步找到镜像的所有层

接下来对步骤1和步骤2做进一步分析

首先是如何根据配置文件构造一个Image对象:

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
func (is *store) Get(id ID) (*Image, error) {
// todo: Check if image is in images
// todo: Detect manual insertions and start using them
//到imagedb/content/sha256/下读取镜像配置文件
config, err := is.fs.Get(id.Digest())
if err != nil {
return nil, err
}

//根据json配置文件的内容构造镜像的Image对象
img, err := NewFromJSON(config)
if err != nil {
return nil, err
}
img.computedID = id

//设置父镜像id,实际上就是到metadata目录下的相应目录中读取parent文件
//parent文件不存在则没有父镜像
img.Parent, err = is.GetParent(id)
if err != nil {
img.Parent = ""
}

return img, nil
}

函数首先调用存储后端fs的Get()函数读取镜像的配置文件,将读取内容存入config变量中。然后调用NewFromJSON()函数,根据配置文件的内容构造一个Image对象

镜像配置文件为JSON格式,可以使用jq工具查看镜像配置文件的信息。在安装完jq后,可以使用命令jq '.' 配置文件名 查看镜像配置文件的内容

镜像配置文件的内容:

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
root@8a7d95c22e2b:/var/lib/docker/image/overlay2/imagedb/content/sha256# pwd
/var/lib/docker/image/overlay2/imagedb/content/sha256
oot@8a7d95c22e2b:/var/lib/docker/image/overlay2/imagedb/content/sha256# ls
2a4cca5ac898476c2c47a8d6a17102e00241d6fa377fbe4d50787fe3d7a8d4d6
root@8a7d95c22e2b:/var/lib/docker/image/overlay2/imagedb/content/sha256# jq '.' 2a4cca5ac898476c2c47a8d6a17102e00241d6fa377fbe4d50787fe3d7a8d4d6
{
"architecture": "amd64",
"config": {
"Hostname": "",
"Domainname": "",
"User": "",
"AttachStdin": false,
"AttachStdout": false,
"AttachStderr": false,
"Tty": false,
...
},
...
"rootfs": {
"type": "layers",
"diff_ids": [
"sha256:833649a3e04c96faf218d8082b3533fa0674664f4b361c93cad91cf97222b733",
"sha256:a6a01ad8b53fac9c52a907f40b70a6c61fe305db83a63ae83c970e7be1029d86",
"sha256:d2bb1fc88136e1c5d59909e7704a9eb6671ea7aa4a0d4272f3e2689fc31a6bd1",
"sha256:2bbb3cec611d9e3c4016eba00c9f87c51c7d03e54ccd9f12f8f04ac369f5243d",
"sha256:8600ee70176b569d0776833f106d239d56043cb854a5edbb74aff6c5e8d4782d"
]
}
}

所以NewFromJSON()函数实际上就是根据这些信息来构造出一个Image对象。注意配置文件最后的“diff_ids”,这部分记录了该镜像所有镜像层的diffID(存于Image对象的RootFS成员中),也就是镜像层元数据目录下diff文件中的内容。但是要获取镜像层的元数据需要得到镜像层的ChainID(镜像层元数据映射表的key类型为ChainID),实际上每一层镜像层的ChainID都是根据父层的ChainID和该层的diffID计算来的。可以通过层存储的CreateChainID()函数来得到该层的ChainID

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
func CreateChainID(dgsts []DiffID) ChainID {
return createChainIDFromParent("", dgsts...)
}

func createChainIDFromParent(parent ChainID, dgsts ...DiffID) ChainID {
if len(dgsts) == 0 {
return parent
}
if parent == "" {
return createChainIDFromParent(ChainID(dgsts[0]), dgsts[1:]...)
}
// H = "H(n-1) SHA256(n)"
dgst := digest.FromBytes([]byte(string(parent) + " " + string(dgsts[0])))
return createChainIDFromParent(ChainID(dgst), dgsts[1:]...)
}

ubuntu镜像的层次关系以及每层的diffID,chainID如下表:

描述 编号 diffID chainID
顶层镜像层 5 sha256:8600… sha256:01c7…
4 sha256:2bbb… sha256:8f1d…
3 sha256:d2bb… sha256:fd96…
2 sha256:a6a0… sha256:809c…
底层镜像层 1 sha256:8336… sha256:8336…
  • 创建底层镜像层的chainID时,数组dgsts只有一个元素,即底层镜像层的diffID,因此函数createChainIDFromParent()最终返回的chainID实际就是diffID
  • 为后面的镜像层创建chainID时,数组dgsts包含该层及所有父层的diffID,下标越小层次越低。第一次调用createChainIDFromParent()时,parent为“”,所以会以底层镜像的diffID做为父层的ChainID(实际上底层镜像层的diffID也就是底层镜像层的chainID),根据其余层的diffID组成的数组继续递归处理。第二次调用createChainIDFromParent()时(第一次递归),parent为底层镜像的chainID,dgsts为剩余层diffID组成的数组,函数会根据底层镜像层的chainID(参数parent)以及编号为2的镜像层的diffID,调用digest.FromBytes()计算编号为2的镜像层的chainID。然后以编号为2的镜像层的chainID为parent,根据剩余层的diffID数组接着递归处理,直到得到最后一层的chainID后,再一次调用createChainIDFromParent(),会返回parent,也就是最后一层的chainID

实际上在装载镜像的过程中,当调用NewFromJSON()读取镜像配置文件并构造出镜像的元数据对象img后,会调用img.RootFS.ChainID()进行上述处理,RootFS结构如下:

1
2
3
4
type RootFS struct {
Type string `json:"type"`
DiffIDs []layer.DiffID `json:"diff_ids,omitempty"`
}

RootFS的DiffIDs存有镜像所有层的diffID,RootFS的ChainID()函数会调用CreateChainID()函数,传入diffID数组(RootFS的DiffIDs),最终会获得顶层镜像层的chainID。然后就可以根据这个chainID,查找层存储的镜像层元数据映射表,找到顶层镜像层的元数据,用来设置镜像元数据imageMeta的layer成员


查找镜像的所有层

在了解一个镜像元数据对象(imageMeta类型)如何与镜像层进行关联后,就可以找到镜像的所有层了

imageMeta的layer成员指向镜像顶层镜像层的元数据对象(roLayer),镜像层元数据对象的cacheID成员记录了镜像层在存储驱动目录下的目录名,parent成员指向父层镜像层的元数据对象,因此,可以用cacheID在存储驱动目录下找到顶层镜像层的内容,然后根据parent获取父层镜像层的元数据对象,根据父层镜像层元数据对象的cacheID在存储驱动目录下找到父层镜像层的内容,重复这个过程,直到在存储驱动目录下找到底层镜像的内容

总的来说就是,通过镜像元数据对象的layer成员,可以将镜像存储与层存储联系起来,然后层存储可以根据顶层镜像层的元数据—layer,完成该镜像所有内容的查找操作

文章目录
  1. 1. 创建镜像存储
    1. 1.1. 创建存储后端
    2. 1.2. 创建镜像存储
      1. 1.2.1. 装载镜像
  2. 2. 查找镜像的所有层
|