Docker:存储卷
存储卷
在Docker
中,容器的文件存储结构如下:
容器时基于镜像产生的,一个镜像可以实例化出多个容器,为了节省镜像的存储消耗,并不是每个容器都会拷贝一份镜像,而是所有容器共用一份镜像。
为此,Docker
采用联合文件系统来存储文件,在镜像内部,所有文件分为多层存储,每个容器创建时,创建一个容器层,在容器层内部修改文件,不会影响镜像。
比如说容器删除一个文件,容器不会去镜像内部删除镜像的某一层,而是新开一层标记某个文件已经删除。
上图中,容器删除了镜像中的文件,但是其实log.txt
没有真的被删除,只是对于这个容器来说,被标记为删除了,其他容器依然可以看到log.txt
。
这种存储结构,可以减少很多的存储损耗,但是任何事物都有两面性,没有完美的解决方案。
持久化
:如果容器被删除,那么整个容器层都直接销毁,数据无法持久化保存到操作系统性能
:每次容器读写,都要经过联合文件系统,性能极大降低
为此,Docker
提供了另一种存储方式存储卷
,用于存储需要持久化,或者IO频率高,对性能有要求的文件。
存储卷可以绕过联合文件系统,直接读写操作系统的文件系统。因为绕过联合文件系统,所以IO效率可以显著提高,另一方面,这些数据并不存储在容器层,而是存储在操作系统,就算容器被删除,容器层被销毁,存储卷也不会被销毁,可以持久化保存。
对于联合文件系统的更多详细信息,会在其他博客有专门讲解,本博客不再深入联合文件系统。
命令
docker volume ls
功能
:列出存储卷
语法:
docker volume ls [option]
选项:
-f
:依据条件过滤-q
:仅显示名称
示例:
此处有三个存储卷,其中my_volume
是有名称的,而前两个是没有名称的。DRIVER
这一栏,是卷的存储驱动,绝大部分存储卷的驱动都是local
,其可以满足大部分场景的需求。
如果加上-q
选项,不会显示DRIVER
这一列,只显示名称。
使用-f
进行过滤:
-f
可以对查询进行过滤,并且支持正则表达式,例如-f name=my*
表示查询卷名以my
开头的存储卷。
=
docker volume create
功能
:创建存储卷
语法:
docker volume create [option] [volume]
选项:
-d --driver
:指定驱动,默认为local
--label
:指定元数据
创建一个匿名存储卷:
创建卷时,如果不指定名称,那么创建的卷就是没有名字的匿名卷。
创建命名卷:
如果需要命名,那么命令的最后一个参数就是卷名。
指定元数据:
创建卷时,可以通过--label
指定元数据,以键值对的形式。元数据可以帮助快速查询容器,比如通过-f
选项可以指定元数据。
docker volume inspect
功能
:查看存储卷的详细信息
语法:
docker volume inspect [option] volume
此处test_vm_2
的信息中,可以看到刚才指定的元数据TEST=2
,以及卷名,存储驱动等等。
docker volume rm
功能
:删除存储卷
语法:
docker volume rm [option] volume [volume...]
选项:
-f
:强制删除
对于有卷名的存储卷,可以通过卷名删除,如果是匿名的,只能复制一大串随机字符串来指定删除。
如果这个卷正在被容器使用,那么无法直接删除,此时需要通过-f
强制删除。
docker volume prune
功能
:删除所有不使用的匿名存储卷
语法:
docker volume prune [option]
选项:
-f
:强制删除
第一次查询,主机中有三个匿名卷,通过prune
删除后,所有匿名卷都被删除了。过程中会进行一次确认[y/N]
,使用-f
参数,则不会进行这次确认。
分类
以上所有操作,都没有把存储卷和容器关联起来,其实存储卷分为三种类型,不同的存储卷创建方式不同。
数据卷
:默认映射到宿主机的/var/lib/docker/volumes
目录下,只需要在容器内部指定挂载点,而在宿主机的挂载点,docker
自动完成绑定卷
:由用户自己指定宿主机的一个文件或目录,共享到容器中临时卷
:前两者在容器销毁后,都可以持久化保存。但是临时卷并不会持久化保存,容器删除后临时卷也会销毁。主要用于高性能IO存储临时数据的场景。
创建以上三种卷,主要在docker run
运行容器时,通过-v
或者--mount
选项进行指定。
数据卷
-v
语法:
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
语法:
docker run --mount key=value [key=value ...]
参数:
type
:存储卷类型,指定volume
表示数据卷stc
:对于命名卷,表示卷名,对于匿名卷,此字段省略dst
:文件在容器的挂载路径ro
:只读方式挂载
多个参数以逗号,
分隔。
通过--volume
创建一个命名卷:
存储卷挂载命令如下:
--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
语法:
docker run -v path:directory[:option]
name
:绑定卷在宿主机的路径directory
:绑定卷在容器内部的路径option
:可以指定ro
表示read only
,此时只有宿主机可以修改绑定卷,容器只读
除了path
这个参数,其它的和数据卷相同。在数据卷中,使用name:
来指派一个存储卷,或者不填写该选项,直接创建一个匿名卷。如果填入的是一个有效的宿主机路径path:
,那么就会把宿主机的指定目录或文件,作为绑定卷与容器共享。
为nginx
容器创建一个绑定卷:
首先在当前目录创建一个index.html
文件写入hello world
,随后将该文件绑定到容器中,命令:
-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
语法:
docker run --mount key=value [key=value ...]
参数:
type
:存储卷类型,指定bind
表示绑定卷stc
:宿主机的有效路径dst
:文件在容器的挂载路径ro
:只读方式挂载
多个参数以逗号,
分隔。此处的type
与数据卷不同,数据卷type=volume
,绑定卷type=bind
。
通过--volume
创建一个绑定卷:
绑定卷命令:
--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
语法:
docker run --tmpfs path
只需要指定容器中的一个目录为临时卷即可,由于并不于主机互通,无需指定主机路径或者卷名。
创建一个临时卷:
此处把/usr/share/nginx/html
目录指定为了临时卷,进入目录后,原先存在的两个html
文件也被覆盖了,说明临时卷也会清空目录内的原有内容。
--mount
语法:
docker run --mount key=value [key=value ...]
参数:
type
:存储卷类型,指定tmpfs
表示临时卷dst
:文件在容器的挂载路径tmpfs-size
:临时卷的限制大小tmpfs-mode
:挂载模式,以八进制形式指定目录权限,默认为1777
创建一个内存限制不超过1m
的临时卷:
临时卷命令:
--mount type=tmpfs,dst=/usr/share/nginx/html,tmpfs-size=1m
进入容器后,创建一个5m
的文件index.html
:
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
。