Docker中存储应用

在什么场景下使用虚拟机,在什么场景下使用物理机。这些问题并没有一个统一的标准答案,因为它们都有自身最适用的场景,在此场景下,它们都是不可替代的。

原本是没有虚拟机的,所有的应用都直接运行在物理机上,计算和存储资源都难以增减,要么资源不够用,要么就是把过剩的资源浪费掉,所以现在虚拟机被广泛地使用,而物理机的使用场景被极大地压缩到了像数据库系统这样的特殊应用上。

原本也没有容器,大部分的应用都运行在虚拟机上,只有少部分特殊应用仍然运行在物理机上。但现在所有的虚拟机技术方案,都无法回避两个主要的问题,一个是Hypervisor本身的资源消耗与磁盘I/O性能的降低,另一个是虚拟机仍然还是一个独立的操作系统,对很多类型的应用来说都显得太重了。所以,容器技术出现并逐渐火热,所有应用可以直接运行在物理机的操作系统上,可以直接读/写磁盘,应用之间通过计算、存储和网络资源的命名空间进行隔离,为每个应用形成一个逻辑上独立的“容器操作系统”。

 

容器

1. 容器技术框架

容器技术框架如图所示。

 

服务器层包含了容器运行时的两种场景,泛指容器运行的环境。资源管理层的核心目标是对服务器和操作系统的资源进行管理,以支持上层的容器运行引擎。应用层泛指所有运行于容器上的应用程序,以及所需的辅助系统,包括监控、日志、安全、编排、镜像仓库等。

运行引擎主要指常见的容器系统,包括Docker、rkt、Hyper、CRI-O等,负责启动容器镜像、运行容器应用和管理容器实例。运行引擎又可以分为管理程序(Docker Engine、OCID、hyperd、rkt、CRI-O等)和运行时环境(runC/Docker、runV/Hyper、runZ/Solaris等)。需要注意的是,运行引擎是单机程序(类似虚拟化中的KVM和Xen),运行于服务器操作系统之上,接受上层集群管理系统的管理。

容器的集群管理系统类似于针对虚拟机的集群管理系统,它们都是通过对一组服务器运行分布式应用,细微的区别是,针对虚拟机的集群管理系统需要运行在物理服务器上,而容器集群管理系统既可以运行在物理服务器上,也可以运行在虚拟服务器上。常见的容器集群管理系统有Kubernetes、Docker Swarm、Mesos,其中Kubernetes的地位可以与OpenStack相比。围绕Kubernetes,CNCF基金会已经建立了一个非常强大的生态体系,这是Docker Swarm和Mesos都不具备的。

 

2. Docker

Docker的思想来自集装箱,集装箱解决了什么问题?在一艘大船上,货物被规整地摆放起来,并被集装箱标准化,集装箱和集装箱之间不会互相影响,从而不再需要专门运送水果的船和专门运送化学品的船,只要这些货物在集装箱里封装好,就可以用一艘大船全部运走。现在流行的云计算就类似于大货轮,而Docker就类似于集装箱。

不同的应用程序可能会有不同的应用环境,比如.net开发的网站和PHP开发的网站依赖的软件就不一样,如果把它们依赖的软件都安装在一个服务器上就需要调试很久,而且很麻烦,还可能会造成一些冲突。这时,为了隔离.net开发的网站和PHP开发的网站,我们可以在服务器上创建不同的虚拟机,在不同的虚拟机上放置不同的应用。但是虚拟机的开销比较高,Docker则可以实现类似的应用环境的隔离,并且开销比较小。

如果开发软件的时候用的是Ubuntu,但是运维人员管理的都是Centos,运维人员在把软件从开发环境转移到生产环境的时候就会遇到一些环境转换的问题。这时,开发者可以通过Docker把开发环境直接封装后转移给运维人员,运维人员直接部署给他的Docker就可以了,而且部署速度快。

Docker使用客户—服务器模型,客户端接收用户的请求,然后发送给服务器端,服务器端收到消息后,执行相应的动作,包括编译、运行、发布容器等。其中,客户端和服务器端既可以运行在相同的主机上,也可以运行在不同的主机上,它们之间可以通过REST API、socket等进行交互,Docker架构如图所示。

 

1)Docker守护进程

Docker守护进程(Dockerd)始终监听是否有新的请求到达。当用户调用某个命令或API时,这些调用将被转化为某种类型的请求发送给守护进程。Docker守护进程管理各种Docker对象,包括镜像、容器、网络、卷、插件等。

2)Docker客户端

Docker客户端是Docker用户与守护进程进行交互的主要方式。当我们在命令行界面输入docker命令时,Docker客户端将封装消息并传递给守护进程,守护进程根据消息的类型来采取不同的行动。

3)Docker仓库

Docker仓库存储着Docker的镜像。Docker Hub和Docker cloud是公共的Docker仓库,任何人都可以将自己的本地镜像上传到公共仓库,或者下载公共仓库的镜像到本地。我们可以使用配置文件来指定Docker仓库的位置,默认的仓库是Docker Hub。

例如,当我们想从仓库拉取nginx镜像时,首先在命令行输入如下命令,从远端复制nginx镜像到本地:docker pull nginx

一旦上述命令执行完毕,nginx镜像将会被复制到本地,并由Docker引擎管理。通过list命令来检查已经成功拉取的镜像:

由于镜像已经被复制到本地,当启动nginx镜像的容器时,Docker引擎将直接从本地读取内容,运行速度将非常快,可以使用如下命令去运行一个容器:

 

容器与镜像

通过运行一个镜像来产生一个容器。如图6所示,镜像就是一堆只读层(Read Layer)的叠加,除底层(即原始只读层)以外,每一层都有一个指针指向它的下一层。

 

Docker通过联合文件系统(Union File System)技术将不同的层整合成一个文件系统,屏蔽了多层的存在,为用户提供了一个统一的视角—— 一个镜像只存在一个文件系统。

与镜像类似,容器也是一堆层的叠加,唯一的区别在于,容器最上面那一层是可读/写的。在容器存在的生命周期内,任何修改都将被写在可写层,包括创建、删除文件等,如图所示。

 

而对于运行时的容器来说,除了一个联合可读/写文件系统,还包括隔离的进程空间及其中的进程,如图所示。

 

Docker存储

在默认情况下,容器运行时产生的临时文件都被存储到容器的可写层(也叫容器层),也就是说这些数据被保存在容器内部,当容器的生命周期结束,这些数据也会随之消失,因此将产生如下3个方面的问题。

· 数据持久化问题。当容器停止运行以后,容器产生的数据将会丢失,包括对文件的创建、删除、修改等操作。

· 高度耦合性问题。容器可写层和容器所属主机之间存在高度的耦合性,因此数据不易迁移,不易共享、不易备份。

· 性能不佳问题。在默认情况下,容器可写层通过Docker的存储驱动来管理存储。容器通过调用内核模块来实现联合文件系统,这种通过更高层地抽象而得到的文件系统对I/O性能往往影响较大。

为了解决这些问题,Docker的早期版本就已经支持绑定挂载存储方式,使用这种方式时,宿主机的一个文件或文件夹被映射到容器中,容器可以修改绑定的文件或文件夹内的数据,甚至宿主机的任何进程都可以被修改。

此外,Docker在1.8版本之后推出了对数据卷插件的支持。在容器中运行的应用,可以将需要保存的数据写入持久化的数据卷。一方面,由于以微服务架构为主的容器应用多数为分布式系统,容器可能在多个节点中动态地启动、停止、伸缩或迁移,因此,当容器应用具有持久化的数据时,必须确保数据能被不同的节点访问。另一方面,容器是面向应用的运行环境,数据通常要保存到文件系统中,即存储接口以文件形式更适合应用进行访问。综合来说,容器平台较适宜的应是具有共享文件接口的存储系统。

 

临时存储

在默认情况下,容器内产生的数据都是临时的,都被存储到容器的可写层,所有的数据流都经过联合文件系统。通过采用非常灵活的模块化机制,Docker提供了多种存储方案供用户选择,用户可以根据不同的场景来选择恰当的存储驱动。

Docker使用存储驱动来管理镜像层和容器层的内容,并定义了一套存储驱动的接口,只要实现了相应的方法,就可以扩展出一种存储引擎。目前,已经实现的存储引擎包括aufs、Btrfs、device mapper、VFS、Overlayer等。用户需要根据不同的场景选择恰当的存储驱动,从而有效提高Docker的性能。

那么如何选择合适的存储驱动呢?这需要我们首先明白Docker是如何构建和存储镜像的,以及了解各种不同存储驱动的背后机制。

 

1.不同存储驱动实现细节

不同的存储驱动实现的细节各不相同,但是都基于一个原理、两个策略:镜像分层原理,写时复制策略和用时分配策略(Allocate-on-Demand)。

1)镜像分层原理

如前所述,镜像是由很多只读层叠加而成的。对于Docker来说,镜像中的每一层都可以和DockerFile文件中的一条指令对应起来。比如下面的这个DockerFile文件:

 

DockerFile文件由4行指令组成,每一行指令都对应镜像的某一层。

· 第1行指令:FROM表示从ubuntu:15.04开始构建镜像,因此这一层是原始层。

· 第2行指令:COPY表示复制一些文件到镜像。

· 第3行指令:RUN表示使用make命令来构建应用程序。

· 第4行指令:CMD表示在容器内运行的某条命令。

如下图所示,我们可以将上述的DockerFile文件看成由4个只读层叠加而成的一个镜像。存储驱动的作用就是处理不同层之间的交互。

 

当我们创建一个容器时,Docker将会在顶端创建一个可写层。在容器的生命周期内,任何修改都将被写在可写层。当容器被删除时,可写层也随之被删除,但是只读镜像层却不改变。基于这个原则,不同的容器可以共享相同的底层只读镜像,同时,每个容器又拥有独立的可写层来保存各自的状态和修改,其结构如图所示。

 

2)写时复制策略

写时复制策略可以最大化复制文件的效率。想象这样一种情景,基于一个镜像启动多个容器,如果每个容器都去申请一个镜像文件系统,那么重复的数据将会占用大量的存储空间。而写时复制技术可以让所有的容器共享同一个镜像文件系统,所有数据都从同一个镜像中读取,只有对文件进行写操作时,才会把要写的文件复制到自己的文件系统中。

3)用时分配策略

用时分配策略是指只有在写入一个新的文件时才分配空间,从而提高存储资源的利用率。例如,启动一个容器时,并不会为这个容器预先分配磁盘空间,只有当有新文件写入时,才会按需分配新空间。

 

2.AUFS存储驱动

在早期Docker版本中,AUFS是默认的存储驱动,但由于AUFS并没有合并到Linux内核中,有些Linux发行版对其没有提供支持。Docker官方建议,在内核4.0及更高的版本中,应尽量选择性能和稳定性更高的Overlay2驱动。如果用户选择使用AUFS存储驱动,应该首先检查内核是否支持,命令如下:

 

对于运行中的容器,用户可以使用info命令查看容器当前使用的存储驱动,命令如下:

 

作为一种联合文件系统,AUFS的主要功能是将不同物理位置的目录进行合并,然后挂载到同一个目录下,以单一目录的组织形式提供给用户。AUFS分层存储如下图所示,镜像的每一层对应/var/lib/docker/aufs下的一个文件,AUFS将各个层次组织成统一目录提供给容器使用,经过AUFS的处理,最终显示给用户的是/var/lib/docker/目录(这个目录将被Docker管理,用户不要直接修改这个目录下的文件)。

 

1)镜像和容器在磁盘上的组织形式

我们通过一个具体的例子来说明镜像和容器是如何构建和组织的,当用户拉取Docker仓库中的某个镜像时,镜像的每一层将以文件的形式被下载到本地/var/lib/docker/aufs文件夹下,命令如下:

 

镜像层和容器层的所有信息存储在文件夹/var/lib/docker/aufs下,比如/var/lib/docker/aufs/layers/存储了镜像层的元数据信息。

2)AUFS文件系统的读/写流

通过AUFS读文件时,有以下3种不同的情况。

(1)文件仅存在于镜像层:如果在容器层不存在这个文件,AUFS将会到镜像的各个层次从上到下搜索这个文件,搜索到之后,将直接读取镜像上的文件内容。

(2)文件仅存在于容器层:如果文件存在于容器层,那么就直接从容器层读取这个文件。

(3)文件既存在于容器层,也存在于镜像层:AUFS将从容器层读取文件,将会屏蔽镜像层与容器层具有相同文件名的文件。

通过AUFS修改文件时,有以下两种不同的情况。

(1)第一次写文件:容器对某个文件进行第一次写操作时,这个文件并不在容器层,因此AUFS需要执行copy_up操作将文件从镜像层复制到容器层,再将数据写到容器层副本中。此时有两个因素会严重影响容器的写性能:①AUFS操作的最小粒度是文件,这就意味着copy_up操作需要复制一个文件的所有内容,即使仅仅是修改一个大文件中的一个字符,仍然需要将整个文件从镜像层复制到容器层,这会严重影响容器的写性能;②需要依次搜索镜像文件的每一层去查找文件,因此会有更长的延迟。但是,这两个因素都只影响第一次写文件,随后对这个文件的修改将直接在容器层进行。

(2)删除文件(或文件夹):有时,容器层需要删除一个文件,但是镜像层中的文件是只读的,无法删除,那么应该如何处理呢?AUFS的处理方式是在容器层创建一个隐藏文件去屏蔽对底层镜像文件的使用。比如对于文件的删除,将会在容器层创建一个writeout文件,writeout文件会阻止容器对这个文件的访问,即禁止容器层使用镜像层中的某个文件。对文件夹的删除操作,创建的是opaque文件。

3)AUFS性能分析

AUFS存储驱动对于写操作延迟较大,其主要原因是,在第一次写操作时,需要为文件分配存储空间,然后在镜像层中搜索文件,最后复制文件内容到容器层,这一系列操作带来了较长的延迟。随着镜像层次的增加,写延迟会被进一步放大。在写操作比较密集的业务场景下,虽然我们可以通过固态硬盘加速容器层的读/写性能,但是Docker官方强烈建议采用另外一种存储方式:卷存储。其主要原因是卷存储可以绕开存储驱动,使用卷驱动可以直接将数据写在卷上,从而避免联合文件系统带来的额外开销的问题。

与AUFS相比,虽然Overlay2更稳定,性能更好。但是AUFS也有一些独有的优点,如高效的磁盘利用率。想象这样一种业务场景:某项业务需要非常密集的容器数量来支撑。在这种场景下,AUFS的共享镜像将会带来两个好处:使大量容器省去了拉取镜像的时间,直接使用本地共享镜像启动;最小化的磁盘利用空间。

 

3.OverlayFS存储驱动

OverlayFS也是一种联合文件系统,但是与AUFS相比,OverlayFS实现更加简单,效率更高,并且在2014年被正式加入Linux主线内核中。Docker官方强烈建议用OverlayFS取代AUFS。当前最新版Docker支持两种类型的OverlayFS存储驱动:Overlay和Overlay2(Overlay2具有更高的性能和更高的索引节点利用率)。

OverlayFS建立在其他文件系统之上,并且不直接参与存储,其主要功能是合并底层文件系统,然后提供统一的文件系统供上层使用。

1)激活OverlayFS存储驱动

Docker官方建议,如果环境允许(包括内核版本、Docker版本),应该尽量选择OverlayFS存储驱动。按照如下步骤启动Overlay2存储驱动:

 

2)Overlay的存储机制

Overlay是由两层组成的,如图19所示,lowerdir层也称为镜像层,upperdir层也称为容器层,Overlay将镜像层和容器层的内容组合,然后提供给用户统一的视角,称为merged层。

当镜像层和容器层含有相同的文件时,容器层的文件将显示到merged层,镜像层的文件将被屏蔽。当没有相同文件时,容器层或镜像层的文件将直接映射到merged层。这就意味着OverlayFS仅支持一层镜像文件,当镜像包含多层时,各个镜像层中,每下一层的文件将以硬链接的方式出现在它的上一层中,以此类推。

 

3)OverlayFS在磁盘中的组织形式

以Ubuntu为例,我们从仓库拉取Ubuntu镜像,并观察命令行输出:

 

从输出我们可以看到,Ubuntu镜像由5层组成。在/var/lib/docker/overlay/目录下,镜像的每一层都对应一个目录,每个目录存储相应镜像层的内容:

容器层也在/var/lib/docker/overlay/目录下,容器层目录包括3个文件夹和一个文件,内容如下:

 

4)OverlayFS的读/写流程

读文件时,主要包括3种情形:①如果读取的文件存在于镜像层,那么直接读取镜像层文件;②如果读取的文件存在于容器层,那么直接读取容器层文件;③文件既存在于容器层也存在于镜像层,那么读取容器层文件,镜像层文件将被屏蔽。

修改文件操作包括第一次写文件、删除文件、重命名文件等情况。其基本原理与AUFS机制类似,第一次写文件时,如果文件不在容器层,需要先将文件从镜像层复制到容器层,再修改文件。删除文件时,本质是添加writeout文件来屏蔽对文件的再次访问。

5)OverlayFS的性能分析

与AUFS、devicemapper存储驱动相比,在大部分情况下,OverlayFS有更好的性能。与Overlay相比,Overlay2性能更好。但是需要注意如下几种情况。

(1)页缓存。当多个容器共享同一个文件时,OverlayFS的页缓存机制可以共享给多个容器同时使用,从而大大提高内存利用率。

(2)缩减搜索空间,提高copy_up性能。当第一次写文件时,AUFS和OverlayFS会调用copy_up来复制文件,这会影响写性能,尤其是大文件,会进一步增大写延迟。当镜像层由多层组成时,AUFS为了找到相应文件需要逐层搜索镜像文件,由于OverlayFS拥有更少的镜像层,更多的共享页缓存,因此会大大提高搜索效率,降低延迟。

(3)索引节点限制。当一台主机含有大量容器和镜像时,使用Overlay会消耗大量的索引节点,唯一的解决方法是格式化文件系统。因此,Docker官方建议尽量采用Overlay2存储驱动。

 

持久化存储

针对上述临时存储存在的问题,Docker提供了数据卷功能来进行数据的持久化。数据卷将宿主机某个文件夹挂载在容器内部,绕开分层文件系统,相当于容器内部直接读/写宿主机的某个文件夹,因此数据卷的I/O性能与主机磁盘的I/O性能一致,并且脱离了容器的生命周期,获得持久化数据的能力。但是,数据卷也有一些缺点:仅能读/写本地磁盘,无法读/写远端数据;不能随容器迁移,无法实现数据共享。

为了解决这些问题,Docker在1.8版本之后推出了对数据卷插件的支持,来管理数据卷的生命周期,它的主要工作是将第三方存储映射到宿主机的本地文件系统中,以便容器使用该数据卷。按照数据卷插件的规范,第三方厂商的数据卷可以在Docker引擎中提供数据服务,使外置存储可以超过容器的生命周期而独立存在。这意味着各种存储设备只要满足数据卷插件接口的标准,就可以接入Docker容器的运行平台中了。

1.数据卷插件API

数据卷插件的API主要有以下内容。

· 创建接口/VolumeDriver.Create:需要创建数据卷时调用。

· 删除接口/VolumeDriver.Remove:需要删除数据卷时调用。

· 挂载接口/VolumeDriver.Mount:容器每次启动时都会调用一次该API。

· 路径接口/VolumeDriver.Path:返回卷在主机上的实际位置。

· Unmount接口/VolumeDriver.Unmount:容器每次停止时调用。

· Inspect接口/VolumeDriver.Get:Docker数据卷Inspect时调用。

· 列出接口/VolumeDriver.List:激活插件时调用,用于询问当前已有的卷,防止重复创建。

第三方存储厂商通过定义这些接口来定制所需的存储驱动,因此写一个存储插件非常简单,只要把上面这些请求实现即可。当前主流的一些云供应厂商和存储厂商都定义了自己的卷驱动,例如支持微软的Azure file storage plugin,支持谷歌、EMC、OpenStack、Amazon的REX-Ray plugin等。当使用第三方卷驱动时,数据卷的创建主要包括如下4个步骤。

· 用户通过Docker命令行向Docker守护进程发送卷创建命令。

· 卷创建命令通过Plugin API访问第三方提供的卷驱动插件。

· 第三方卷驱动将第三方存储映射到本地文件系统。

· 通过本地文件系统提供给容器。

数据卷创建完成之后,容器通过卷驱动将数据持久化到第三方存储。

用户可以通过Docker命令来创建和管理卷,例如通过docker volume create命令进行卷的创建,也可以在容器运行时动态地创建卷存储。同一个数据卷,可以被同时挂载到多个容器上以实现数据的共享,当没有任何容器使用卷时,数据卷也不会自动删除,除非调用Docker命令进行删除,如docker volume purge命令。

不同的卷驱动带来了卷后端存储的多样化,数据后端可以是本地、远端或云供应商等。如果用户没有显式地创建数据卷(即调用docker volume create),那么当容器第一次挂载数据卷时,卷将被自动创建,并且当容器停止或被移除时,数据卷也不会丢失。另外,对数据卷而言,不同的容器可分配不同的权限,例如,一部分容器有读/写权限,另一部分容器只有读权限。

2.卷存储的使用场景

在如下情况下,Docker官方建议使用卷存储解决方案。

· 远程存储、云存储等非本地存储方案。Docker实现了多种卷驱动,支持多种后端存储方案。

· 数据备份、数据恢复、数据迁移。

· 卷共享。

· 持久化数据。

用户使用卷向容器提供持久化存储,有两种方式。

· 将宿主机“目录”挂载到容器中:宿主机的目录既可以是本地文件系统的一个子目录,也可以是一块已经被格式化的块设备,其中块设备既可以由物理设备提供,也可以由云提供。

· 将宿主机的“文件”挂载到容器中:原生态的卷具有不可移植性、不易迁移、数据安全性较差、与宿主机文件系统有较强的耦合性等一系列问题。为了解决这些问题,Docker提供了分布式卷flocker的解决方案。数据会被存储到flocker后端,而不是本地主机上,即使宿主机出现故障,数据也不会丢失。

3.卷的使用

可以直接使用Docker命令在宿主机操作卷,主要包括创建、列出、删除、重命名等一些操作。

· 创建一个名为our-volume的卷,命令如下:

 

· 列出已创建的卷,命令如下:

 

· 检查卷的详细信息,命令如下:

 

· 移除数据卷,命令如下:

 

Docker也支持在启动容器的时候,通过-v 或 --mount 标志来创建指定的数据卷。

· 使用--mount标志启动容器,命令如下:

 

· 使用-v标志启动容器,命令如下:

 

· 当不再需要卷时,将会移除卷,命令如下:

· 当需要卷驱动时,使用--volume 或--volume-driver 来启动容器,使--driver标志来创建卷。以flocker为例,命令如下:

 

  • 1
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值