【Docker】容器存储管理综述【容器持久化数据管理+应用场景+与传统存储的对比】

1. 容器存储背景

1.1、容器 VS 虚拟机

容器技术是目前云计算中不可或缺的一部分,相比于传统虚拟机而言,容器技术在操作系统级别为虚拟化提供了一种更轻量化的选择。传统虚拟机(VM)在虚拟化硬件和主机操作系统之上通过hypervisor管理层运行客户端操作系统的完整副本,运行过程占用大量空间,限制了单台物理主机上可部署的虚拟机数量,而启动时间长使得虚拟机托管短生命周期的应用程序代价过高。下图为虚拟机和Docker容器的系统层次结构对比图。

【Docker】容器存储管理综述【容器持久化数据管理+应用场景+与传统存储的对比】

 基于容器的虚拟化技术通过在应用程序之间共享主机操作系统的库和内核来解决这些问题,它不再需要运行整个臃肿的客户端操作系统,而是通过命名空间namespace机制使每个应用程序都运行在一个独立的命名空间,也就是容器中。简单理解来看,容器就是通过namespace机制打包隔离运行的一组“容器化”进程。运行在主机操作系统之上的后台进程Docker守护进程则取代了Hypervisor,它可以直接与主机操作系统通信,负责管理Docker容器为其分配资源,而应用的所有依赖包括源代码都打包在Docker镜像。相比于虚拟机而言,容器产生的开销更小,占用的空间更少,启动的速度更快。

1.2、镜像与层

Docker 镜像作为Docker容器的基础,由一系列层堆叠组成,每个层代表镜像的Dockerfile中的一条指令。除了最上层容器层之外,每个层都是只读了,每个层与之前的层只有一部分差异。当创建并启动一个新的容器时,Docker会加载只读镜像层并在其上(即镜像栈顶部)添加一个可读写层,这个层通常被称为“容器层”。如果运行中的容器修改了现有的一个已经存在的文件,那该文件将会从读写层下面的只读层复制到读写层,该文件的只读版本仍然存在,只是已经被读写层中该文件的副本所隐藏。当删除Docker容器,并通过该镜像重新启动时,之前的更改将会丢失。在Docker中,只读层及在顶部的读写层的组合被称为Union File System(联合文件系统)。下图显示了一个基于 Ubuntu 15.04 镜像的容器。

【Docker】容器存储管理综述【容器持久化数据管理+应用场景+与传统存储的对比】

1.3、容器与层

容器和镜像之间的主要区别是顶部的可写层。所有对容器添加新的或修改现有数据的内容都存储在该可写层中。当容器被删除时,可写层也被删除,底层镜像则保持不变。由于每个容器都有自己的可写的容器层,并且所有更改都存储在此容器层中,因此多个容器可以共享对相同基础镜像的访问权限,并且拥有自己的数据状态。下图显示了共享相同 Ubuntu 15.04 镜像的多个容器。Docker 使用存储驱动程序来管理镜像层和可写容器层的内容。每个存储驱动程序都处理实现的方式不同,但所有驱动程序都使用可堆叠的镜像层和写入时复制(CoW)策略。存储驱动程序处理有关这些层相互交互方式的详细信息,不同的存储驱动程序可以使用,在不同情况下各有优缺点。

【Docker】容器存储管理综述【容器持久化数据管理+应用场景+与传统存储的对比】

由于容器灵活、高效、易于扩展,并面向云计算,因此希望将其应用范围扩展到徽服务之外,例如大型的容器化的数据库、甚至存储系统,但是通过容器设计企业级的、长期存储数据的应用目前仍是困难的。

1.4、容器持久化数据存储

Docker容器架构包括三种类型的数据存储 1)第一类是镜像存储,可利用现有的共享存储进行交付,要求类似于服务器虚拟化环境中虚拟机镜像分发保护的平台架构。容器镜像的数据存储容量相对完整的虚拟机镜像较小,可以高效地存储、共享。其缺点在于无法存储动态应用程序的数据。2)第二类是容器的管理数据,如存储配置、日志记录等管理数据。3)第三类是容器应用数据的存储,容器使用分层文件系统,将所有新写入的数据存储在临时虚拟层,无法直接修改最底层的镜像,写入日录和文件。一旦容器消失,所有的临时存储都将丢失。

容器上承载的应用分为有状态和无状态,容器本身更倾向于无状态化、可水平拓展的应用,但并不意味所有的应用都是无状态的,一些应用服务的状态需要保存比如日志等相关信息,因此需要持久化存储。而容器的特性决定了容器本身是非持久化的,当容器被删除时其上的数据也一并删除。

针对Docker容器的持久化数据访问有多种选择:Docker存储驱动(可以理解为Docker文件系统驱动);Docker卷;直接设备访问(device mapper,也可以理解为一种存储驱动)。但这三种方式目前仍然存在各自的问题:

  • Docker存储驱动:1)、对文件进行分层访问,需要通过多个文件或持久层复制数据,存在空间和速度问题;2)、必须为驱动程序配置整个Docker服务守护程序,且除非重新配置和重新安装否则无法更改。
  • Docker数据卷:1)、卷在容器启动时指定,不能后续添加;2)、数据不隔离;3)、由于卷驱动程序中的内部同步点,多线程应用程序不能很好地扩展。
  • 直接设备访问:1)、容器启动时指定并授予对设备的直接访问权限,且在重启时要求完全相同的驱动程序,新驱动安装后必须重新启动容器;2)、必须授予违反隔离原则的容器特殊权限,因为它允许每个容器在使用相同规范启动的容器中访问或覆盖共享驱动程序中的数据。

2. Docker容器存储

2.1、存储驱动

Docker 使用存储驱动程序来管理镜像层和可写容器层的内容与数据操作。每个存储驱动程序都处理实现的方式不同,但所有驱动程序都使用可堆叠的镜像层和写入时复制(CoW)策略。

2.1.1、AUFS

AUFS是一个联合文件系统,AUFS存储驱动是文件级存储驱动。AUFS能透明覆盖一或多个现有文件系统的层状文件系统,把多层合并成文件系统的单层表示,这些目录在AUFS术语中称为分支,在Docker术语中称为层。简单来说就是支持将不同目录挂载到同一个虚拟文件系统下的文件系统,这种文件系统可以一层一层地叠加修改文件。无论底下有多少层都是只读的,只有最上层的文件系统是可写的。

【Docker】容器存储管理综述【容器持久化数据管理+应用场景+与传统存储的对比】

当需要修改一个文件时,AUFS创建在可写层使用CoW创建该文件的副本,将文件从只读层复制到上层可写层进行修改,结果保存在可写层。在Docker中,底下的只读层就是image,可写层就是Container,从上层至下层具有覆盖性,当上层和下层存在相同文件时,上层文件会覆盖下层文件。当执行读操作时,由上至下检索,当找到文件时读取。AUFS因此会因为过深的镜像层数影响读写性能。AUFS的逻辑结构如上图所示。

容器的每个镜像层/容器层在Docker主机上表示为/var/lib/docker/中的子目录。AUFS通过联合挂载提供了所有层的统一视图,关于镜像和容器层的所有信息都存储在子目录/var/lib/docker/aufs/中:

  • diff/:存储每个layer的内容,每个都存储在一个单独的子目录中。包括可写容器层中的改变也保存在该文件子目录对应的文件目录下。
  • layers/:有关镜像层堆叠方式的元数据。该目录包含Docker主机上每个镜像或容器层的一个文件,每个文件都包含堆栈中其下面所有层的ID(其父项)。
  • mnt/:挂载点,每个映像或容器层一个,用于为容器集合和挂载统一文件系统。对于只读镜像这些目录始终为空。

2.1.2、BTRFS

Btrfs被称为下一代写时复制文件系统,并入Linux内核。Btrfs存储驱动是文件级驱动,但也能像Device mapper一样直接操作底层设备。Btrfs把文件系统的一部分配置为一个完整的子文件系统,称为subvolume。采用“子卷”的一个大的文件系统可以被划分为多个子文件系统,这些子文件系统共享底层的设备空间,在需要磁盘空间时便从底层设备中分配,类似于应用调用 malloc()分配内存。1)当写入新文件时,btrfs通过用时分配为容器快照分配一个新的数据块,文件写在这个空间里;2)当修改已有文件时,btrfs从文件当前存在的层读取原始数据,使用CoW分配一个新的原始数据快照并在其中修改数据,并仅将修改的块写入容器的可写层,再更新快照中的文件系统元数据指向此新数据;3)当读数据时,针对快照执行的读取与针对子卷执行的读取基本相同;4)当删除底层中的文件/目录时,Btrfs会屏蔽底层中文件/目录的存在。若先创建文件然后将其删除,则此操作将在Btrfs文件系统本身中执行,并回收该空间。

为了灵活利用设备空间,Btrfs 将磁盘空间划分为多个chunk,而每个chunk可以使用不同的磁盘空间分配策略,比如某些chunk只存放metadata,某些chunk只存放数据。这种方式有很多优点比如Btrfs支持动态添加设备而无需解挂载文件系统或重启Docker。用户在系统中增加新的磁盘之后,可以使用Btrfs命令将该设备直接添加到文件系统中。

【Docker】容器存储管理综述【容器持久化数据管理+应用场景+与传统存储的对比】

Btrfs存储驱动与设备映射或其他驱动的工作方式不同,整个/var/lib/docker/目录存储在Btrfs卷上。Btrfs存储驱动将每个镜像和容器存储在自己的Btrfs子卷或快照中:镜像的基础层存储为子卷,而子镜像层和容器存储为快照,如上图。Btrfs把一个大的文件系统当成一个资源池,配置成多个完整的子文件系统,可以往资源池里加新的子文件系统,而基础镜像则是子文件系统的快照,每个子镜像和容器都有自己的快照,这些快照则都是subvolume的快照。

2.1.3、Overlay / Overlay2

OverlayFS是一种现代化联合文件系统,是文件级驱动,类似AUFS但相对更快速和稳定。Overlay存储驱动在Linux内核3.18后支持,Overlay2存储驱动在Linux内核4.0以后支持,可能的情况下选择后者会更高效。Overlay存储驱动的结构如下图所示:

【Docker】容器存储管理综述【容器持久化数据管理+应用场景+与传统存储的对比】

OverlayFS将单个Linux主机上的两个目录分层,并将它们显示为单个目录。这些目录称为层,统一过程称为联合挂载,下层目录称为lowerdir,上层目录称为upperdir,统一视图通过命名为merged的目录公开。当镜像层和容器层包含相同文件的情况下,容器层“获胜”并且隐藏镜像层中相同文件的存在。当需要修改一个文件时,使用CoW将文件从只读的lowerdir复制到可写的upperdir进行修改,结果保存在upperdir层。

Overlay:和AUFS驱动不同的是Overlay只有两层,镜像层是lowerdir,容器层是upperdir。统一视图通过名为merged的目录公开,该目录实际上是容器挂载点。Overlay驱序仅包含两层,意味着多层镜像不能实现为多个OverlayFS层。 因此每个镜像层都在/var/lib/docker/overlay下实现为自己的目录,然后使用硬链接作为应用与较低层共享数据。但硬链接的使用导致过度使用inode,并且可能需要对后端文件系统进行额外配置。

Overlay2:驱动程序则支持多达128个较低的OverlayFS层。此功能为与层相关的Docker命令(如build、commit)提供了更好的性能,且在后端文件系统上消耗的inode更少。

2.1.4、ZFS

ZFS 是革命性的下一代文件系统,它从根本上改变了文件系统的管理方式,ZFS 完全抛弃了“卷管理”,不再创建虚拟的卷,而是把所有设备集中到一个存储池中来进行管理,用“存储池”的概念来管理物理存储空间。文件系统过去都是构建在物理设备之上的,为了管理这些物理设备并为数据提供冗余,“卷管理”的概念提供了一个单设备的映像。而ZFS创建在虚拟的被称为“zpools”的存储池之上。每个存储池由若干虚拟设备(virtual devices,vdevs)组成,zpool上的文件系统可以使用这些虚拟设备的总存储容量。Zpool的大小可在运行时通过动态添加专用设备来扩容,且ZFS可限制每个容器的可用存储空间。

ZFS包括:1)文件系统:精简配置,按需从zpool分配空间;2)快照:文件系统的只读空间高效的副本;3)克隆:快照的可读写副本,存储与之前层的差异。文件系统、快照和克隆都从底层zpool分配空间。下图为创建克隆的过程:1)从文件系统创建只读快照;2)从快照创建可写克隆,包含与父镜像层的任何差异。

【Docker】容器存储管理综述【容器持久化数据管理+应用场景+与传统存储的对比】

每个运行中容器的统一文件系统都挂载在/var/lib/docker/zfs/graph/中的挂载点上。镜像的基础层是ZFS文件系统,每个孩子层都是基于其下层ZFS快照的ZFS克隆。一个容器是基于其源镜像顶层ZFS快照的ZFS克隆。快照是只读的,而克隆是可写的。下图为一个基于双层镜像的运行时容器。

【Docker】容器存储管理综述【容器持久化数据管理+应用场景+与传统存储的对比】

Docker里ZFS首先从zpool里分配一个ZFS文件系统给镜像的基础层;而其他镜像层则是这个ZFS文件系统快照的克隆;当容器启动时则在镜像最顶层生成一个可写层。当要写一个新文件时,使用按需分配将一个新的数据快从zpool里生成,新的数据写入这个块,而这个新空间存于容器层(ZFS的克隆)。当要修改一个已存在的文件时,为要修改的快分配一个新空间并使用写时复制把原始数据复制到新空间完成修改。

2.1.5、Device Mapper

Docker的 device mapper 存储驱动程序利用内核框架Device Mapper的精简配置和快照功能进行镜像和容器管理,其使用专用块设备而不是格式化的文件系统,并在块级别上对文件进行操作。它提供一种从逻辑设备到物理设备的映射框架机制,在该机制下用户可以方便的根据需要制定存储资源的管理策略。Device mapper存储驱动可通过将物理存储添加到Docker主机来拓展这些设备。

通过Device mapper 存储驱动程序启动 Docker 时,所有镜像及容器层都保存在 /var/lib/docker/devicemapper/,它由一个或多个块级设备支持。Device mapper驱动首先在块设备上创建一个资源池,然后在资源池上创建一个带有文件系统的基本设备,所有镜像都是这个基本设备的快照,而容器则是镜像的快照。所以在容器里看到的文件系统是资源池上基本设备的文件系统的快照,初始不为容器分配空间:1)当写入新文件时,通过用时分配为容器快照分配新的块并写入数据;2)当要修改已有文件时,使用CoW为容器快照分配块空间,将要修改的数据复制到在容器快照中新的块里再进行修改。

【Docker】容器存储管理综述【容器持久化数据管理+应用场景+与传统存储的对比】

如上图所示,Docker 主机上有两个容器在运行,分别是 ubuntu 和 busybox。每个镜像层都是下面层的快照,每个镜像的最底层是池中base device 的快照。在运行容器时,容器是它所依赖的镜像的快照。

2.1.6、Docker存储驱动的对比及适应场景

如下图所示,为Docker存储驱动的对比以及适用场景。

【Docker】容器存储管理综述【容器持久化数据管理+应用场景+与传统存储的对比】

2.2、数据卷

当容器删除后,任何写到容器但没有保存到数据卷的数据都会和容器一同删除。为了能够保存(持久化)数据以及共享容器间的数据,Docker提出了Volume的概念。简单理解来看,Volume就是一个目录或者文件,它可以绕过默认的联合文件系统或存储驱动,而以正常的文件或者目录的形式存在于宿主机上。这增强了数据卷与存储驱动程序控制的独立性,当容器被删除时,存储在数据卷中的任何数据都会保留在 Docker 主机上。

【Docker】容器存储管理综述【容器持久化数据管理+应用场景+与传统存储的对比】

数据卷作为Docker 主机文件系统上的一个目录或文件,直接挂载到容器中,而不被存储驱动程序控制。对数据卷的读写操作绕过存储驱动程序,并以本地主机速度运行。可以将任意数量的数据卷装入容器,多个容器也可以共享一个或多个数据卷。上图显示了一个运行两个容器的独立 Docker 主机:每个容器都存在于Docker主机本地存储区/var/lib/docker/内其自己的地址空间中,在主机/data 目录上还有一个共享数据卷,作为数据卷被直接挂载到两个容器中。

Docker数据卷可以将容器以及容器产生的数据分离开来,从而使得删除容器时,不会影响相关的数据。可以通过两种方式来初始化Volume:数据卷、数据卷容器:

  • 数据卷:数据卷的使用类似于 Linux 下对目录进行 mount,挂载一个主机目录作为数据卷,你想在容器中使用主机上的某个目录,你可以通过-v参数来指定:docker run -v /host/path:/some/path ;
  • 数据卷容器:授权一个容器访问另一个容器的Volume,使用--volumes-from参数来执行。针对持久性数据在容器间共享的情况,可以通过创建一个数据卷容器,然后从此容器上挂载数据。常见的使用场景是使用纯数据容器来持久化数据库、配置文件或者数据文件等。

容器持久化数据卷方案中持久层的生产价值,不是通过弹性,而是可以拓展数据卷通过灵活可编程,例如通过设计的API来扩展存储。Docker已发布容器卷插件规范,允许第三方厂商的数据卷在Docker引擎中提供数据服务。这种机制意味着外置存储可以超过容器的生命周期而独立存在,且各种存储设备只要满足接口API标准,就可接入Docker容器的运行平台中。即现有的各种存储可以通过简单的驱动程序封装,从而实现和Docker容器的对接。可以说,驱动程序实现了和容器引擎的北向接口,底层则调用后端存储的功能完成数据存取等任务。目前已经实现的Docker Volume Plugin中,后端存储包括常见的NFS,GlusterFS和块设备等。

2.3、容器存储的应用以及与传统存储的比较

2.3.1、容器存储的应用场景

容器持久化的解决方案在于需要不需要保存数据,以及保存在哪?

  • 当数据保存在本地(故障率高):那就是本地硬盘,即通过上述容器存储驱动以及数据卷来对本地数据进行存取操作。
  • 当数据保存在外部:1)分布式系统(并行处理),那么就需要分布式系统支持Volume Plugin;2)共享存储(高可用),那么外部存储需要支持Volume Plugin的驱动程序,问题即转变为选择哪种存储/存储软件,以及其是否支持Volume Plugin。

2.3.2、容器存储与传统存储的比较

1、容器存储

容器存储简单理解来看其实就是通过存储驱动以及数据卷实现对容器镜像、容器管理数据、以及容器应用数据的存储,其本质上仍然需要依赖传统的文件系统来支撑。

对于存储驱动,其对应的挂载目录均需要使用对应的后端文件系统进行格式化挂载管理,如下图所示;而对于数据卷,其依赖于主机文件系统的管理操作。

【Docker】容器存储管理综述【容器持久化数据管理+应用场景+与传统存储的对比】

2、文件系统存储、块存储和对象存储,三种存储系统实现容器持久存储对比?

这三者都是通过实现Volume Plugin来支持,与标准的文件系统,块,对象在使用上的差别是类似的:

  • 块存储比较快,扩展弱,价格贵
  • 文件系统存储性能中等,扩展强,价格中等,分布式环境可用
  • 对象存储性能弱,扩展极强,价格低,一般针对分布式环境或者云环境。

所以从实现上是没多大区别的,只要Driver支持。

目前看来,块存储的应用最为广泛,主要是块存储的技术储备最多,并且广泛使用。对象存储次之,现在对象存储基于S3/swift/ OSS接口,随着公有云的普及,被广泛使用。文件存储,目前还是NAS传统存储的天下,性能和可靠性最为稳定,cephFS目前还是不足以满足生产环境的使用。