专栏首页和baron一起学习TKE[docker](六)docker -- 存储管理
原创

[docker](六)docker -- 存储管理

Docker镜像元数据管理

Docker镜像在设计上将镜像元数据与镜像文件的存储完全隔离开了。与Docker像管理相关的概念,包括repository, image, layer。 Docker在管理镜像层元数据时,采用的也正是从上至下repository, image, layer三个层次。由于Docker以分层的形式存储镜像,所以repository与image这两类元数据并无物理上的镜像文件与之对应,而layer这种元数据则存在物理上的镜像层文件与之对应。

1、repository元数据

repository即由具有某个功能的Docker镜像的所有迭代版本构成的镜像库。repository在本地的持久化文件存放于/var/lib/docker/image/some-graph一river/repositories.json中,结构如下所示:

repository结构示意图.png

文件中存储了所有repository的名字(如busybox),每个repository下所有版本镜像的名字和tag(如busybox:latest)以及对应的镜像ID。而referenceStore的作用便是解析不同格式的repository名字,并管理repository与镜像ID的映射关系。

2、image元数据

image元数据包括了镜像架构(如amd64)、操作系统(如Linux)、镜像默认配置、构建该镜像的容器ID和配置、创建时间、创建该镜像的Docker版本、构建镜像的历史信息以及rootfs组成。其中构建镜像的历史信息和rootfs组成部分除了具有描述镜像的作用外,还将镜像和构成该镜像的镜像层关联了起来。Docker会根据历史信息和rootfs中的diff_ids计算出构成该镜像的镜像层的存储索引chainID.

imageStore则管理镜像ID与镜像元数据之间的映射关系以及元数据的持久化操作,持久化文件位于/var/lib/docker/image/graph_ driver/imagedb/content/sha256/[image-id〕中。

3、layer元数据

用户在Docker宿主机上下载了某个镜像层之后,Docker会在宿主机上基于镜像层文件包和image元数据,构建本地的layer元数据,包括diff, parent, size等。而当Docker将在宿主机上产生新的镜像层上传到registry时,与新镜像层相关的宿主机上的元数据也不会与镜像层一块打包上传。

Docker中定义了Layer和RWLayper两种接口,分别用来定义只读层和可读写层的一些操作,又定义了roLayer和mountedLayer,分别实现了上述两种接口。其中,roLayer用于描述不可改变的镜像层,mountedLayer用于描述可读写的容器层。

具体来说,roLayer存储的内容主要有索引该镜像层的chainID、该镜像层的校验码difflD、父镜像层parent, graphdriver存储当前镜像层文件的cacheID、该镜像层的大小size等内容。这些元数据的持久化文件位于/var/lib/docker/image /graph_ driver/layerdb/sha256/chainID/文件夹下,其中,diffID和size可以通过一个该镜像层包计算出来;chainID和父镜像层parent需要从所属image元数据中计算得到;而cacheID是在当前Docker宿主机上随机生成的一个uuid,在当前宿主机上与该镜像层一一对应,用于标示并索引graphdriver中的镜像层文件。

mountedLayer存储的内容主要为索引某个容器的可读写层(也叫容器层)的ID(也对应容器的ID )、容器init层在graphdriver中的ID—initID、读写层在graphdriver中的ID mountID以及容器层的父层镜像的chainID—parent。持久化文件位于/var/lib/docker/image/graph_ driver/layerdb/mounts/container_ id/路径下。

Docker存储驱动

为了支持镜像分层与写时复制机制这些特性,Docker提供了存储驱动的接口。存储驱动根据操作系统底层的支持提供了针对某种文件系统的初始化操作以及对镜像层的增、删、改、查和差异比较等操作。目前存储系统的接口已经有aufs, btrfs, devicemapper, vfs,overlay, zfs这6种具体实现,其中vfs不支持写时复制,是为使用volume(Docker提供的文件管理方式,在3.7节将会具体介绍)提供的存储驱动,仅仅做了简单的文件挂载操作;剩下5种存储驱动支持写时复制,它们的实现有一定的相似之处。

1、存储驱动的功能与管理

  • 存储驱动接口的定义

GraphDriver中主要定义了Driver和ProtoDriver两个接口,所有的存储驱动通过实现Driver接口提供相应的功能,而ProtoDriver接口则负责定义其中的基本功能。这些基本功能包括如下8种:

 - String()返回一个代表这个驱动的字符串,通常是这个驱动的名字。
 - Create()创建一个新的镜像层,需要调用者传进一个唯一的ID和所需的父镜像的ID .
 - Remove()尝试根据指定的ID删除一个层。
 - Get()返回指定ID的层的挂载点的绝对路径。
 - Put()释放一个层使用的资源,比如卸载一个已经挂载的层。
 - Exists()查询指定的ID对应的层是否存在。
 - Status()返回这个驱动的状态,这个状态用一些键值对表示。
 - Cleanup()释放由这个驱动管理的所有资源,比如卸载所有的层。

而正常的Driver接口实现则通过包含一个ProtoDriver的匿名对象实现上述8个基本功能,除此之外,Driver还定义了其他4个方法,用于对数据层之间的差异(diff)进行管理。

 - Diff()将指定ID的层相对父镜像层改动的文件打包并返回。
 - Changes()返回指定镜像层与父镜像层之间的差异列表。
 - ApplyDiff()从差异文件包中提取差异列表,并应用到指定ID的层与父镜像层,返回新镜像层的大小。
 - DiffSize()计算指定ID层与其父镜像层的差异,并返回差异相对于基础文件系统的大小。

综上所述,Docker中的任何存储驱动都需要实现上述。river接口。当我们在Docker中添加一个新的存储驱动的时候,可以实现。river的全部12个方法,或是实现ProtoDrive:的8个方法再使用naiveDiffDriver进一步封装。不管那种做法,只要集成了基本存储操作和差异操作的实现,一个存储驱动就算开发完成了。

  • 存储驱动的创建过程

首先,前面提到的各类存储驱动都需要定义一个属于自己的初始化过程,并且在初始化过程中向GraphDriver注册自己。GraphDriver维护了一个drivers列表,提供从驱动名到驱动初始化方法的映射,这用于将来根据驱动名称查找对应驱动的初始化方法。

而所谓的注册过程,则是存储驱动通过调用GraphDriver提供自己的名字和对应的初始化函数,这样GraphDriver就可以将驱动名和这个初始化方法保存到drivers。

当需要创建一个存储驱动时(比如aufs的驱动),GraphDriver会根据名字从drivers中查找到这个驱动对应的初始化方法,然后调用这个初始化函数得到对应的。river对象。这个创建过程如下所示:

 - (1)依次检查环境变量DOCKER_ DRIVER和变量DefaultDriver是否提供了合法的驱动名字(比如aufs ),其中DefaultDriver是从Docker daemon启动时的一storage-driver或者一s参数中读出的。获知了驱动名称后,GraphDriver就调用对应的初始化方法,创建一个对应的Driver对象实体。
 - (2)若环境变量和配置默认是空的,则GraphDriver会从驱动的优先级列表中查找一个可用的驱动。“可用”包含两个意思:第一,这个驱动曾经注册过自己;第二,这个驱动对应的文件系统被操作系统支持(这个支持性检查会在该驱动的初始化过程中执行)。在Linux平台下,目前优先级列表依次包含了这些驱动:aufs, btrfs, zfs, devicemapper, overlay和vfs.
 - (3)如果在上述6种驱动中查找不到可用的,则GrapthDriver会查找所用注册过的驱动,找到第一个注册过的、可用的驱动并返回。不过这一设计只是为了将来的可扩展性而存在,用于查找自定义的存储驱动插件,现在有且仅有的上述6种驱动一定会注册自己。

2、常用存储驱动分析

aufs

aufs(advanced multi layered unification filesystem)是一种支持联合挂载的文件系统,简单来说就是支持将不同目录挂载到同一个目录下,这些挂载操作对用户来说是透明的,用户在操作该目录时并不会觉得与其他目录有什么不同。这些目录的挂载是分层次的,通常来说最上层是可读写层,下层是只读层。所以,aufs的每一层都是一个普通文件系统。

当需要读取一个文件A时,会从最顶层的读写层开始向下寻找,本层没有,则根据层之间的关系到下一层开始找,直到找到第一个文件A并打开它。

当需要写入一个文件A时,如果这个文件不存在,则在读写层新建一个;否则像上面的过程一样从顶层开始查找,直到找到最近的文件A , aufs会把这个文件复制到读写层进行修改。

由此可以看出,在第一次修改某个已有文件时,如果这个文件很大,即使只要修改几个字节,也会产生巨大的磁盘开销。

当需要删除一个文件时,如果这个文件仅仅存在于读写层中,则可以直接删除这个文件;否则就需要先删除它在读写层中的备份,再在读写层中创建一个whiteout文件来标志这个文件不存在,而不是真正删除底层的文件。

当新建一个文件时,如果这个文件在读写层存在对应的whiteout文件,则先将whiteout文件删除再新建。否则直接在读写层新建即可。

那么镜像文件在本地存放在哪里呢?

我们知道Docker的工作目录是//var/lib/docker,查看该目录下的内容可以看到如下文件。

   /var/lib/docker# ls
   aufs/  containers/  image/  network/  tmp/  trust/  volumes/
   
   /var/lib/docker/aufs# ls
   diff/  layers/  mnt/

其中:mnt为aufs的挂载目录,diff为实际的数据来源,包括只读层和可读写层,所有这些层最终一起被挂载在mnt上的目录,layers下为与每层依赖有关的层描述文件。

最初,mnt和layers都是空目录,文件数据都在diff目录下。一个Docker容器创建与启动的过程中,会在/var/lib/docker/aufs下面新建出对应的文件和目录。Docker镜像管理部分与存储驱动在设计上完全分离,镜像层或者容器层在存储驱动中拥有一个新的标示ID,在镜像层(roLayer)中称为cacheID,容器层(mountedLayer)中为mountID。在Unix环境下,mountID是随机生成的并保存在mountedLayer的元数据mountID中,持久化在image/aufs/layerdb/mounts/container-id/mount-id中。下面以mountID为例。创建一个新镜像层的步骤如下:

  • (1)分别在mnt和diff目录下创建与该层的mountID同名的子文件夹。
  • (2)在layers目录下创建与该层的mountID同名的文件,用来记录该层所依赖的所有的其他层。
  • (3)如果参数中的parent项不为空(这里由于是创建容器,parent就是镜像的最上层),说明该层依赖于其他的层。GraphDriver就需要将parent的mountID写入到该层在layers下对应mountID的文件里。然后GraphDriver还需要在layers目录下读取与上述parent同mountID的文件,将parent层的所有依赖层也复制到这个新创建层对应的层描述文件中,这样这个文件才记录了该层的所有依赖。创建成功后,这个新创建的层的描述文件如下:
     $cat /var/lib/docker/aufs/layers/<mountID>
     //父层的ID
     fOCe1c53a3d1ed981cf45c92c14711ec3a9929943c2e06128fb62281426c2ob6
     //接下来3条是父层的描述文件的全部内容
     4fdd0019e2153bc182860fa260495e9cb468b8e7bbe1e0d564fd775o869f9095
     40437055b94701b71abefb1e48b6ae585724533b64052f7d72face83fe3b95cd
     ff3601714f3169317ed0563ff3g3f282fbb6ac9a5413d753b7oda72881d74975

随后GraphDriver会将diff中属于容器镜像的所有层目录以只读方式挂载到mnt下,然后在diff中生成一个以当前容器对应的<mount工。>-init命名的文件夹作为最后一层只读层,这个文件夹用于挂载并重新生成如下代码段所列的文件:

mnt文件目录.png

接下来会在diff中生成一个以容器对应mountID为名的可读写目录,也挂载到mnt目录下。所以,将来用户在容器中新建文件就会出现在mnt下以mountID为名的目录下,而该层对应的实际内容则保存在diff目录下。以aufs为例的话,Docker镜像的主要存储目录和作用可以通过图3-13来解释:

Docker镜像在aufs文件系统的组织形式.png

最后,当我们用docker commit把容器提交成镜像后,就会在diff目录下生成一个新的cacheID命名的文件夹,存放了最新的差异变化文件,这时一个新的镜像层就诞生了。而原来的以mountID为名的文件夹已然存在,直至对应容器被删除。

device-mapper

Device Mapper是Linux 2.6内核中提供的一种从逻辑设备到物理设备的映射框架机制,在该机制下,用户可以很方便地根据自己的需要制定实现存储资源的管理策略.

简单来说,Device Mapper包括3个概念:映射设备、映射表和目标设备,如图3-14所示。映射设备是内核向外提供的逻辑设备。一个映射设备通过一个映射表与多个目标设备映射起来,映射表包含了多个多元组,每个多元组记录了这个映射设备的起始地址、范围与一个目标设备的地址偏移量的映射关系。目标设备可以是一个物理设备,也可以是一个映射设备,这个映射设备可以继续向下迭代。一个映射设备最终通过一棵映射树映射到物理设备上。Device Mapper本质功能就是根据映射关系描述IO处理规则,当映射设备接收到IO请求的时候,这个IO请求会根据映射表逐级转发,直到这个请求最终传到最底层的物理设备上。

device-mapper机制示意图.png

Docker下面的devicemapper存储驱动是使用Device Mapper的精简配置(thin-provisioning)和快照(snapshotting)功能实现镜像的分层。这个模块使用了两个块设备(一个用于存储数据,另一个用于存储元数据),并将其构建成一个资源池(thin pool )用以创建其他存储镜像的块设备。数据区为生成其他块设备提供资源,元信息存储了虚拟设备和物理设备的映射关系。Copy on Write发生在块存储级别。devicemapper在构建一个资源池后,会先创建一个有文件系统的基础设备,再通过从已有设备创建快照的方式创建新的设备,这些新创建的块设备在写人内容之前并不会分配资源。所有的容器层和镜像层都有自己的块设备,均是通过从其父镜像层创建快照的方式来创建(没有父镜像层的层从基础设备创建快照)。层次结构如图3-15所示。

devicemapper镜像层结构示意图.png

与aufs一样,如果Docker使用过devicemapper存储驱动,在//var/lib/docker/下会创建devicemapper/以及image/devicemapper目录。同样,image/devicemapper也是存储了镜像和逻辑镜像层的元数据信息。

最终,具体的文件都会存储在//var/lib/docker/devicemapper文件夹下,这个文件夹下有3个子文件夹,其中mnt为设备挂载目录,devicemapper下存储了loop-lvm模式下的两个稀疏文件,metadata下存储了每个块设备驱动层的元数据信息。

overlay

OverlayFS是一种新型联合文件系统(union filesystem ),它允许用户将一个文件系统与另一个文件系统重叠( overlay ),在上层的文件系统中记录更改,而下层的文件系统保持不变。Docker的overlay存储驱动便建立在OverlayFS的基础上。

OverlayFS主要使用4类目录来完成工作,被联合挂载的两个目录lower和upper,作为统一视图联合挂载点的merged目录,还有作为辅助功能的work目录。作为upper和lower被联合挂载的统一视图,当同一路径的两个文件分别存在两个目录中时,位于上层目录upper中的文件会屏蔽位于下层lower中的文件,如果是同路径的文件夹,那么下层目录中的文件和文件夹会被合并到上层。

在了解了OverlayFS的原理后,下面介绍一下Docker的overlay存储驱动是如何实现的。

首先请读者直观感受一下overlay的目录结构。overlay存储驱动的工作目录是/var/lib/docker/overlay/。

overlay文件结构.png

可以清楚地看到overlay目录下面以UUID命名的文件夹下的目录结构分为两种,一种是只有root目录的,另一种则有3个文件夹和一个文件lower-id。根据QUID中是否带一init后缀以及UUID名,很容易能判断出来,前者是镜像层的目录,后者是容器层(包括init.)的目录。前面介绍OverlayFS原理是将一层目录重叠于另一层目录之上,也就是说OverlayFS文件系统只会涉及两个目录,而Docker镜像却可能有许多层。为了解决这种不对应的情况,overlay存储驱动在存储镜像层的时候,会把父镜像层的内容“复制”到当前层,然后再写入当前层,为了节省存储空间,在“复制”的过程中,普通文件是采用硬链接的方式链接到父镜像层对应文件,其他类型的文件或文件夹则是按照原来的内容重新创建。所以上层镜像层拥有其依赖镜像层的所有文件,而最上面的镜像层则拥有了整个镜像的文件系统,这也是为什么镜像层对应的目录中只有一个root文件夹。

至于另一种目录结构,upper对应上层目录,merged对应挂载点目录,work对应辅助工作(比如copy up操作需要用到)目录,但lower-id却是一个文件,里面记录了该容器层所属容器的镜像最上面镜像层的cache-id,在本书上面的实验环境中,lower-id内记录的是e43c26d23b<省略部分…>acca, Docker使用该cache-id找到所依赖镜像层的root目录作为下层目录。

在准备最上层可读写容器层的时候,会将init层的lower-id与uppe:目录中的内容全部复制到容器层中。最后为容器准备rootfs时,将对应的4种文件夹联合挂载即可。

原创声明,本文系作者授权云+社区发表,未经许可,不得转载。

如有侵权,请联系 yunjia_community@tencent.com 删除。

我来说两句

0 条评论
登录 后参与评论

相关文章

  • [docker](五)docker -- 镜像管理

    Docker镜像是一个只读的Docker容器模板,含有启动Docker容器所需的文件系统结构及其内容,因此是启动一个Docker容器的基础。Docker镜像的文...

    baron
  • [docker](九)docker -- 容器安全

    Docker目前已经在安全方面做了一定的工作,包括Docker daemon在以TCP形式提供服务的同时使用传输层安全协议;在构建和使用镜像时会验证镜像的签名证...

    baron
  • 《TKE学习》TKE简介(一)

    腾讯云容器服务(Tencent Kubernetes Engine,TKE)是高度可扩展的高性能容器管理服务,您可以在托管的云服务器实例集群上轻松运行应用程序。...

    baron
  • CoreOS那些事之Rkt容器尝鲜(下) 转

    2015年是各种容器技术与名词扎堆的一年,Docker的出现使得“应用容器”的实施变得易如反掌的同时,也带动了它的许多竞争者。其中一个比较有趣的看点就在于“容器...

    bdcn
  • Docker的三个概念

    KEVINGUO_CN
  • 跟我一起学docker(三)--镜像的常用操作

    IT故事会
  • Jib使用小结(Maven插件版)

    版权声明:本文为博主原创文章,遵循 CC 4.0 BY-SA 版权协议,转载请附上原文出处链接和本声明。

    程序员欣宸
  • Docker--DockerFile与镜像

        Dockerfile是一个用于引导docker镜像生成过程的文件,遵循其特定的语法,我们便可以创建一个自己的镜像。

    匠心Java
  • 如何有效地对Docker的镜像进行管理?

    容器的存储空间如何提供? 前段时间,笔者看到一篇文章,题目是“容器就是Linux”,写的不错。容器说简单点就是容器级别的虚拟化,在一个Kernel Space...

    魏新宇
  • Docker镜像的目录存储讲解

    我们成功安装完docker后,执行命令行sudo docker run hello-world, 如果是第一次执行,则会从远程拉取hello-world的镜像到...

    Jerry Wang

扫码关注云+社区

领取腾讯云代金券