容器系列之docker存储卷

docker存储卷
背景:一个程序,对于容器来说,启动时依赖于可能不止一层的镜像,联合挂载启动而成,使用overlay2文件系统,引导最上层的可写层,对于读写层来说,所有在容器中可执行的操作,包括对数据和内容的修改,都是保存在最上层之上的,对于下层内容的操作,假设要删除一个文件,需要使用写时复制。
  - docker镜像由多个只读层叠加面成,启动容器时,docker会加载只读镜像层并在镜像栈顶部加一个读写层
  - 如果运行中的容器修改了现有的一个已经存在的文件,那该文件将会从读写层下面的只读层复制到读写层,该文件版本仍然存在,只是已经被读写层中该文件的副本所隐藏,此即“写时复制(COW)”机制
描述:如果一个文件在最底层是可见的,如果在layer1上标记为删除,最高的层是用户看到的Layer2的层,在layer0上的文件,在layer2上可以删除,但是只是标记删除,用户是不可见的,总之在到达最顶层之前,把它标记来删除,对于最上层的用户是不可见的,当标记一删除,只有用户在最上层建一个同名一样的文件,才是可见的。
对于这类的操作,修改删除等,一般效率非常低,如果对一于I/O要求比较高的应用,如redis在实现持久化存储时,是在底层存储时的性能要求比较高。
假设底层运行一个存储库mysql,mysql本来对于I/O的要求就比较高,如果mysql又是运行在容器中自己的文件系统之上时,也就是容器在停止时,就意味着删除,其实现数据存取时效率比较低,要避免这个限制要使用存储卷来实现。
存储卷:可以想象来在各全局的名称空间中,也就是理解为在宿主机中找一个本地的文件系统,可能存在某一个目录中,直接与容器上的文件系统中的某一目录建立绑定关系。
    类似于挂载一样,宿主机的/data/web目录与容器中的/container/data/web目录绑定关系,然后容器中的进程向这个目录中写数据时,是直接写在宿主机的目录上的,绕过容器文件系统与宿主机的文件系统建立关联关系,使得可以在宿主机和容器内共享数据库内容,让容器直接访问宿主机中的内容,也可以宿主机向容器供集内容,两者是同步的。
    mount名称空间本来是隔离的,可以让两个本来是隔离的文件系统,在某个子路径上建立一定程度的绑定关系,从而使得在两个容器之间的文件系统的某个子路径上不再是隔离的,实现一定程度上共享的效果。
在宿主机上能够被共享的目录(可以是文件)就被称为volume。
优点是容器中进程所生成的数据,都保存在存储卷上,从而脱离容器文件系统自身后,当容器被关闭甚至被删除时,都不用担心数据被丢失,实现数据可以脱离容器生命周期而持久,当再次重建容器时,如果可以让它使用到或者关联到同一个存储卷上时,再创建容器,虽然不是之前的容器,但是数据还是那个数据,特别类似于进程的运行逻辑,进程本身不保存任何的数据,数据都在进程之外的文件系统上,或者是专业的存储服务之上,所以进程每次停止,只是保存程序文件,对于容器也是一样,
容器就是一个有生命周期的动态对象来使用,容器关闭就是容器删除的时候,但是它底层的镜像文件还是存在的,可以基于镜像再重新启动容器。
    但是容器有一个问题,一般与进程的启动不太一样,就是容器启动时选项比较多,如果下次再启动时,很容器会忘记它启动时的选项,所以最好有一个文件来保存容器的启动,这就是容器编排工具的作用。一般情况下,是使用命令来启动操作docker,但是可以通过文件来读,也就读文件来启动,读所需要的存储卷等,但是它也只是操作一个容器,这也是需要专业的容器编排工具的原因。
    另一个优势就是容器就可以不置于启动在那台主机之上了,如几台主机后面挂载一个NFS,在各自主机上创建容器,而容器上通过关联到宿主机的某个目录上,而这个目录也是NFS所挂载的目录中,这样容器如果停止或者是删除都可以不限制于只能在原先的宿主机上启动才可以,可以实现全集群范围内调试容器的使用,当再分配存储、计算资源时,就不会再局限于单机之上,可以在集群范围内建立起来,基本各种docker的编排工具都能实现此功能,但是后面严重依赖于共享存储的使用。
    考虑到容器应用是需要持久存储数据的,可能是有状态的,如果考虑使用NFS做反向代理是没必要存储数据的,应用可以分为有状态和无状态,有状态是当前这次连接请求处理一定此前的处理是有关联的,无状态是前后处理是没有关联关系的,大多数有状态应用都是数据持久存储的,如mysql,redis有状态应用,在持久存储,如nginx作为反向代理是无状态应用,tomcat可以是有状态的,但是它有可能不需要持久存储数据,因为它的session都是保存在内存中就可以的,会导致节点宕机而丢失
session,如果有必要应该让它持久,这也算是有状态的。


应用状态:是否有状态或无状态,是否需要持久存储,可以定立一个正轴坐标系,第一象限中是那些有状态需要存储的,像mysql,redis等服务,有些有有状态但是无需进行存储的,像tomcat把会话保存在内存中时,无状态也无需要存储的数据,如各种反向代理服务器nginx,lvs请求连接都是当作一个独立的连接来调度,本地也不需要保存数据,第四象限是无状态,但是需要存储数据是比较少见。


    运维起来比较难的是有状态且需要持久的,需要大量的运维经验和大量的操作步骤才能操作起来的,如做一个Mysql主从需要运维知识、经验整合进去才能实现所谓的部署,扩展或缩容,出现问题后修复,必须要了解集群的规模有多大,有多少个主节点,有多少个从节点,主节点上有多少个库,这些都要一清二楚,才能修复故障,这些就强依赖于运维经验,无状态的如nginx一安装就可以了,并不复杂,对于无状态的应用可以迅速的实现复制,在运维上实现自动化是很容易的,对于有状态的现状比较难脱离运维人员来管理,即使是k8s在使用上也暂时没有成熟的工具来实现。
    总之:对于有状态的应用的数据,不使用存储卷,只能放在容器本地,效率比较低,而导致一个很严重问题就是无法迁移使用,而且随着容器生命周期的停止,还不能把它删除,只能等待下次再启动状态才可以,如果删除了数据就可能没了,因为它的可写层是随着容器的生命周期而存在的,所以只要持久存储数据,存储卷就是必需的。
docker存储卷难度:对于docker存储卷运行起来并不太麻烦,如果不自己借助额外的体系来维护,它本身并没有这么强大,因为docker存储卷是使用其所在的宿主机上的本地文件系统目录,也就是宿主机有一块磁盘,这块磁盘并没有共享给其他的docker主要,然后容器所使用的目录,只是关联到宿主机磁盘上的某个目录而已,也就是容器在这宿主机上停止或删除,是可以重新再创建的,但是不能调度到其他的主机上,这也是docker本身没有解决的问题,所以docker存储卷默认就是docker所在主机的本地,但是自己搭建一个共享的NFS来存储docker存储的数据,也可以实现,但是这个过程强依赖于运维人员的能力,

使用存储卷的原因
  - 关闭并重启容器,其数据不受影响,但是删除docker容器,则其更改将会全部丢失
  - 存在的问题
存储于联合文件系统中,不易于宿主机访问
容器间数据共享不便
删除容器其数据会丢失
  - 解决方案:卷
卷是容器上一个或多个"目录“,此类目录可绕过联合文件系统,与宿主机上的某目录绑定(关联)

存储卷原理
  - volume于容器初始化之时会创建,由base image提供的卷中的数据会于此期间完成复制
  - volume的初意是独立于容器的生命周期实现数据持久化,因此删除容器之时既不会删除卷,也不会对哪怕未被引用的卷做垃圾回收操作
  - 卷为docker提供了独立于容器的数据管理机制
可以把“镜像”想像成静态文件,例如“程序”,把卷类比为动态内容,例如“数据”,于是,镜像可以重用,而卷可以共享
卷实现了“程序(镜像)"和”数据(卷)“分离,以及”程序(镜像)“和"制作镜像的主机”分离,用记制作镜像时无须考虑镜像运行在容器所在的主机的环境
描述:有了存储卷,如果写在/上,还是存在联合挂载文件系统中,如果要写到卷上,就会写到宿主机关联的目录上,程序运行过程生成的临时数据会写到tmp目录中,也就会在容器的可写层中存储,随着容器被删除而删除,并没太大的影响,只有关键型的数据才会保存在存储卷上.

 

Volume types


- Docker有两种类型的卷,每种类型都在容器中存在一个挂载点,但其在宿主机上位置有所不同;
绑定挂载卷:在宿主机上的路径要人工的指定一个特定的路径,在容器中也需要指定一个特定的路径,两个已知的路径建立关联关系
docker管理卷: 只需要在容器内指定容器的挂载点是什么,而被绑定宿主机下的那个目录,是由容器引擎daemon自行创建一个空的目录,或者使用一个已经存在的目录,与存储卷建立存储关系,这种方式极大解脱用户在使用卷时的耦合关系,缺陷是用户无法指定那些使用目录,临时存储比较适合

在容器中使用volumes

- 为docker run 命令使用-v 选项可使用volume
docker-managed volume
docker run -it -name rbox1 -v /data busybox   #/data指定docker的目录
docker inspect -f {{.Mounts}} rbox1   查看rbox1容器的卷,卷标识符及挂载的主机目录

bind-mount volume
docker run -it -v HOSTDIR:VOLUMEDIR --name rbox2 busybox  #宿主机目录:容器目录
docker inspect -f {{.Mounts}} rbox2

 

实操:docker管理卷

[root@node1 ~]# docker run --name b2 -it -v /data busybox
/ # ls /
bin   data  dev   etc   home  proc  root  sys   tmp   usr   var  #data默认是不存在的
[root@node1 ~]# docker inspect b2|grep -A 5  Mounts
        "Mounts": [
            {
                "Type": "volume",
                "Name": "fd6c6484126148c8d2876f068175ba3998b8f442a2635d94daeef87ca8721c90",
                "Source": "/var/lib/docker/volumes/fd6c6484126148c8d2876f068175ba3998b8f442a2635d94daeef87ca8721c90/_data",  宿主上的目录
                "Destination": "/data",     容器中的目录

宿主机: 可以很方便实现在宿主机和容器之间共享目录

[root@node1 ~]# cd /var/lib/docker/volumes/fd6c6484126148c8d2876f068175ba3998b8f442a2635d94daeef87ca8721c90/_data
[root@node1 _data]# echo "hello reid" > testreid.html
[root@node1 _data]# cat testreid.html 
hello reid
reid

容器

/ # cat /data/testreid.html      
hello reid
/ # echo reid >> /data/testreid.html 

docker绑定卷

[root@node1 ~]# docker run --name b2 -it --rm -v /docker/volume/b2:/data busybox   ##目录会自动创建
/ # ls
bin   data  dev   etc   home  proc  root  sys   tmp   usr   var
[root@node1 _data]# docker inspect b2 |grep Mounts -A 5
        "Mounts": [
            {
                "Type": "bind",
                "Source": "/docker/volume/b2",   宿主机目录
                "Destination": "/data",          容器目录
                "Mode": "",
[root@node1 _data]# echo reid > /docker/volume/b2/reid.html
[root@node1 _data]# cat /docker/volume/b2/reid.html
reid

持久的实现

[root@node1 ~]# docker run --name b2 -it --rm -v /docker/volume/b2:/data busybox
/ # exit   
[root@node1 _data]# ls /docker/volume/b2/reid.html   容器退出删除后,宿主机上的目录还存在,而还可以基于它的基础上开启目录
/docker/volume/b2/reid.html
[root@node1 ~]# docker run --name b2 -it --rm -v /docker/volume/b2:/data/web busybox
/ # cat /data/web/reid.html 
reid

使用golong模板来查看

[root@node1 _data]# docker inspect -f {{.Mounts}} b2    #注意{{}}中使用根开始写
[{bind  /docker/volume/b2 /data/web   true rprivate}]
[root@node1 _data]# docker inspect -f {{.NetworkSettings}} b2
{{ 1d11d66176eefb49868940f3a35cca51842da2518878e4f9eb1a7c1534d4680e false  0 map[] /var/run/docker/netns/1d11d66176ee [] []} {7c465c22a5794bf473ca54a492555832ee0308472b595889e25718cc7a6ddeac 10.0.0.1  0 10.0.0.3 16  02:42:0a:00:00:03} map[bridge:0xc420186180]}
[root@node1 _data]# docker inspect -f {{.NetworkSettings.IPAddress}} b2
10.0.0.3

 

场景:一个docker容器可以关联到宿主机的目录中,也可以让两个docker容器同时关联到同一个宿主机的目录中,实现共享使用同一个存储卷,容器之间的数据共享

[root@node1 _data]# docker run --name b3 -it --rm -v /docker/volume/b2:/data busybox  #再添加一个容器,可以再使用同一个目录
/ # cat /data/reid.html 
reid
/ # echo reid2 >> /data/reid.html 
[root@node1 ~]# docker run --name b2 -it --rm -v /docker/volume/b2:/data/web busybox
/ # cat /data/web/reid.html 
reid
reid2

  

场景:需要多个容器同进使用多个卷,卷在那里写每次初始化时都要使用-v来指定,如果不想记录这个路径,docker还支持复制其他的存储卷路径
实现:制定一个容器,不执行任何任务,创建时,只要指定它的存储路径,作为其他相关联容器的基础架构容器,其他的容器启动时去复制它的存储卷设置,但是这样的点浪费,不过使用joined container的基础的话,几个容器本来就有密切的关系,如nginx+tomcat,nginx的容器和tomcat容器共享一个底层的网络,有一个对外的接口,有一个loop接口,这样80给nginx,在内loop给tomcat,请求进来,nginx作为反射代理转给tomcat就可以了,再加一个mysql,也是使用loop接口来通讯。
让它们共享网络名称空间中的uts,net,ipc,还可以共享存储卷,ngInx处理静态,tomcat处理动态的,在同一个目录下,使用存储卷来解决这个问题,这种组织方式使用构建应用。


共享卷
- 多个容器的卷使用同一个主机目录,如

  docker run -it --name c1 -v /docker/volumes/v1:/data/ busybox

- 复制使用其他容器的卷,为docker run 命令使用--volumes-from选项

  docker run -it --name bbox1 -v /docker/volumes/v1:/data busybox
  docker run -it --name bbox2 --volumes-from bbox1 busybox

制定基础镜像(网上有专门制作基础架构容器的,不用启动,只要创建就可以了)

[root@node1 ~]# docker run --name basiccon -it -v /data/basic/volume/:/data/web/html busybox
/ # ls /data/
web
[root@node1 ~]# docker run --name nginx --network container:basiccon --volumes-from basiccon -it busybox   #加入网络,同时复制卷
/ # ls /data/
web

 

posted @ 2018-10-19 14:59  Reid21  阅读(1653)  评论(0编辑  收藏  举报