Docker技术原理之Linux UnionFS(容器镜像)

栏目: 编程工具 · 发布时间: 7年前

内容简介:前面我们讨论了Docker容器实现隔离和资源限制用到的技术Linux namespace 、Linux CGroups,本篇我们来讨论Docker容器镜像用到的技术UnionFS。联合文件系统(Union File System):2004年由纽约州立大学石溪分校开发,它可以把多个目录(也叫分支)内容联合挂载到同一个目录下,而目录的物理位置是分开的。UnionFS允许只读和可读写目录并存,就是说可同时删除和增加内容。UnionFS应用的地方很多,比如在多个磁盘分区上合并不同文件系统的主目录,或把几张CD光

0.前言

前面我们讨论了 Docker 容器实现隔离和资源限制用到的技术Linux namespace 、Linux CGroups,本篇我们来讨论Docker容器镜像用到的技术UnionFS。

1.关于UnionFS

1)什么是UnionFS

联合文件系统(Union File System):2004年由纽约州立大学石溪分校开发,它可以把多个目录(也叫分支)内容联合挂载到同一个目录下,而目录的物理位置是分开的。UnionFS允许只读和可读写目录并存,就是说可同时删除和增加内容。UnionFS应用的地方很多,比如在多个磁盘分区上合并不同文件系统的主目录,或把几张CD光盘合并成一个统一的光盘目录(归档)。另外,具有写时复制(copy-on-write)功能UnionFS可以把只读和可读写文件系统合并在一起,虚拟上允许只读文件系统的修改可以保存到可写文件系统当中。

2)docker的镜像rootfs,和layer的设计

任何程序运行时都会有依赖,无论是开发语言层的依赖库,还是各种系统lib、操作系统等,不同的系统上这些库可能是不一样的,或者有缺失的。为了让容器运行时一致,docker将依赖的操作系统、各种lib依赖整合打包在一起(即镜像),然后容器启动时,作为它的根目录(根文件系统rootfs),使得容器进程的各种依赖调用都在这个根目录里,这样就做到了环境的一致性。

不过,这时你可能已经发现了另一个问题: 难道每开发一个应用,都要重复制作一次rootfs吗(那每次pull/push一个系统岂不疯掉)?

比如,我现在用Debian操作系统的ISO做了一个rootfs,然后又在里面安装了Golang环境,用来部署我的应用A。那么,我的另一个同事在发布他的Golang应用B时,希望能够直接使用我安装过Golang环境的rootfs,而不是重复这个流程,那么本文的主角UnionFS就派上用场了。

Docker镜像的设计中,引入了层(layer)的概念,也就是说,用户制作镜像的每一步操作,都会生成一个层,也就是一个增量rootfs(一个目录),这样应用A和应用B所在的容器共同引用相同的Debian操作系统层、Golang环境层(作为只读层),而各自有各自应用程序层,和可写层。启动容器的时候通过UnionFS把相关的层挂载到一个目录,作为容器的根文件系统。

需要注意的是,rootfs只是一个操作系统所包含的文件、配置和目录,并不包括操作系统内核。这就意味着,如果你的应用程序需要配置内核参数、加载额外的内核模块,以及跟内核进行直接的交互,你就需要注意了: 这些操作和依赖的对象,都是宿主机操作系统的内核,它对于该机器上的所有容器来说是一个“全局变量”,牵一发而动全身。

3)各 Linux 版本的UnionFS不同

由于各种原因(有兴趣的可自行谷歌),Linux各发行版实现的UnionFS各不相同,所以Docker在不同linux发行版中使用的也不同。你可以通过 docker info 来查看docker使用的是哪种,比如:

Storage Driver: overlay2
Storage Driver: aufs

2.举个例子(debain aufs)

1)准备如下目录和文件

$ tree
.
|-- a
|   |-- a.log
|   `-- x.log
`-- b
    |-- b.log
    `-- x.log

2)执行挂载命令

$ mkdir mnt
$ mount -t aufs -o dirs=./a:./b none ./mnt
$ tree ./mnt
./mnt
|-- a.log
|-- b.log
`-- x.log

可以看到被挂载的mnt目录合并了目录a和目录b

3)修改

$ echo test > mnt/x.log 
$ cat mnt/x.log 
test
$ cat a/x.log 
test
$ cat b/x.log

你会发现x.log在a、b目录都存在,在修改后只有a目录生效了,原因是我们在mount aufs命令中,没有指a、b目录的权限, 默认上来说,命令行上第一个(最左边)的目录是可读可写的,后面的全都是只读的 ,所以会出现上面这种情况,你也可以在挂载的时候自己指定权限( mount -t aufs -o dirs=./a=rw:./b=rw none ./mnt ),如果你有兴趣可以去尝试一下,这里就不再演示了。

那么再试一下修改b目录(只读目录)才有的b.log文件试一下呢:

$ echo test > mnt/b.log 
$ cat mnt/b.log 
test
$ cat b/b.log 
$ cat a/b.log 
test

你会发现,b目录下的文件没有被修改,而是在a目录(可读写目录)创建了一个b.log。

4)删除

$ touch b/bb.log
$ rm mnt/a.log
$ rm mnt/bb.log
$ ls -al mnt
-rw-r--r-- 1 root root    0 Sep 19 23:11 b.log
-rw-r--r-- 1 root root    0 Sep 19 23:11 x.log
$ ls -al a
-rw-r--r-- 1 root root    0 Sep 19 23:15 .wh.bb.log
-rw-r--r-- 1 root root    0 Sep 19 23:11 b.log
-rw-r--r-- 1 root root    0 Sep 19 23:11 x.log
$ ls -al b
-rw-r--r-- 1 root root    0 Sep 19 23:11 b.log
-rw-r--r-- 1 root root    0 Sep 19 23:14 bb.log
-rw-r--r-- 1 root root    0 Sep 19 23:11 x.log

你会看到在mnt目录中删除a.log和bb.log后,a目录(可读写)中的a.log真的删除了,而b目录(只读)中的bb.log还在,只是a目录中多个.wh.bb.log这个文件。

一般来说只读目录都会有whiteout的属性,所谓whiteout的意思,就是如果在union中删除的某个文件,实际上是位于一个readonly的目录上,那么,在mount的union这个目录中你将看不到这个文件,但是readonly这个层上我们无法做任何的修改,所以,我们就需要对这个readonly目录里的文件作whiteout。AUFS的whiteout的实现是通过在上层的可写的目录下建立对应的whiteout隐藏文件来实现的。所以上面的 rm mnt/bb.log 操作和 touch a/.wh.bb.log 效果相同。

5)来看一个docker容器

我们一起来执行如下命令:

#启动一个容器
$ docker run -dt golang:1.8.3 /bin/sh
7bcd61b6ccd79a7367cb9872015ad20871be5b44f8bad74d35e045c89b610f34

#通过上面容器id查看挂载点
$ ls /var/lib/docker/image/aufs/layerdb/mounts/7bcd61b6ccd79a7367cb9872015ad20871be5b44f8bad74d35e045c89b610f34 
init-id  mount-id  parent
$ cat /var/lib/docker/image/aufs/layerdb/mounts/7bcd61b6ccd79a7367cb9872015ad20871be5b44f8bad74d35e045c89b610f34/mount-id 
e4e2f1159f512ab74a6afbfeca51413cc3b6a24e86caccf91e40a9d611ce0a9b#

可以看到容器挂载的目录是 e4e2f1159f512ab74a6afbfeca51413cc3b6a24e86caccf91e40a9d611ce0a9b ,那么找到该目录,看看里面的文件都有些什么:

$ ls /var/lib/docker/aufs/mnt/e4e2f1159f512ab74a6afbfeca51413cc3b6a24e86caccf91e40a9d611ce0a9b                   
bin  dev  etc  go  go%  home  lib  lib64  media  mnt  opt  proc  root  run  sbin  srv  sys  tmp  usr  var

一个完整的操作系统根目录出现在里面。我们再来看看这个rootfs联合挂载的层级结构:

# 通过上面找到的mount-id查看aufs的内部id(也叫si)
$ cat /proc/mounts |grep e4e2f1159f512ab74a6afbfeca51413cc3b6a24e86caccf91e40a9d611ce0a9b
none /var/lib/docker/aufs/mnt/e4e2f1159f512ab74a6afbfeca51413cc3b6a24e86caccf91e40a9d611ce0a9b aufs rw,relatime,si=63e50947768841ec,dio,dirperm1 0 0

# 然后通过si查看layer
$ cat /sys/fs/aufs/si_63e50947768841ec/br[0-9]*
/var/lib/docker/aufs/diff/e4e2f1159f512ab74a6afbfeca51413cc3b6a24e86caccf91e40a9d611ce0a9b=rw
/var/lib/docker/aufs/diff/e4e2f1159f512ab74a6afbfeca51413cc3b6a24e86caccf91e40a9d611ce0a9b-init=ro
/var/lib/docker/aufs/diff/974a7e81b15c1eb6ea6c3c66dfb50dfcdf7b99b1e6458e2d3dca9451e2414106=ro
/var/lib/docker/aufs/diff/fd68755d715f47edc7f5ceaa2e5dc6788d4ca36a4d50f51a92a53045cd0b9fb1=ro
/var/lib/docker/aufs/diff/0e1237afa6d0fff72d9fdd5f84ef7275b1a49448d7523d590686131a3b129496=ro
/var/lib/docker/aufs/diff/440bf3d93514f6a35bd99d4ac098d9b709e878146e355c670bd8f1f533c185c5=ro
/var/lib/docker/aufs/diff/57e27832290597d0c5f2dc2ab55d1c53a7aa8a2a40eb6d21d014ad1210b1bb6f=ro
/var/lib/docker/aufs/diff/55da955ef5752f9c3d1810a7b23e0325dd7947a0c0aaecf6ae373f3e33979143=ro

由此我们找到了每个增量rootfs(即layer)所在的目录,那么现在你可以在容器里执行上面UnionFS中实验过的增删改,看看在最终被修改的layer是哪个,这里就不一一实验了。从上面可以看到容器的layer一共有8层:

第一部分 只读层

它是这个容器的rootfs最下面的6层(xxx=ro结尾)。可以看到,它们的挂载方式都是只读的(ro+wh,即readonly+whiteout,上面已经讲过 一般来说只读目录都会有whiteout属性 )。

第二部分 Init层

它是一个以“-init”结尾的层,夹在只读层和读写层之间。Init层是Docker项目单独生成的一个内部层,专门用来存放/etc/hosts、/etc/resolv.conf等信息。需要这样一层的原因是,这些文件本来属于只读的系统镜像层的一部分,但是用户往往需要在启动容器时写入一些指定的值比如hostname,所以就需要在可读写层对它们进行修改。可是,这些修改往往只对当前的容器有效,我们并不希望执行docker commit时,把这些信息连同可读写层一起提交掉。所以,Docker做法是,在修改了这些文件之后,以一个单独的层挂载了出来。而用户执行docker commit只会提交可读写层,所以是不包含这些内容的。

第三部分 可读写层

它是这个容器的rootfs最上面的一层,它的挂载方式为:rw,即read write。在没有写入文件之前,这个目录是空的。而一旦在容器里做了写操作,你修改产生的内容就会以增量的方式出现在这个层中。删除ro-wh层等文件时,也会在rw层创建对应的个whiteout文件,把只读层里的文件“遮挡”起来。最上面这个可读写层的作用,就是专门用来存放你修改rootfs后产生的增量,无论是增删改,都发生在这里。而 当我们使用完了这个被修改过的容器之后,还可以使用docker commit和push指令,保存这个被修改过的可读写层,并上传到Docker Hub上,供其他人使用。而与此同时,原先的只读层里的内容则不会有任何变化 。这,就是增量rootfs的好处。

最终,这8个层都被联合挂载到/var/lib/docker/aufs/mnt目录下,表现为一个完整的操作系统和golang环境供容器使用。

6)性能

IBM的研究中心对Docker的性能给了一份非常不错的性能报告(PDF) 《An Updated Performance Comparison of Virtual Machinesand Linux Containers》

这里扒了两张图下来,顺序读写和随机读写:

Docker技术原理之Linux UnionFS(容器镜像)

顺序读写

Docker技术原理之Linux UnionFS(容器镜像)

随机读写

3.对照Docker源码

1)在启动docker daemon时会根据系统初始化好能使用的unionfs

func NewDaemon(ctx context.Context, config *config.Config, pluginStore *plugin.Store) (daemon *Daemon, err error) {
    //...
    for operatingSystem, gd := range d.graphDrivers {
        layerStores[operatingSystem], err = layer.NewStoreFromOptions(layer.StoreOptions{
            Root:                      config.Root,
            MetadataStorePathTemplate: filepath.Join(config.Root, "image", "%s", "layerdb"),
            GraphDriver:               gd,
            GraphDriverOptions:        config.GraphOptions,
            IDMapping:                 idMapping,
            PluginGetter:              d.PluginStore,
            ExperimentalEnabled:       config.Experimental,
            OS:                        operatingSystem,
        })
    }
    //...
}

func NewStoreFromOptions(options StoreOptions) (Store, error) {
    driver, err := graphdriver.New(options.GraphDriver, options.PluginGetter, graphdriver.Options{
        Root:                options.Root,
        DriverOptions:       options.GraphDriverOptions,
        UIDMaps:             options.IDMapping.UIDs(),
        GIDMaps:             options.IDMapping.GIDs(),
        ExperimentalEnabled: options.ExperimentalEnabled,
    })
    //...
}

// New creates the driver and initializes it at the specified root.
func New(name string, pg plugingetter.PluginGetter, config Options) (Driver, error) {
    //...
    driversMap := scanPriorDrivers(config.Root)
    list := strings.Split(priority, ",")
    logrus.Debugf("[graphdriver] priority list: %v", list)
    for _, name := range list {
        if name == "vfs" {
            // don't use vfs even if there is state present.
            continue
        }
        if _, prior := driversMap[name]; prior {
            driver, err := getBuiltinDriver(name, config.Root, config.DriverOptions, config.UIDMaps, config.GIDMaps)
            //...
            return driver, nil
        }
    }
    //...
}

2)再来看创建容器时,如何使用这个driver的

//docker daemon创建容器api的http handler
router.NewPostRoute("/containers/create", r.postContainersCreate)

//handler 挨着往下扒~
func (s *containerRouter) postContainersCreate(ctx context.Context, w http.ResponseWriter, r *http.Request, vars map[string]string) error {
    //...
    ccr, err := s.backend.ContainerCreate(types.ContainerCreateConfig{
        Name:             name,
        Config:           config,
        HostConfig:       hostConfig,
        NetworkingConfig: networkingConfig,
        AdjustCPUShares:  adjustCPUShares,
    })
    //...
}

func (daemon *Daemon) ContainerCreate(params types.ContainerCreateConfig) (containertypes.ContainerCreateCreatedBody, error) {
    return daemon.containerCreate(params, false)
}

func (daemon *Daemon) containerCreate(params types.ContainerCreateConfig, managed bool) (containertypes.ContainerCreateCreatedBody, error) {
    //...
    container, err := daemon.create(params, managed)
    //...
}

func (daemon *Daemon) create(params types.ContainerCreateConfig, managed bool) (retC *container.Container, retErr error) {
    //...
    //创建init和rw层
    // Set RWLayer for container after mount labels have been set
    rwLayer, err := daemon.imageService.CreateLayer(container, setupInitLayer(daemon.idMapping))
    //...
}

func (i *ImageService) CreateLayer(container *container.Container, initFunc layer.MountInit) (layer.RWLayer, error) {
    var layerID layer.ChainID
    if container.ImageID != "" {
        img, err := i.imageStore.Get(container.ImageID)
        if err != nil {
            return nil, err
        }
        layerID = img.RootFS.ChainID()
    }

    rwLayerOpts := &layer.CreateRWLayerOpts{
        MountLabel: container.MountLabel,
        InitFunc:   initFunc,
        StorageOpt: container.HostConfig.StorageOpt,
    }

    // Indexing by OS is safe here as validation of OS has already been performed in create() (the only
    // caller), and guaranteed non-nil
    //这里的layerStores正式NewDaemon时 layerStores[operatingSystem], err = layer.NewStoreFromOptions(...)这里的这个
    return i.layerStores[container.OS].CreateRWLayer(container.ID, layerID, rwLayerOpts)
}

3)接下来我们来追溯CreateRWLayer的实现:

func (ls *layerStore) CreateRWLayer(name string, parent ChainID, opts *CreateRWLayerOpts) (RWLayer, error) {
    //...
    //这里driver不同环境有不同的实现,下面我们主要来看aufs的实现
    if err = ls.driver.CreateReadWrite(m.mountID, pid, createOpts); err != nil {
        return nil, err
    }
    //
    //这里的saveMount正是save上面2.5里面查看挂载点的mount-id,init-id,parent
    //
    if err = ls.saveMount(m); err != nil {
        return nil, err
    }

    return m.getReference(), nil
}

//我们这里来看aufs的实现
//
// CreateReadWrite creates a layer that is writable for use as a container
// file system.
func (a *Driver) CreateReadWrite(id, parent string, opts *graphdriver.CreateOpts) error {
    return a.Create(id, parent, opts)
}

// Create three folders for each id
// mnt, layers, and diff
func (a *Driver) Create(id, parent string, opts *graphdriver.CreateOpts) error {

    if opts != nil && len(opts.StorageOpt) != 0 {
        return fmt.Errorf("--storage-opt is not supported for aufs")
    }

    if err := a.createDirsFor(id); err != nil {
        return err
    }
    // Write the layers metadata
    f, err := os.Create(path.Join(a.rootPath(), "layers", id))
    if err != nil {
        return err
    }
    defer f.Close()

    if parent != "" {
        ids, err := getParentIDs(a.rootPath(), parent)
        if err != nil {
            return err
        }

        if _, err := fmt.Fprintln(f, parent); err != nil {
            return err
        }
        for _, i := range ids {
            if _, err := fmt.Fprintln(f, i); err != nil {
                return err
            }
        }
    }

    return nil
}

至此,容器镜像的实现我们就讨论的差不多了,有兴趣的朋友,可以在去看看devicemapper、overlay2等驱动,这就不一一展开讨论了。

参考


以上所述就是小编给大家介绍的《Docker技术原理之Linux UnionFS(容器镜像)》,希望对大家有所帮助,如果大家有任何疑问请给我留言,小编会及时回复大家的。在此也非常感谢大家对 码农网 的支持!

查看所有标签

猜你喜欢:

本站部分资源来源于网络,本站转载出于传递更多信息之目的,版权归原作者或者来源机构所有,如转载稿涉及版权问题,请联系我们

Web Data Mining

Web Data Mining

Bing Liu / Springer / 2011-6-26 / CAD 61.50

Web mining aims to discover useful information and knowledge from Web hyperlinks, page contents, and usage data. Although Web mining uses many conventional data mining techniques, it is not purely an ......一起来看看 《Web Data Mining》 这本书的介绍吧!

html转js在线工具
html转js在线工具

html转js在线工具

HEX HSV 转换工具
HEX HSV 转换工具

HEX HSV 互换工具

HSV CMYK 转换工具
HSV CMYK 转换工具

HSV CMYK互换工具