内容简介:前面我们讨论了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》 。
这里扒了两张图下来,顺序读写和随机读写:
顺序读写
随机读写
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(容器镜像)》,希望对大家有所帮助,如果大家有任何疑问请给我留言,小编会及时回复大家的。在此也非常感谢大家对 码农网 的支持!
猜你喜欢:本站部分资源来源于网络,本站转载出于传递更多信息之目的,版权归原作者或者来源机构所有,如转载稿涉及版权问题,请联系我们。