什么是存储卷?
存储卷就是将宿主机的本地文件系统中存在的某个目录直接与容器内部的文件系统上的某一目录建立绑定关系。这就意味着,当我们在容器中的这个目录下写入数据时,容器会将其内容直接写入到宿主机上与此容器建立了绑定关系的目录。在宿主机上的这个与容器形成绑定关系的目录被称作存储卷。
卷的本质是文件或者目录,它可以绕过默认的联合文件系统,直接以文件或目录的形式存在于宿主机上。
宿主机的 /data/web
目录与容器中的 /container/data/web
目录绑定关系,然后容器中的进程向这个目录中写数据时,是直接写在宿主机的目录上的,绕过了容器文件系统,与宿主机的文件系统建立关联关系,实现了宿主机和容器共享数据共享。让容器直接访问宿主机中的内容,也可以宿主机向容器写入内容,容器和宿主机的数据读写是同步的。
不好理解的话,可以将这个过程看作是在你的 Windows 电脑上插了个 U 盘。其实也没有什么不好理解的,可以先看看后面的实战部分再来理解。
为什么需要存储卷?
-
数据丢失问题
容器按照业务类型,总体可以分为两类:- 无状态的(数据不需要被持久化)
- 有状态的(数据需要被持久化)
显然,容器更擅长无状态应用。因为未持久化数据的容器根目录的生命周期与容器的生命周期一样,容器文件系统的本质是在镜像层上面创建的读写层,运行中的容器对任何文件的修改都存在于该读写层,当容器被删除时,容器中的读写层也会随之消失。虽然容器希望所有的业务都尽量保持无状态,这样容器就可以开箱即用,并且可以任意调度,但实际业务总是有各种需要数据持久化的场景,比如 MySQL、Kafka 等有状态的业务。因此为了解决有状态业务的需求,Docker 提出了卷(Volume)的概念。
-
性能问题
UnionFS 对于修改删除等,一般效率非常低,如果对一于 I/O 要求比较高的应用,使用 Docker 的联合文件系统可能就不是非常适合了!如 redis 在实现持久化存储时,在底层存储时的性能要求比较高。 -
宿主机和容器互访不方便
宿主机访问容器,或者容器访问要通过docker cp
来完成,操作非常不方便。 -
容器和容器共享不方便
每个 Docker 容器在运行时都是隔离的,拥有自己独立的文件系统、进程空间和网络环境等。这种隔离性保证了容器的安全性和稳定性,但同时也带来了一些限制,如文件共享的不便。我们即使是使用docker cp
命令也无法直接将文件从一个容器拷贝到另一个文件。
存储卷分类
目前 Docker 提供了三种方式将数据从宿主机挂载到容器中。
volume docker
管理卷,默认映射到宿主机的/var/lib/docker/volumes
目录下,只需要在容器内指定容器的挂载点是什么,而被绑定宿主机下的那个目录,是由 Docker 容器引擎 daemon 自行创建一个空的目录,或者使用一个已经存在的目录,与存储卷建立存储关系,这种方式极大解脱用户在使用卷时的耦合关系,缺陷是用户无法指定那些使用目录,适用于临时存储。bind mount
绑定数据卷,映射到宿主机指定路径下,在宿主机上的路径要人工的指定一个特定的路径,在容器中也需要指定一个特定的路径,两个已知的路径建立关联关系。tmpfs mount
临时数据卷,映射到于宿主机内存中,一旦容器停止运行,tmpfs mount
会被移除,数据就会丢失,用于高性能的临时数据存储。
管理卷 Volume
Volume 命令操作
docker volume create
- 功能:创建存储卷。
- 语法:
docker volume create [OPTIONS] [VOLUME]
- 参数:
- -d 或 –drive:用于指定创建数据卷时所使用的存储驱动。这个不用管使用默认的
local
就行了。 - –label:该选项允许为存储卷添加元数据标签。这些标签是以键值对的形式存储的,可以用于标识、分类或过滤卷。通过给存储卷添加标签,你可以更灵活地管理和查询存储卷。
- 演示:
如下图,我们不指定创建的管理卷名称,Docker 会给我随机生成一个管理卷的名称。当我们指定创建出的管理卷的名称就使用自己指定的!我们在创建时指定的元数据标签可以通过 docker volume inspect
查看!其次,通过该命令还能查看挂载点,即 Docker 引擎自行创建的一个空目录,可以用于将来容器的绑定!
可以看到确实是一个空的目录:
docker volume inspect
- 功能:查看一个或者多个卷的详细信息。
- 语法:
docker volume inspect [OPTIONS] VOLUME [VOLUMES]
- 参数:
- -f:指定返回的文件格式,例如:json。
- 演示:
通过docker volume inspect
我们能看到卷的详细信息。包括:卷的创建时间,卷的驱动,卷的标签,卷的挂载点,卷的名字等等。
docker volume ls
-
功能:列出所有卷。
-
语法:
docker volume ls [OPTIONS]
-
别名:
docker volume list
-
参数:
- -f 或 –filter:按指定条件过滤。
- -q:仅显示名称。
- –format:指定返回的格式,如:json。
-
演示:
我们可以列出全部的卷,也可以按照条件筛选。
是 purpose 哈,单词刚刚拼写错了,不好意思哈!😭😭😭😭😭😭😭😭
docker volume rm
- 功能:删除卷,前提条件是容器不使用!
- 语法:
docker volume rm [OPTIONS] VOLUME [VOLUME...]
- 参数:
- -f 或 –force:强制删除。
- 演示:
如下图:我们尝试删除一个正在被容器使用的卷,是会直接报错的!!!
我们将这个容器停止之后还是不能删除,除非加上 -f
选项,或者将容器删除!!
docker volume prune
- 功能:删除不使用的本地卷
- 语法:
docker volume prune [OPTIONS]
- 参数:
- -f 或 –force:不提示是否删除。
- –filter:过滤。
- 演示:
如下图,我们直接将本地不使用的卷全部删除。
可以看到,他不能删除我们手动命名的卷!
-v
参数
- 功能:完成目录映射,之前我们使用 docker volume create 知识创建了管理卷,我们需要在容器创建的时候指定
-v
参数来完成容器目录和管理卷的绑定(映射)。 - 语法:
docker run -v name:directory[:options]
- 参数:
- name:卷的名称。
- directory:卷映射到容器的目录,不可以使用相对路径!。
- options:选项,如 ro 表示 readonly,只读。
- 演示:
如下图:我们在用nginx
镜像启动并运行一个容器的时候指定-v
选项,将nginx
服务器首页的index.html
文件所在的目录绑定到myvolume1
这个管理卷上。我们查看myvolume1
的挂载点,并且看一看里面的内容!可以看到内容符合我们的预期!
现在,我们在宿主机上对index.html
文件的内容进行修改,然后通过浏览器访问,看主页会不会发生变化!我们使用 vim 打开index.html
,然后对内容进行修改。我在网上找了一个相对好看的html css
页面!!
使用浏览器访问,发现首页的确发生了变化,说明容器绑定存储卷之后,目录下的文件在宿主机和容器是共享的!!
同理,对容器目录下文件的修改宿主机也是可以看到的!前提是该目录下的文件可以修改哈!
如下图:我们在容器内对目录下的文件做修改,在宿主机上能看到修改之后的内容。
我们在启动容器的时候在 -v
选项后面加上 ro
表示只读,这样的话,在容器中就没有办法对 绑定(映射) 的目录做更改了!
如下图:指定 ro
选项之后,我们尝试在容器中对映射目录下的文件做更改会直接报错!!!
我们退出容器,在宿主机上对文件进行修改是没有问题的!
如下图:当我们使用 -v
选项指定一个不存在的管理卷时,Docker 引擎会自动帮我们创建这个管理卷。
我们可以使用 docker inspect
命令,在容器的详细信息中看到卷的挂载信息!Type
是 volume
表示是管理卷哈!
--mount
参数
-
功能:完成目录映射。
-
语法:
--mount '<key>=<value>, <key>=<value>'
-
参数:
- type:存储卷的类型,
bind
表示绑定数据卷,volume
表示存储卷,tmpfs
表示临时数据卷。 - source 或 src:对于命名卷,这是卷的名称;对于匿名卷,省略这个字段即可!
- destination 或 dst 或 target:文件或目录挂载在容器中的路径,不可以使用相对路径!!。
- ro 或 readonly:只读方式挂载。
- type:存储卷的类型,
-
演示:
如下图:我们使用 nginx 镜像启动并运行一个容器!不指定type
默认是管理卷 (volume),不指定src
会创建一个匿名卷,也就是 Docker 引擎帮我们创建一个,名字随机!
如果想设置为只读:只读不能使用匿名卷哦!
docker run -d --name mynginx1 -p 9999:80 --mount src=myvolume1, dst=/usr/share/nginx/html, readonly nginx:1.25.4
同样地,使用 --mount
选项,如果 src
指定的卷不存在,会自动创建的哦!
我们使用 nginx 镜像启动并运行一个容器,使用 --mount
参数,并且不指定 type
。
docker run -d --name mynginx1 -p 9999:80 --mount src=myvolume5,dst=/usr/share/nginx/html nginx:1.25.4
可以看到,默认的 type
就是 volume
即管理卷!
绑定卷 bind
-v
参数和 --mount
参数都可以完成绑定卷的创建。
-v
参数
- 功能:完成卷映射。
- 语法:
docker run -v name:directory[:options]
- 参数:
- name:必须是宿主机的目录,这个和管理卷不同。不可以使用相对路径!
- directory:卷映射到容器的目录,不可以使用相对路径!
- options:选项,如 ro 表示 readonly,只读。
- 演示:
如下图:我们使用-v
选项创建一个绑定卷,因为我之前没有创建/root/test_bind
这样的目录,Docker 就会自动帮我们创建!我们查看这个目录下的内容,发现啥也没有!
这也就意味着容器中的/usr/share/nginx/html
目录下也是空的。使用浏览器访问就会访问出错!
可见绑定卷和管理卷在这一方面还是有差别的,管理卷能将容器映射目录中的内容进行同步,而绑定卷只在乎宿主机目录的内容!
同样,我们可以通过 docker inspect mynginx1
命令来查看容器的挂载情况。可以看到 type
是 bind
表示绑定卷
--mount
参数
- 功能:完成卷映射。
- 语法:
--mount '<key>=<value>, <key>=<value>'
- 参数:
- type:存储卷的类型,
bind
表示绑定数据卷,volume
表示存储卷,tmpfs
表示临时数据卷。 - source 或 src:对于命名卷,这是卷的名称;对于匿名卷,省略这个字段即可!
- destination 或 dst 或 target:文件或目录挂载在容器中的路径,不可以使用相对路径!!。
- ro 或 readonly:只读方式挂载。
- type:存储卷的类型,
- 演示:
我们使用以下命令,以--mount
选项创建一个绑定卷!然后通过docker inspect
查看容器的 Mounts 信息。
docker run -d --name mynginx1 -p 9999:80 --mount type=bind,src=/root/test_bind,dst=/usr/share/nginx/html nginx:1.25.4
临时卷 tmpfs
临时卷数据位于内存中,在容器和宿主机之外。
tmpfs 局限性
- 不同于卷和绑定挂载,不能在容器之间共享 tmpfs 挂载。
- 这个功能只有在 Linux 上运行 Docker 时才可用。Windows 用不了,嘻嘻嘻!
--tmpfs
选项创建临时卷
- 功能:完成临时卷映射。
- 语法:
--tmpfs DPCKER_PATH
- 演示:
我们使用docker run
命令运行容器的时候加上--tmpfs
选项就可以啦!
docker run -d --name mynginx1 -p 9999:80 --tmpfs /usr/share/nginx/html nginx:1.25.4
我们使用 docker inspect
看容器的 Mounts 信息,是空的!
但是里面有一个 Tmpfs 的字段可以看到挂载信息:
--mount
选项创建临时卷
- 功能:完成目录映射。
- 语法:
--mount '<key>=<value>, <key>=<value>'
- 参数:
- type:存储卷的类型,
bind
表示绑定数据卷,volume
表示存储卷,tmpfs
表示临时数据卷。 - destination 或 dst 或 target:文件或目录挂载在容器中的路径,不可以使用相对路径!!
- tmpfs-size:tmpsfs 挂载的大小 (以字节为单位),默认无限制。
- tmps-mode:tmpfs 的八进制文件模式。
- type:存储卷的类型,
- 演示:
docker run -d --name mynginx1 -p 9999:80 --mount type=tmpfs,dst=/usr/share/nginx/html nginx:1.25.4
可以看到使用 --mount
选项创建的容器,还是能在 Mouts 字段中看到 tmpfs 的。
我们去看一下容器中目录的内容哈:可以看到里面啥也没有,说明 tmpfs 会覆盖容器目录原有的内容!!!
我们向 index.html
里面写点东西:用浏览器访问是没有任何问题的!
echo "hello tmpfs" > /usr/share/nginx/html/index.html
然后我们回到宿主机,重启这个容器,再次访问浏览器!可以看到原来写入到 index.html
文件的内容没了!说明 tmpfs
是内存级的文件,一旦容器重启数据就会丢失!
docker restart mynginx1
docker 卷的生命周期
如下图:我们创建一个管理卷,并使用 nginx 镜像启动一个容器,并且将 nginx 服务器的首页绑定到这个管理卷上。然后,我们将容器停止并删除。最后在宿主机上查看挂载点目录下的文件,发现文件并没有随着容器的消失而消失。
以上结论对绑定卷同样适用!
Docker 卷共享
如下图,我们创建一个绑定卷,指定目录为 /root/test_bind
。然后使用 nginx 镜像运行多个容器,将这些容器中的 nginx 服务器的主页目录挂载到 root/test_bind
上!
# 第一个容器
docker run -d --name mynginx1 -p 9997:80 --mount type=bind,src=/root/test_bind,dst=/usr/share/nginx/html nginx:1.25.4
# 第二个容器
docker run -d --name mynginx2 -p 9998:80 --mount type=bind,src=/root/test_bind,dst=/usr/share/nginx/html nginx:1.25.4
# 第三个容器
docker run -d --name mynginx3 -p 9999:80 --mount type=bind,src=/root/test_bind,dst=/usr/share/nginx/html nginx:1.25.4
我们进入到宿主机的 /root/test_bind
目录下,在里面创建一个 index.html
的文件,然后再网上找一个页面源码,粘贴到 index.html
文件中!
在我们创建这个 index.html
文件之前,访问三个 nginx
服务器都是 403 forbidden
。创建之后逐个刷新,可以看到每个 nginx 服务器的主页都发生了变化!这就是 Docker
卷共享啦!
文件消失了?
如下图:我们使用 busybox
镜像运行一个容器,进入容器之后,我们创建一个文件,退出容器。在宿主机上使用 find 命令查找这个文件!我们发现是能够在宿主机上找到这个文件的!
这是为什呢?docker 容器不是使用了 namespace 资源隔离和 cgroups 资源控制实现与宿主机的隔离了吗?为什么还能看到容器中的文件呢?
- 在使用镜像创建容器时,实际上是在镜像的文件系统之上创建了一个可写层。这个可写层是容器的一部分,用于存储容器运行时产生的数据、文件变动以及新创建的文件等。当你在容器内创建新文件或者对现有文件进行修改时,这些操作实际上都是在容器的可写层上进行的,而镜像的文件系统则保持不变。
- 当使用容器技术创建一个容器时,基础镜像中的文件系统是只读的,也就是说用户无法直接修改基础镜像中的文件。这样做的好处是可以确保基础镜像的完整性和稳定性,因为任何对基础镜像的修改都会被保存在容器的可写层中,而不会影响到基础镜像本身。容器技术通过使用只读的基础镜像和可写的容器层来实现文件的修改和添加,同时保证了基础镜像的稳定性和可重复性。
- 镜像中的文件系统在宿主机上一般是不可见的。镜像是一个静态的文件,它包含了容器运行所需的文件系统结构和配置信息。这个文件系统在宿主机上实际上是以一种特殊的格式(比如 Docker 的aufs、overlayfs等)存储在宿主机的文件系统中的。当你运行一个容器时,容器引擎会将镜像中的文件系统加载到容器的文件系统命名空间中,这样容器就可以访问并使用这些文件了。但是,在宿主机上,你不能直接看到镜像中的文件系统,也无法直接对其进行操作。
- 当创建一个容器时,实际上是在宿主机上创建了一个与宿主机系统隔离但可以共享部分资源的进程。容器的文件系统,包括可写层,通过一种叫做联合文件系统(Union File System)的技术,在宿主机的文件系统上进行了一种特殊的挂载。
- 基础镜像的只读部分:容器启动时会加载一个基础镜像,这个基础镜像包含了容器所需的文件系统结构,但是这部分文件系统是只读的,宿主机不能对其进行修改。
- 容器的可写层:在基础镜像之上,容器会有一个可写层,用于存储容器内的运行时数据,比如应用程序的日志、数据库的数据等。这个可写层是宿主机上的一个特殊目录,它通过联合文件系统技术与基础镜像的只读部分进行合并,形成了容器的完整文件系统。
- 宿主机的文件系统:在宿主机上,容器的可写层实际上是以一种特殊的方式挂载到了宿主机的文件系统中。这种挂载使得宿主机可以看到容器的可写层,也可以对其进行读写操作。
- 这种特殊的挂载就可以使用我们今天学的存储卷来实现。
现在我们使用 tmpfs
与容器中的特定目录 /app
绑定,然后在容器中的这个目录下创建一个文件,再次在宿主机上查找这个文件,来看看找不找得到哈!
由实验现象可以进一步证明 tmpfs
是内存级文件!并且可以看到 tmpfs
的隐私性很高呢!
什么时候用 volume,什么时候用 bind、tmpfs?
volume
:volume 是docker
的宿主机文件系统一部分,用于不需要规划具体目录的场景。bind
:bind mount
完全是依赖于主机的目录结构和操作系统,用于目录需要提前规划,比如mysql
的目录需要个空间大的,其他服务有不占用的时候,用volume
就不太合适了。tmpfs
:用于敏感文件存储,文件不想存储的宿主机和容器的可写层之中。
存储卷在实际研发中带来了哪些问题?
- 跨主机使用:
docker
存储卷是使用其所在的宿主机上的本地文件系统目录,也就是宿主机有一块磁盘,这块磁盘并没有共享给其他的docker
主机,容器在这宿主机上停止或删除,是可以重新再创建的,但是不能调度到其他的主机上,这也是docker
本身没有解决的问题,所以docker
存储卷默认就是docker
所在主机的本地。虽然自己搭建一个共享的NFS
来存储docker
存储的数据,也可以实现,但是这个过程强依赖于运维人员的能力。所以为了满足未来应用的存储和数据分离,越来越多的分布式存储方案出现,如 s3 系列,nfs 等。 - 启动参数未知:
容器有一个问题,一般与进程的启动不太一样,就是容器启动时选项比较多,如果下次再启动时,很容器会忘记它启动时的选项,所以最好有一个文件来保存容器的启动,这就是容器编排工具的作用。一般情况下,是使用命令来启动操作docker
,但是可以通过文件来读,也就读文件来启动,读所需要的存储卷等,但是它也只是操作一个容器,如果要几十上百个容器操作,就需要专业的容器编排工具这种一般像开源的 k8s,各个云厂商也有自己的企业版编排软件。 - 复杂场景仍然需要运维:
对于有状态要持久的集群化组件,如MySQL
的主从。部署维护一个MySQL
主从需要运维知识、经验整合进去才能实现所谓的部署,扩展或缩容,出现问题后修复,必须要了解集群的规模有多大,有多少个主节点,有多少个从节点,主节点上有多少个库,这些都要一清二楚,才能修复故障,这些就强依赖于运维经验这种复杂的场景往往还是需要人力,很难有完美的工具出现。