1. 为什么要使用存储卷

  • Docker镜像由多个只读层叠加而成,启动容器时,Docker会加载只读镜像层,并在镜像栈顶部添加一个读写层
  • 如果运行中的容器修改了现有的一个已经存在的文件,那该文件将会从读写层下面的只读层复制到读写层,该文件的只读版本仍然存在,只是已经被读写层中该文件的副本所隐藏,这个叫做“写时复制cow”机制
  • 关闭并重启容器,其数据不受影响;但是删除Docker容器,则其更改将会全部丢失
  • 存在的问题就是:存储于联合文件系统中,不易于宿主机访问;容器间数据共享不便;删除容器其数据会丢失
  • 解决方案就是使用volume,中文叫卷或者存储卷,“卷”是容器上的一个或多个“目录”,此类目录可绕过联合文件系统,与宿主机上的某个目录“关联”或者叫“绑定”

file

docker是容器运行的底层引擎,在组织和运行其容器时,一般只运行一个程序及其子程序。 而程序启动时所借助的镜像,很可能是不止一层的联合挂载组成的。像这种文件系统aufs和overlayfs2,一定要在最上面挂载一个读写层,对此读写层来说,所有在容器中所执行的操作,事实上都是保存在这一层之上的,而对下层内容的操作,比如要删除文件,我们就需要使用cow机制。 如果一个文件在底层layer0是存在的,而后在Layer1上标记删除,他不会真的删除,而是标记为删除。在layer2上就看不见了,在Layer0上的文件,在Layer2上标记为删除,用户也同样看不见。也就是说,如果一个数据在到达最上层之前,我们把它标记为删除,对于最上层的用户一定是不可见的,如果是没标记为删除的,或者是标记为删除的,而用户在最上层又建了一个一模一样的同名文件的,用户才可见。我们去访问一个文件,修改删除之类的操作之后,再访问效率非常的低。尤其是那些对IO要求比较高的应用,比如:redis这样的系统,在存储数据的时候,势必在于实现底层数据存储的时候,对于底层存储系统的性能要求较高。又比如,我们运行了一个存储系统,比如mariadb,或者mysql,本来mysql对于性能的要求就比较高,而mysql是运行在容器中自己创建的文件系统,也就是通过联合挂载之后,运行在最上层的可写文件系统之上的时候,不仅在容器停止的时候,数据会被删除,而且在存储的时候,效率也非常的低。要想绕过这种使用的限制,我们需要使用存储卷的形式来实现。在宿主机上,找一个文件系统,这个文件系统上可能存在一个目录,我们就把这个目录直接与容器内部的文件系统之上的某一目录,建立绑定关系,就好像挂载一样。比如:我们把主机上的/data/web与容器内的/containers/data/web目录建立映射关系,向/data/web中写目录的时候,会写在宿主机的/containers/data/web目录,就好像mount –bind的效果。这样就使得我们容器内的进程实现数据保存时,能够绕过容器内部文件系统的限制,从而与宿主机的目录建立关联关系,我们可以在容器内和系统上共享数据内容,让他们的数据是同步的,本来mount这样的名称空间本来应该是隔离的,但是我们却可以让两个本来是隔离的文件系统在某个子路径上建立一定程度的绑定关系,从而使得在两个容器之间的文件系统的子目录上不再是隔离的,而是能够实现一定意义上共享的效果。所以像这样的关联关系使得像容器之间跨文件系统共享数据这用需求变得容易了。而主机上的这个目录,就是跟容器内建立绑定关系的目录,对于容器来讲,就称作volume,就是存储卷。

2. 什么叫存储卷呢

  • 存储卷提供了多种功能来持久存储和共享数据
    • Volume于容器初始化之时就会创建,由base image提供的卷中的数据会于此期间完成复制
    • 数据卷可以在容器间实现共享和复用
    • 对于数据卷中数据的更改是直接体现在宿主机上的绑定的目录的
    • 在更新镜像的时候,数据卷是不变的
    • 即使容器被删除了,数据卷还是存在的

file

如果容器内进程的所有有效数据都是保存在存储卷,从而脱离的容器自身的文件系统以后,带来的好处就是,当容器关闭甚至删除时,我们都不用担心数据会丢失了。不删除与之绑定的在宿主机上的存储目录就可以了。因此,数据就实现了持久,脱离了容器生命周期的持久,这样即便容器被删除,只要我们的容器在重建的时候,让他关联到同一个存储卷上,虽然创建的容器已经不是此前的容器了,但是数据还是那个数据。容器会被当成一个有生命周期的动态对象来使用,容器关闭就是容器被删除的时候,但是底层的镜像不删除,我们可以基于镜像再启动一个容器。我们启动容器的时候,如果我们忘记了上次关联的文件目录怎么办?如果可以的话,我们希望有一个文件能够保存下来我们上次的容器是怎样启动和保存相关配置的,这个一般都是容器编排工具的作用。这样还有一个好处就是,容器启动在哪台主机上就不太重要了。如果我们有多个docker host,他们所关联的卷也不是宿主机本地的文件系统,我们会使用nfs,共享出来一个目录,这每一个docker主机挂载了nfs目录,对于宿主机来讲,就好像是挂载了本地目录一样,先确保宿主机能够驱动,并且连接到后端的存储服务上去。这每一个宿主机都必须是nfs客户端,并且能够正常挂载nfs文件系统。在宿主机之上,我们启动某一个容器的时候,使用自己本地的目录,关联到了容器上的某个目录,而这个目录恰恰就是nfs之上的某个目录,随后容器启动,保存数据会保存在宿主机之上,但实际上是保存在宿主机之上的。然后,这个容器就可以被删除了,下次容器启动在哪并不重要,我们可以在全集群内调度这个容器并启动,只要nfs上的数据目录还在,且宿主机也能够关联到nfs之上,我们就还能访问到。因此,我们以后再去分配存储计算和内存资源的时候,就不需要局限在单机之上了,而是在集群内部所有的机器上进行。这种机制很多容器编排工具都能实现,但是这后面非常依赖一个共享的存储系统。这是因为考虑到容器应用是需要持久存储数据的,有可能是有状态的,如果我们跑一个nfs的反向代理,有没有必要存数据呢????其实我们说过应用一般分为有状态和无状态的,有状态应用是说,这次请求的处理和此前的处理是有关联的,而无状态应用,是没有关联关系的。而大多数有关联关系的应用是需要持久存储数据的。想mysql这种就是有状态应用,而nginx这种反向代理就是无状态的应用。而tomcat实际上是有状态的,但是他并不需要持久存储数据,它的session都是存储在内存当中的。这样就会导致一旦节点宕机,session就消失了。从这个角度来说,他也是有状态的,需要持久存储的。一般来说,有状态的应用都需要持久存储。我们运维当中最难的就是那些有状态,且需要持久存储的。我们需要有大量运维步骤的支撑才能够运行起来。比如mysql的主从,需要我们的运维经验融合进去,才能安全的扩展,缩容。还有问题的修复。对于无状态的应用,我们可以非常容易的形成复制,从而实现自动化。对于有状态的来说,这几乎是无法脱离运维技能来管理的一个维度。因此极少有平台,能够在这个级别上取代运维人员的工作。这个在早期的k8s上是无法实现,而现在虽然有解决方案,但是依然不成熟。

  • 存储卷的初衷是独立于容器的生命周期实现数据持久化,因此删除容器之时既不会删除卷,也不会对哪怕未被引用的卷做垃圾回收操作;当然,如果我们删除容器的时候使用特殊的选项,也能够一并删除存储卷。
  • 卷为docker提供了独立于容器的数据管理机制
    • 可以把“镜像”想象成静态文件,例如“程序”,把卷类比为动态内容,例如“数据”;于是,镜像可以重用,而卷可以共享;
    • 卷实现了“程序(镜像)”和“数据(卷)”分离,以及“程序(镜像)”和“制作镜像的主机”分离,用户制作镜像时,无须在考虑镜像运行的容器所在的主机的环境

file

Docker有了存储卷之后,在内部读取数据时,如果写在/上,那么还是会写在联合挂载文件系统上,只有写在一个卷上来,比如/data,就会被关联到宿主机的某一个目录上来。又比如,我们在运行程序的时候会产生一些临时的文件,这些文件通常都会被写在/tmp下面,而tmp下面的数据都会被写在联合挂载的文件系统上。由于这些数据不需要保存,他们会随着容器的删除而删除。

如果说我们期望应用能够在多台机器上运行的话,就需要他在其他宿主机上启动的时候,能够找到此前存储数据的地方,否则,这种启动和运行,就不再是此前的容器所实现的效果了。这个对我们的生产是不适用的,因此,持久存储是必备的条件。 对于这种有状态的应用来讲,不用存储卷,数据只能放在容器本地,即使我们能接受他的存储效率,但是他会导致容器无法被迁移,相当于与当前宿主机绑定。一旦生命周期停止,我们只能让他处于停止状态而不能删除,需要下次启动的时候再启动这个容器,才能保证数据还在。一旦删除了就不存在了,因为他的可写层是随着容器的生命周期存在而存在的。所以这么一来,大家应该牢牢建立起来一个概念,对于有状态的应用,只要他有存数据的需求,那么持久卷就是必须的。

不过对于docker的存储卷用起来并没有这么麻烦,他并没有我们想象的这么强大的功能,至少说,如果不借助额外的体系来组织这个维度的。因为docker的存储卷默认的情况下是使用其所在的宿主机之上的本地文件系统目录的。也就是说,宿主机自己有一块磁盘,磁盘并没有共享给其他的docker主机,而这个容器所使用的目录,是关联到宿主机上磁盘的一个目录而已,也就意味着这个容器在当前一个宿主机上,启动,创建,再删除,只要他关联到宿主机的目录,那么他的数据就还存在,如果我们把他调度到其他机器上就不能的,这也是docker本身没有解决的问题。所以说,docker的存储卷本身只能是宿主机的本地存储,而不是我们刚才说的共享存储。大家发现,其实如果我们手动创建nfs服务器并且挂载到宿主机上,然后让容器在挂载的时候挂载nfs服务器所提供的存储的的方式也可以实现数据的跨主机迁移。但是这样的缺陷也很明显,就是这样也很依赖于运维人员的手动操作。

3. 存储卷的类型

Docker有两种类型的卷,每种类型都在容器中存在一个挂载点,但其在宿主机上的位置有所不同

  • Bind mount volume:一个在宿主机文件系统上指向用户指定位置的卷。也就是说,两个卷都需要手工指定,然后才能建立绑定关系。
  • Docker-managed volume:docker进程创建的,在宿主机上的一个由docker管理的分区。也就是说,我们只管指定容器内部的卷路径,而在宿主机上的目录是由docker创建的,我们不需要人工干预。这样极大的减少了卷在使用时候的耦合关系,缺点是没法手动指定目录,这种卷用于临时使用很方便,但是如果容器删除,而需要再次挂载这个卷的时候,就必须找到指定位置的目录,也就是含有容器ID的目录,重新挂载上去才能继续使用上次的卷。

file

4. 在容器中使用Volumes

为docker run命令使用-v选项即可使用Volume

4.1. Docker-managed Volume

docker run -it -name bbox1 -v /data busybox
# 查看bbox1容器的卷、卷标识符及挂载的主机目录
docker inspect -f  bbox1

比如: 创建一个容器,并且在容器中创建一个/data卷,不指定他在宿主机上的位置

docker run --name b2 -it -v /data busybox

使用docker inspect来观察一下容器,会看到

.
            "Volumes": {
                "/data": {}
            },
.

而挂载的位置在

.
        "Mounts": [
            {
                "Type": "volume",
                "Name": "d788bc746c78a9c78a309c7d619ba791ba9a8d5b907f1041e537c209d13c3aaa",
                "Source": "/var/lib/docker/volumes/d788bc746c78a9c78a309c7d619ba791ba9a8d5b907f1041e537c209d13c3aaa/_data",
                "Destination": "/data",
                "Driver": "local",
                "Mode": "",
                "RW": true,
                "Propagation": ""
            }
        ],
.

也就是说,如果我们在/var/lib/docker/volumes/d788bc746c78a9c78a309c7d619ba791ba9a8d5b907f1041e537c209d13c3aaa/_data中创建一个文件,在容器的目录/data中也可能看到且可以编辑

4.2. Bind-mount Volume

docker run -it -v HOSTDIR:VOLUMEDIR --name bbox2 busybox
docker inspect -f  bbox2

这次我们使用本机的/tmp作为存储卷送给docker的/data/tmp目录去挂载

docker run --name b2 -d -it -v /tmp:/data/tmp busybox

我们在通过docker inspect去查看mounts

        "Mounts": [
            {
                "Type": "bind",
                "Source": "/tmp",
                "Destination": "/data/tmp",
                "Mode": "",
                "RW": true,
                "Propagation": "rprivate"
            }
        ],

另外,如果指定的宿主机目录并不存在,docker也会自动创建目录。

4.3. Shared Volume

既然,我们可以把一个目录分给容器做为存储卷使用,我们同样也可以把这个目录同时分给另外一个目录使用,同时也实现了在容器间共享数据了。也就是让他们同时挂载宿主机上的同一个卷。

docker run --name b3 --it -d --rm -v /data/tmp:/tmp busybox
docker run --name b4 --it -d --rm -v /data/tmp:/tmp busybox

通常来说,我们使用共享卷的时候,需要在创建的时候指定这个卷,也就是要记住宿主机的目录的名字。而Docker还支持共享已经启动的容器的存储卷,也就是说,我们只要指定容器名字,就可以读取这个容器内的定义好了的宿主机和容器内目录,且在创建新容器的时候直接应用到新容器中。也就是说,我们先创建一个容器,只要这个容器存在就可以,不一定要启动,只不过在创建这个容器的时候,我们指定他应该使用什么样的存储路径,但是我们不启动他,让他作为其他容器启动时候的架构容器,或者叫模板容器,我们创建新容器的时候复制架构容器的配置来启动新容器。好处就在于,我们可以直接获取架构容器中的设定,当然,如果只是为了存储卷这一个用途,这样有点大材小用。我们前面还学过joined containers,他还可以共享网络的名称空间。

  • 多个容器的卷使用同一个主机目录,例如