Docker:存储卷
存储卷
在Docker中,容器的文件存储结构如下:

容器时基于镜像产生的,一个镜像可以实例化出多个容器,为了节省镜像的存储消耗,并不是每个容器都会拷贝一份镜像,而是所有容器共用一份镜像。
为此,Docker采用联合文件系统来存储文件,在镜像内部,所有文件分为多层存储,每个容器创建时,创建一个容器层,在容器层内部修改文件,不会影响镜像。
比如说容器删除一个文件,容器不会去镜像内部删除镜像的某一层,而是新开一层标记某个文件已经删除。

上图中,容器删除了镜像中的文件,但是其实log.txt没有真的被删除,只是对于这个容器来说,被标记为删除了,其他容器依然可以看到log.txt。
这种存储结构,可以减少很多的存储损耗,但是任何事物都有两面性,没有完美的解决方案。
- 持久化:如果容器被删除,那么整个容器层都直接销毁,数据无法持久化保存到操作系统
- 性能:每次容器读写,都要经过联合文件系统,性能极大降低
为此,Docker提供了另一种存储方式存储卷,用于存储需要持久化,或者IO频率高,对性能有要求的文件。

存储卷可以绕过联合文件系统,直接读写操作系统的文件系统。因为绕过联合文件系统,所以IO效率可以显著提高,另一方面,这些数据并不存储在容器层,而是存储在操作系统,就算容器被删除,容器层被销毁,存储卷也不会被销毁,可以持久化保存。
对于联合文件系统的更多详细信息,会在其他博客有专门讲解,本博客不再深入联合文件系统。
命令
docker volume ls
- 功能:列出存储卷
语法:
            
            
              bash
              
              
            
          
          docker volume ls [option]选项:
- -f:依据条件过滤
- -q:仅显示名称
示例:

此处有三个存储卷,其中my_volume是有名称的,而前两个是没有名称的。DRIVER这一栏,是卷的存储驱动,绝大部分存储卷的驱动都是local,其可以满足大部分场景的需求。
如果加上-q选项,不会显示DRIVER这一列,只显示名称。
使用-f进行过滤:

-f可以对查询进行过滤,并且支持正则表达式,例如-f name=my*表示查询卷名以my开头的存储卷。
=
docker volume create
- 功能:创建存储卷
语法:
            
            
              bash
              
              
            
          
          docker volume create [option] [volume]选项:
- -d --driver:指定驱动,默认为- local
- --label:指定元数据
创建一个匿名存储卷:

创建卷时,如果不指定名称,那么创建的卷就是没有名字的匿名卷。
创建命名卷:

如果需要命名,那么命令的最后一个参数就是卷名。
指定元数据:

创建卷时,可以通过--label指定元数据,以键值对的形式。元数据可以帮助快速查询容器,比如通过-f选项可以指定元数据。
docker volume inspect
- 功能:查看存储卷的详细信息
语法:
            
            
              bash
              
              
            
          
          docker volume inspect [option] volume
此处test_vm_2的信息中,可以看到刚才指定的元数据TEST=2,以及卷名,存储驱动等等。
docker volume rm
- 功能:删除存储卷
语法:
            
            
              bash
              
              
            
          
          docker volume rm [option] volume [volume...]选项:
- -f:强制删除
对于有卷名的存储卷,可以通过卷名删除,如果是匿名的,只能复制一大串随机字符串来指定删除。
如果这个卷正在被容器使用,那么无法直接删除,此时需要通过-f强制删除。
docker volume prune
- 功能:删除所有不使用的匿名存储卷
语法:
            
            
              bash
              
              
            
          
          docker volume prune [option]选项:
- -f:强制删除

第一次查询,主机中有三个匿名卷,通过prune删除后,所有匿名卷都被删除了。过程中会进行一次确认[y/N],使用-f参数,则不会进行这次确认。
分类
以上所有操作,都没有把存储卷和容器关联起来,其实存储卷分为三种类型,不同的存储卷创建方式不同。
- 数据卷:默认映射到宿主机的- /var/lib/docker/volumes目录下,只需要在容器内部指定挂载点,而在宿主机的挂载点,- docker自动完成
- 绑定卷:由用户自己指定宿主机的一个文件或目录,共享到容器中
- 临时卷:前两者在容器销毁后,都可以持久化保存。但是临时卷并不会持久化保存,容器删除后临时卷也会销毁。主要用于高性能IO存储临时数据的场景。
创建以上三种卷,主要在docker run运行容器时,通过-v或者--mount选项进行指定。
数据卷
-v语法:
            
            
              bash
              
              
            
          
          docker run -v name:directory[:option]参数:
- name:绑定的现有卷的名称,可以省略,此时会创建一个新的匿名卷
- directory:数据卷在容器内部的路径
- option:可以指定- ro表示- read only,此时只有宿主机可以修改数据卷,容器只读
后续操作,都基于nginx镜像,在nginx镜像内部,路径/usr/share/nginx/html存放着ngxin的首页文件,以该目录为存储卷位置。
使用现有卷test_vm_1作为数据卷:

通过-v test_vm_1:/usr/share/nginx/html,表示把存储卷test_vm_1这个存储卷,挂载到容器的/usr/share/nginx/html路径下。
随后查询test_vm_1,找到其在宿主机的存储路径,该路径下多出两个文件50x.html和index.html,这就是nginx内部创建的文件,通过数据卷共享到宿主机了。

就算删除掉这个容器,存储卷内的内容依然存在。
创建匿名数据卷:

如果容器创建时,只指定挂载路径,但是没有指定存储卷名,那么就会创建一个匿名卷。以上例子中,创建了一个fb48...的匿名卷,进入其存储目录后,也出现了两个共享的html文件。
另外的,如果指定存储卷名时,存储卷不存在,那么也会自动创建一个新的命名卷。
--mount语法:
            
            
              bash
              
              
            
          
          docker run --mount key=value [key=value ...]参数:
- type:存储卷类型,指定- volume表示数据卷
- stc:对于命名卷,表示卷名,对于匿名卷,此字段省略
- dst:文件在容器的挂载路径
- ro:只读方式挂载
多个参数以逗号,分隔。
通过--volume创建一个命名卷:

存储卷挂载命令如下:
            
            
              bash
              
              
            
          
          --mount type=volume,src=test_vm_3,dst=/usr/share/nginx/html,ro其中type=volume表示使用数据卷,src=test_vm_3表示使用的存储卷,dst=/usr/share/nginx/html指定容器中的挂载路径,ro表示只读,容器内部无法修改该文件。
进入容器内部修改文件:

通过docker exec进入容器后,尝试修改存储卷挂载的目录,此时提示Read-only file system,即这个容器对该目录只读,没有权限进行写入。
不论是-v还是--volume效果是一样的。
绑定卷
对于先前的数据卷,在宿主机的存储路径都是由Docker指定的,如果想要把宿主机的指定文件作为存储卷,共享到容器,就需要绑定卷。
-v语法:
            
            
              bash
              
              
            
          
          docker run -v path:directory[:option]- name:绑定卷在宿主机的路径
- directory:绑定卷在容器内部的路径
- option:可以指定- ro表示- read only,此时只有宿主机可以修改绑定卷,容器只读
除了path这个参数,其它的和数据卷相同。在数据卷中,使用name:来指派一个存储卷,或者不填写该选项,直接创建一个匿名卷。如果填入的是一个有效的宿主机路径path:,那么就会把宿主机的指定目录或文件,作为绑定卷与容器共享。
为nginx容器创建一个绑定卷:

首先在当前目录创建一个index.html文件写入hello world,随后将该文件绑定到容器中,命令:
            
            
              bash
              
              
            
          
          -v ./index.html:/usr/share/nginx/html/index.html此处./index.html是一个有效的本机路径,/usr/share/nginx/html/index.html是容器内部的挂载点。与先前不同,先前指定的是.../html/这个目录,这次指定的是一个具体的.html文件,存储卷既可以共享目录,也可以共享文件。
随后通过docker exec进入容器,查看index.html,发现其内容是hello world。按理来说nginx的这个文件应该初始化为一个有效的html文件,但是最后被宿主机绑定卷的内容给覆盖了。这就是绑定卷与数据卷的区别之一。
绑定卷会把宿主机的文件覆盖容器的文件,哪怕容器的文件原先就有内容。如果宿主机中,绑定卷的内容为空,那么会把容器中的文件也覆盖为空,这会导致一定的数据丢失。但是对于数据卷来说,如果宿主机中内容为空,宿主机会把容器的内容拷贝到宿主机。
之前就可以看出,在数据卷中,把容器内部的50x.html和index.html拷贝到宿主机了。但是如果让绑定卷也绑定这个目录,那么宿主机不仅不会拷贝50x.html和index.html,还会删掉容器内部的50x.html和index.html。
绑定卷创建完成后,宿主机和容器内部的文件就同步了,虽然刚创建时宿主机的文件优先级高,但是创建完成后,容器与宿主机对这个文件的操作权限是相同的,只要不指定readonly。
--volume语法:
            
            
              bash
              
              
            
          
          docker run --mount key=value [key=value ...]参数:
- type:存储卷类型,指定- bind表示绑定卷
- stc:宿主机的有效路径
- dst:文件在容器的挂载路径
- ro:只读方式挂载
多个参数以逗号,分隔。此处的type与数据卷不同,数据卷type=volume,绑定卷type=bind。
通过--volume创建一个绑定卷:

绑定卷命令:
            
            
              bash
              
              
            
          
          --mount type=bind,src=./html,dst=/usr/share/nginx/html把宿主机的./html目录,绑定到容器的/usr/share/nginx/html目录中。ls查看宿主机的./html目录,发现内容为空,进入容器后查看/usr/share/nginx/html目录,依然为空。
原先这个目录应该有两个html文件,但是由于宿主机绑定卷目录为空,所以这两个文件被覆盖删除了。
另外的,绑定卷是无法通过docker volume ls进行查询的。但是可以在容器中查询到,通过docker inspect:

在mounts这一栏,可以看到存储卷的信息,类型为绑定卷bind,宿主机目录source,容器目录Destination,"RW": true表示可读写。
临时卷
存储卷有两个功能,一个是进行持久化的保存,另一个是进行高效的IO。有的时候只需要高效IO,并不需要持久化,那么就可以使用临时卷。
临时卷为了进一步提高IO效率,对文件存储进行了进一步优化。数据卷和绑定卷都是存储在硬盘中的,但是临时卷存储在内存中,是一个内存级的文件,IO效率极高。但是由于内存不是持久化存储的,所以临时卷在容器停止时就会销毁,注意是停止stop时,而不是删除rm时。
注意点:
- 临时卷是Linux专有的,Windows等其它操作系统上的Docker,无法使用临时卷。
- 临时卷只能挂载目录,不能挂载文件
--tmpfs语法:
            
            
              bash
              
              
            
          
          docker run --tmpfs path只需要指定容器中的一个目录为临时卷即可,由于并不于主机互通,无需指定主机路径或者卷名。
创建一个临时卷:

此处把/usr/share/nginx/html目录指定为了临时卷,进入目录后,原先存在的两个html文件也被覆盖了,说明临时卷也会清空目录内的原有内容。
--mount语法:
            
            
              bash
              
              
            
          
          docker run --mount key=value [key=value ...]参数:
- type:存储卷类型,指定- tmpfs表示临时卷
- dst:文件在容器的挂载路径
- tmpfs-size:临时卷的限制大小
- tmpfs-mode:挂载模式,以八进制形式指定目录权限,默认为- 1777
创建一个内存限制不超过1m的临时卷:

临时卷命令:
            
            
              bash
              
              
            
          
          --mount type=tmpfs,dst=/usr/share/nginx/html,tmpfs-size=1m进入容器后,创建一个5m的文件index.html:
            
            
              bash
              
              
            
          
          dd if=/dev/zero of=index.html bs=1M count=5此处的dd命令,用于创建一个5m的空文件。
显示dd: error writing 'index.html': No space left on device表示创建失败,没有空间了。因为限制了临时卷最多使用1m,此处却创建了5m的文件。
最后通过ls -l查看文件,发现index.html的大小是1048576,也就是1024 * 1024 byte = 1m。