Docker源码分析—层存储

基于Docker 17.05.x

Docker镜像由多层只读镜像层组成,Docker容器实际上就是创建了一层读写层,然后将这层读写层与使用的镜像关联起来。所以实际上镜像和容器在Docker中都是通过“层”来进行存储,每一层对应一个目录,Docker层存储就是用来管理这些“层”以及这些层的元数据

这篇文章根据Docker源码介绍Docker层存储的创建过程

Docker层存储主要包括存储驱动层的元数据存储以及层的元数据映射表3个部分。在Docker源码中,关系如下如:

层的内容存储于/var/lib/docker/driver_name目录中,由存储驱动管理

层的元素据存储于/var/lib/docker/image/driver_name/layerdb目录中,由层的元数据存储管理


层存储结构说明

Docker层存储在源码中使用一个layerStore结构来表示。store字段表示层的元数据存储,driver字段表示具体的存储驱动

存储驱动用一个Driver结构来表示,它实现了graphdriver.Driver接口。Docker支持overlay2,overlay,aufs等多种驱动,我们这里使用overlay2作为存储驱动,因此这个Driver就是overlay2定义的Driver结构。存储驱动管理层的具体内容

层的元数据存储则使用了名为fileMetadataStore的结构来表示,它实现了MetadataStore接口。层的元数据存储管理层的元数据

容器层的元数据通过一个mountedLayer结构来记录,镜像层的元数据则是通过一个roLayer结构来记录。后面会进行介绍

几个目录说明

这里给出几个路径名的简称,下文如有出现可以在这查询:

  • Docker目录:/var/lib/docker
  • 存储驱动目录:/var/lib/docker/overlay2
  • 层的元数据存储目录:/var/lib/docker/image/overlay2/layerdb

由于使用的存储驱动是overlay2,所以后两个目录中将存储驱动名换成了overlay2


创建层存储

层存储记录在Docker daemon的layerStore字段中,在创建daemon时会调用函数NewStoreFromOptions来创建Docker层存储:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
func NewStoreFromOptions(options StoreOptions) (Store, error) {
//1.创建存储驱动
driver, err := graphdriver.New(options.GraphDriver, options.PluginGetter, graphdriver.Options{
Root: options.StorePath,// /var/lib/docker
DriverOptions: options.GraphDriverOptions,//驱动参数选项
UIDMaps: options.UIDMaps,
GIDMaps: options.GIDMaps,
ExperimentalEnabled: options.ExperimentalEnabled,
})
...

//2.根据元数据存储路径模板字符串和1创建的存储驱动来创建层的元数据存储
// options.MetadataStorePathTemplate:/var/lib/docker/image/%s/layerdb
fms, err := NewFSMetadataStore(fmt.Sprintf(options.MetadataStorePathTemplate, driver))
if err != nil {
return nil, err
}

//3.根据1,2调用NewStoreFromGrapDriver创建层存储
return NewStoreFromGraphDriver(fms, driver)
}

这个函数根据StoreOptions结构的参数创建层存储:

1
2
3
4
5
6
7
8
9
10
type StoreOptions struct {
StorePath string //存储路径:/var/lib/docker
MetadataStorePathTemplate string //元数据存储路径模板:/var/lib/docker/image/%s/layerdb
GraphDriver string //存储驱动:overlay2、overlay、aufs、devicemmapper...
GraphDriverOptions []string //存储驱动选项
UIDMaps []idtools.IDMap //
GIDMaps []idtools.IDMap //
PluginGetter plugingetter.PluginGetter
ExperimentalEnabled bool
}

层存储的创建主要分3步:

  1. 调用graphdriver.New()函数创建存储驱动driver
  2. 调用NewFSMetadataStore()函数创建层的元数据存储fms
  3. 根据driver和fms调用函数NewStoreFromGraphDriver函数创建层存储

这里提一下,在2.创建层的元数据存储时,传入了存储驱动对象作为fmt.Sprintf()的第2个参数,在go语言中,如果一个类定义了String()函数,则这个类作为输出时,会调用这个String()函数。对于overlay2,其String()函数返回驱动名

创建存储驱动

在分析graphdriver.New()函数之前,首先需要介绍一个映射表—已注册存储驱动映射表

1
2
3
4
var (
drivers map[string]InitFunc
...
)

这个映射表的key是存储驱动的名称,value是存储驱动的初始化函数。这个映射表是一个全局变量,定义在driver.go

这个表记录了已经注册的存储驱动。存储驱动的注册通过函数Register来完成,就是向这个映射表中添加表项:

1
2
3
4
5
6
7
8
func Register(name string, initFunc InitFunc) error {
if _, exists := drivers[name]; exists {
return fmt.Errorf("Name already registered %s", name)
}
drivers[name] = initFunc

return nil
}

在Docker daemon启动之前,所有Docker内置驱动就已经完成了注册。每个存储驱动定义了自己的init()函数,在go语言中,init()函数会在main函数之前执行:

1
2
3
func init() {
graphdriver.Register(driverName, Init)
}

因此,在daemon启动前,存储驱动通过init函数调用Register(),将自己的名字和驱动初始化函数创建为表项添加到已注册驱动映射表drivers中

在了解了这之后,我们再来看看存储驱动的创建函数graphdriver.New():

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
func New(name string, pg plugingetter.PluginGetter, config Options) (Driver, error) {
...

// 1.将所有在/var/lib/docker下存在目录的已注册驱动添加到映射表driversMap
driversMap := scanPriorDrivers(config.Root)
/*
* 2.按优先级遍历驱动,找到第一个在/var/lib/docker目录下存在目录的驱动
*/
for _, name := range priority {
if name == "vfs" {
// don't use vfs even if there is state present.
continue
}
//如果这个驱动在/var/lib/docker下有对应目录,即存在存储驱动目录
if _, prior := driversMap[name]; prior {
driver, err := getBuiltinDriver(name, config.Root, config.DriverOptions, config.UIDMaps, config.GIDMaps)
if err != nil {
logrus.Errorf("[graphdriver] prior storage driver %s failed: %s", name, err)
return nil, err
}
/*
如果不止一个驱动存在存储驱动目录,则返回错误,
输出可以使用的驱动,提示用户指定某一存储驱动
*/
if len(driversMap)-1 > 0 {
var driversSlice []string
for name := range driversMap {
driversSlice = append(driversSlice, name)
}

return nil, fmt.Errorf("%s contains several valid graphdrivers: %s; Please cleanup or explicitly choose storage driver (-s <DRIVER>)", config.Root, strings.Join(driversSlice, ", "))
}

logrus.Infof("[graphdriver] using prior storage driver: %s", name)
return driver, nil
}
}

/*
* 2.如果所有已注册驱动都没有存储驱动目录,一般第一次启动daemon时会如此。
* 则按优先级遍历驱动,找到第一个支持的存储驱动
*/
for _, name := range priority {
driver, err := getBuiltinDriver(name, config.Root, config.DriverOptions, config.UIDMaps, config.GIDMaps)
if err != nil {
if isDriverNotSupported(err) {
continue
}
return nil, err
}
return driver, nil
}

/*
* 3.如果驱动优先级数组中没有支持的驱动,则遍历整个驱动注册表,查找支持的驱动
* 意思应该是除了内置的驱动,还可能会有其它驱动注册
*/
for name, initFunc := range drivers {
driver, err := initFunc(filepath.Join(config.Root, name), config.DriverOptions, config.UIDMaps, config.GIDMaps)
if err != nil {
if isDriverNotSupported(err) {
continue
}
return nil, err
}
return driver, nil
}
return nil, fmt.Errorf("No supported storage backend found")
}

这个函数主要是根据注册驱动表、驱动优先级字符串数组priority以及存在驱动目录的映射表driversMap来做驱动选择。相应的选择逻辑已经注释在代码中。代码涉及到3个遍历,每个遍历逻辑都会返回最终我们要创建的存储驱动。而最终负责驱动创建的函数则是由驱动注册时注册的初始化函数initFunc来完成的,overlay2注册的initFunc函数定义如下(省略了错误处理):

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
func Init(home string, options []string, uidMaps, gidMaps []idtools.IDMap) (graphdriver.Driver, error) {
//1.参数解析
opts, err := parseOptions(options)
//2.调用modprobe overlay试加载内核overlay模块,判断内核是否支持OverlayFS
err := supportsOverlay();
//3.检查内核版本,内核4.0后OverlayFS才开始支持overlay2
v, err := kernel.GetKernelVersion()
//4.获取后端文件系统,存入全局变量backingFs中
//home:/var/lib/docker/overlay2
fsMagic, err := graphdriver.GetFSMagic(home)
if err != nil {
return nil, err
}
if fsName, ok := graphdriver.FsNames[fsMagic]; ok {
backingFs = fsName
}
// 5.检查后端文件系统是否是支持overlay2的文件系统
switch fsMagic {
case graphdriver.FsMagicBtrfs, graphdriver.FsMagicAufs, graphdriver.FsMagicZfs, graphdriver.FsMagicOverlay, graphdriver.FsMagicEcryptfs:
logrus.Errorf("'overlay2' is not supported over %s", backingFs)
return nil, graphdriver.ErrIncompatibleFS
}
// 6.获取rootUID和rootGID
rootUID, rootGID, err := idtools.GetRootUIDGID(uidMaps, gidMaps)
// 7.创建overlay2层标识符目录/var/lib/docker/overlay2/l
err := idtools.MkdirAllAs(path.Join(home, linkDir), 0700, rootUID, rootGID);

...

//8.构造overlay2存储驱动结构
d := &Driver{
home: home,// /var/lib/docker/overlay2
uidMaps: uidMaps,
gidMaps: gidMaps,
ctr: graphdriver.NewRefCounter(graphdriver.NewFsChecker(graphdriver.FsMagicOverlay)),
supportsDType: supportsDType,
locker: locker.New(),
}

d.naiveDiff = graphdriver.NewNaiveDiffDriver(d, uidMaps, gidMaps)

//9.如果后端文件系统是xfs则支持磁盘配额
if backingFs == "xfs" {
// Try to enable project quota support over xfs.
if d.quotaCtl, err = quota.NewControl(home); err == nil {
projectQuotaSupported = true
}
}
...

return d, nil
}

具体驱动创建的步骤已经注释在代码中。到此,存储驱动的创建总算是大功告成啦。今后,层存储就可以透过driver字段索引到存储驱动,然后调用存储驱动实现的一系列函数来管理层。(如调用层创建函数来创建容器层或镜像层)

创建层的元数据存储

层的元数据存储通过函数NewFSMetadataStore()来创建:

1
2
3
4
5
6
7
8
9
10
func NewFSMetadataStore(root string) (MetadataStore, error) {
//1.创建目录/var/lib/docker/image/overlay2/layerdb
if err := os.MkdirAll(root, 0700); err != nil {
return nil, err
}
//2.构建一个fileMetadataStore结构,root字段存储了元数据的根路径
return &fileMetadataStore{
root: root,
}, nil
}

层的元数据存储的创建十分简单,主要是创建元数据存储的根目录layerdb,然后构造并返回一个fileMetaStore对象

虽然只是返回了一个构造出的层的元数据存储对象,但是这个对象实现了很多层的元数据操作的方法,如元数据的读取,后面将会介绍

创建层存储

现在回到层存储创建的最后一步,即根据存储驱动和层的元数据存储创建层存储:

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
func NewStoreFromGraphDriver(store MetadataStore, driver graphdriver.Driver) (Store, error) {
caps := graphdriver.Capabilities{}
//go语言中的断言,如果存储驱动能转还成一个
//graphdriver.CapabilityDriver,则更新caps
//caps主要用于设置层存储结构的useTarSplit字段
if capDriver, ok := driver.(graphdriver.CapabilityDriver); ok {
caps = capDriver.Capabilities()
}

//1.构造层存储结构
ls := &layerStore{
store: store,//层的元数据存储
driver: driver,//存储驱动
layerMap: map[ChainID]*roLayer{},//只读层映射表
mounts: map[string]*mountedLayer{},//读写层映射表
useTarSplit: !caps.ReproducesExactDiffs,
}

//2.到元数据根目录/var/lib/docker/image/overlay2/layerdb下读取:
// 1)目录sha256:这个目录下存储了镜像层的元数据
// 2)目录mounts:这个目录下存储了容器层的元数据
// 对于镜像层,返回一个字符串数组ids,字符串形式为"sha256:目录名"
// 对于容器层,返回一个字符串数组mounts,字符串形式为”目录名“
ids, mounts, err := store.List()
if err != nil {
return nil, err
}

//3.遍历所有镜像层,调用loadLayer()函数加载镜像层的元数据
for _, id := range ids {
l, err := ls.loadLayer(id)
if err != nil {
logrus.Debugf("Failed to load layer %s: %s", id, err)
continue
}
if l.parent != nil {
l.parent.referenceCount++
}
}

//4.遍历所有容器层,调用loadMount()函数加载容器层的元数据
for _, mount := range mounts {
if err := ls.loadMount(mount); err != nil {
logrus.Debugf("Failed to load mount %s: %s", mount, err)
}
}

return ls, nil
}

这个函数主要完成4个操作:

  1. 根据存储驱动以及层的元数据存储构造层存储结构
  2. 调用Store.List()函数获取镜像层和容器层的元数据目录数组
  3. 遍历每个镜像层的元数据目录,调用loadLayer()加载镜像层元数据
  4. 遍历每个容器层的元数据目录,调用loadMount()加载容器层元数据

加载镜像层元数据

每层镜像层的元数据通过一个roLayer结构来记录,加载元数据就是到每个镜像层的元数据目录下读取镜像层的元数据文件,然后构造一个roLayer结构,并将镜像层的元数据添加到层存储的镜像层元数据映射表中,由函数loadLayer()完成:

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
func (ls *layerStore) loadLayer(layer ChainID) (*roLayer, error) {
//1.如果镜像层元数据映射表中已经存在,则直接获取并返回
cl, ok := ls.layerMap[layer]
if ok {
return cl, nil
}

/*
2.对layer:shaXX:ID
到目录/var/lib/docker/image/overlay2/layerdb/shaXX/ID/下读取
diff,size,parent,cache-id等文件
读取操作需要用到层的元数据存储结构实现的一系列操作来完成
*/
diff, err := ls.store.GetDiffID(layer)
if err != nil {
return nil, fmt.Errorf("failed to get diff id for %s: %s", layer, err)
}

size, err := ls.store.GetSize(layer)
if err != nil {
return nil, fmt.Errorf("failed to get size for %s: %s", layer, err)
}

cacheID, err := ls.store.GetCacheID(layer)
if err != nil {
return nil, fmt.Errorf("failed to get cache id for %s: %s", layer, err)
}

parent, err := ls.store.GetParent(layer)
if err != nil {
return nil, fmt.Errorf("failed to get parent for %s: %s", layer, err)
}

descriptor, err := ls.store.GetDescriptor(layer)
if err != nil {
return nil, fmt.Errorf("failed to get descriptor for %s: %s", layer, err)
}

//3.构造镜像层元数据对象
cl = &roLayer{
chainID: layer, //shaXX:该层在元数据目录下对应的目录名
diffID: diff, //digest ID
size: size, //层的大小
cacheID: cacheID, //存储驱动目录下该层镜像层的目录名(缓存ID)
layerStore: ls, //指向层存储
references: map[Layer]struct{}{},
descriptor: descriptor,
}

//如果存在父层,则继续加载父层
if parent != "" {
p, err := ls.loadLayer(parent)
if err != nil {
return nil, err
}
//设置元数据的parent字段
cl.parent = p
}

//4.将该层镜像层元数据添加到 层存储的镜像层元数据映射表中
ls.layerMap[cl.chainID] = cl

return cl, nil
}

加载容器层元数据

每层容器层的元数据通过一个mountedLayer结构来记录,加载容器层元数据就是到每个容器层的元数据目录下读取容器层的元数据文件,然后构造一个mountedLayer结构,并将容器层的元数据添加到层存储的容器层元数据映射表中,由函数loadMount()完成:

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
func (ls *layerStore) loadMount(mount string) error {
//1.如果容器层元数据映射表中已经存在,则直接获取并返回
if _, ok := ls.mounts[mount]; ok {
return nil
}

/*
2.对mount:ID
到目录/var/lib/docker/image/overlay2/layerdb/mounts/ID/下读取
init-id,mount-id,parent等文件
读取操作需要用到层的元数据存储结构实现的一系列操作来完成
*/

mountID, err := ls.store.GetMountID(mount)
if err != nil {
return err
}

initID, err := ls.store.GetInitID(mount)
if err != nil {
return err
}

parent, err := ls.store.GetMountParent(mount)
if err != nil {
return err
}

//3.构造容器层元数据对象
ml := &mountedLayer{
name: mount,
mountID: mountID,
initID: initID,
layerStore: ls,
references: map[RWLayer]*referencedRWLayer{},
}

//如果存在父层,则继续加载父层
if parent != "" {
p, err := ls.loadLayer(parent)
if err != nil {
return err
}
ml.parent = p

p.referenceCount++
}

//4.将该层容器层元数据添加到 层存储的容器层元数据映射表中
ls.mounts[ml.name] = ml

return nil
}

容器层元数据的加载步骤实际上和镜像层的一样,只是镜像层元数据和容器层元数据所在的目录不同


步骤总结

总结一下,层存储的创建可以概括为4个步骤:

  1. 创建存储驱动
  2. 创建层的元数据存储
  3. 构建层存储的结构对象
  4. 遍历层的元数据目录,加载层的元数据信息,并存入层存储的两个层元数据映射表中
文章目录
  1. 1. 层存储结构说明
    1. 1.1. 几个目录说明
  2. 2. 创建层存储
    1. 2.1. 创建存储驱动
    2. 2.2. 创建层的元数据存储
    3. 2.3. 创建层存储
      1. 2.3.1. 加载镜像层元数据
      2. 2.3.2. 加载容器层元数据
  3. 3. 步骤总结
|