一、什么是存储卷
存储卷就是将宿主机的本地文件系统中存在的某个目录直接与容器内部的文件系统上的某一目录建立绑定关系。这就意味着,当我们在容器中的这个目录下写入数据时,容器会将其内容直接写入到宿主机上与此容器建立了绑定关系的目录。
在宿主机上的这个与容器形成绑定关系的目录被称作存储卷。卷的本质是文件或者目录,它可以绕过默认的联合文件系统,直接以文件或目录的形式存在于宿主机上。
宿主机的 /data/web 目录与容器中的 /container/data/web 目录绑定关系,然后容器中的进程向这个目录中写数据时,是直接写在宿主机的目录上的,绕过容器文件系统与宿主机的文件系统建立关联关系,使得可以在宿主机和容器内共享数据库内容,让容器直接访问宿主机中的内容,也可以宿主机向容器写入内容,容器和宿主机的数据读写是同步的。容器的销毁并不会影响存储卷内的内容。

二、生活案例
存储卷就相当于租了个地下室,建立对应的映射就相当于拥有了这个地下室的钥匙,能够使用房子以外的空间。如果有一天龙卷风来了,房子被吹毁了,但是地下室依然安然无恙,就相当于容器销毁了我们的这个地下室依然没有影响。
三、为什么需要存储卷
1、数据丢失问题
容器按照业务类型,总体可以分为两类:
- 无状态的(数据不需要被持久化)。
- 有状态的(数据需要被持久化)。
显然,容器更擅长无状态应用。因为未持久化数据的容器根目录的生命周期与容器的生命周期一样,容器文件系统的本质是在镜像层上面创建的读写层,运行中的容器对任何文件的修改都存在于该读写层,当容器被删除时,容器中的读写层也会随之消失。
虽然容器希望所有的业务都尽量保持无状态,这样容器就可以开箱即用,并且可以任意调度,但实际业务总是有各种需要数据持久化的场景,比如 MySQL、Kafka 等有状态的业务。因此为了解决有状态业务的需求,Docker 提出了卷(Volume)的概念。
2、性能问题
UnionFS 对于修改删除等,一般效率非常低,如果对一于 I/O 要求比较高的应用,如 redis 在实现持化存储时,是在底层存储时的性能要求比较高。
3、宿主机和容器互访不方便
宿主机访问容器,或者容器访问要通过 docker cp 来完成,应用很难操作。
4、容器和容器共享不方便
四、存储卷分类
目前 Docker 提供了三种方式将数据从宿主机挂载到容器中:
- volume docker 管理卷,默认映射到宿主机的 /data/var/lib/docker/volumes 目录(默认是 /var/lib/docker/volumes,我的是因为做了修改)下,只需要在容器内指定容器的挂载点是什么,而被绑定宿主机下的那个目录,是由容器引擎 daemon 自行创建一个空的目录,或者使用一个已经存在的目录,与存储卷建立存储关系,这种方式极大解脱用户在使用卷时的耦合关系,缺陷是用户无法指定那些使用目录,临时存储比较适合。
- bind mount 绑定数据卷,映射到宿主机指定路径下,在宿主机上的路径要人工的指定一个特定的路径,在容器中也需要指定一个特定的路径,两个已知的路径建立关联关系。
- tmpfs mount 临时数据卷,映射到于宿主机的内存中,一旦容器停止运行,tmpfs mounts 会被移除,数据就会丢失,用于高性能的临时数据存储。

五、管理卷
1、创建卷
存储卷可以通过命令方式创建,也可以在创建容器的时候通过 -v and --mount 指定。
(1)方式一(Volume 命令操作)
命令清单如下:

A. docker volume create
a. 功能
创建存储卷。
b. 语法
docker volume create [OPTIONS] [VOLUME]
c. 关键参数
- -d,--driver:指定驱动,默认是 local。
- --label:指定元数据。
d. 样例
匿名卷:
![]()
命名卷:
![]()
B. docker volume inspect
a. 功能
查看卷详细信息。
b. 语法
docker volume inspect [OPTIONS] VOLUME [VOLUME...]
c. 关键参数
- -f:指定相应个格式,如 json。
d. 样例

C. docker volume ls
a. 功能
列出卷。
b. 语法
docker volume ls [OPTIONS]
c. 关键参数
- --format:指定相应个格式,如 json、table。
- --filter,-f:过滤。
- -q:仅显示名称。
d. 样例
![]()

D. docker volume rm
a. 功能
删除卷,需要容器不使用。
b. 语法
docker volume rm [OPTIONS] VOLUME [VOLUME...]
c. 关键参数
- -f,--force:强制删除。
d. 样例

![]()
![]()
E. docker volume prune
a. 功能
删除不使用的本地卷。
b. 语法
docker volume prune [OPTIONS]
c. 关键参数
- --filter:过滤。
- -f,--force:不提示是否删除。
d. 样例

(2)方式二(-v 或者--mount 指定)
-v 和 -mount 都可以完成管理卷的创建。
A. -v 参数
a. 功能
完成目录映射。
b. 语法
docker run -v name:directory[:options] ... ...
c. 参数
- 第一个参数:卷名称。
- 第二个参数:卷映射到容器的目录。
- 第三个参数:选项,如 ro 表示 readonly。
d. 样例
不指定 ro 选项:

指定 ro 选项:

B. --mount 参数
a. 功能
完成目录映射。
b. 语法
--mount '<key>=<value>,<key>=<value>'
c. 关键参数
- type:类型表示 bind,volume,or tmpfs。
- source,src:对于命名卷,这是卷的名称。对于匿名卷,省略此字段。
- destination,dst,target:文件或目录挂载在容器中的路径。
- ro,readonly:只读方式挂载。
d. 样例
采用 mount 创建 volume:
![]()
通过 docker inspect 可以看到:

![]()

(3)方式三(Dockerfile 匿名卷)
通过 Dockerfile 的 VOLUME 可以创建 docker 管理卷。也可以通过 dockerfile 的 VOLUME 指令在镜像中创建 Data Volume,这样只需要通过该镜像创建的容器都会存在挂载点,但值得注意的是通过 VOLUME 指令创建的挂载点,无法指定主机上对应的目录,而是由 docker 随机生成的。
2、操作案例
(1)Docker 命令创建管理卷
A. 命令创建管理卷
![]()
B. 查看其中一个管理卷
可以看到对应的宿主机目录如下:

C. 进入宿主机目录
可以放到容器里面的内容已经放到该目录中:
![]()
D. 查看容器内容
可以看到具体的 mount 信息:


E. 修改 index.html



F. 通过浏览器查看

可以看到宿主机和容器是同步的。
(2)Docker -v 创建管理卷
A. -v 创建管理卷,并且启动容器
![]()
B. 查看卷目录内容

C. 修改 index.html
![]()

D. 浏览器查看

E. 容器中修改提示无法修改
![]()
结论:指定 ro 的话宿主机可以修改,但是容器里面无法修改。
(3)Docker mount 创建管理卷
A. mount 创建管理卷,并且启动容器
![]()
B. 查看卷创建成功

C. 查看卷挂载点

D. 浏览器查看

E. 宿主机上修改文件内容


F. 查看页面

G. 清理释放空间
![]()
结论:mount 创建的卷数据也会完成同步。
(4)Docker 卷生命周期
A. -v 创建管理卷,并且启动容器
![]()
B. 进入卷目录
注意:此时可以看到容器里面的内容自动的放到了宿主机里面,也就是说宿主机上没有容器会拷贝过去。



C. 修改 index.html

D. 浏览器查看

E. 清理释放空间

F. 进入卷目录查看
可以看到文件并没有被删除:

G. 删除卷
可以看到我们的卷被删除掉了:

(5)Docker 卷共享
A. -v 创建管理卷,并且启动 2 容器,指定同一个卷

B. 进入卷目录

C. 修改 index.html

D. 浏览器查看
可以看到两个都是一样的首页:


E. 清理释放空间

六、绑定卷 bind mount
-v 和 -mount 都可以完成绑定卷的创建
1、创建卷
(1)-v 参数创建卷
A. 功能
完成卷映射。
B. 语法
docker run -v name:directory[:options] .........
C. 参数
- 第一个参数:宿主机目录,这个和管理卷是不一样的。
- 第二个参数:卷映射到容器的目录。
- 第三个参数:选项,如 ro 表示 readonly。
D. 样例
![]()
(2)--mount 参数创建绑定卷
A. 功能
完成目录映射。
B. 语法
--mount '<key>=<value>,<key>=<value>'
C. 关键参数
- type:类型表示 bind,volume or tmpfs。
- source,src:宿主机目录,这个和管理卷是不一样的。
- destination,dst,target:文件或目录挂载在容器中的路径
- ro,readonly:只读方式挂载。
D. 样例
![]()
2、操作案例
(1)mount 创建绑定卷
A. 使用 -mount 方式创建容器
创建 nginx 容器,并将宿主机 /testbindmount1 目录挂载至容器 /usr/share/nginx/html 目录,注意如果 testbindmount1 目录不存在会启动报错。
![]()
B. 查看挂载信息
提前创建好对应目录,并在宿主机上添加 index.html:

C. 进入容器的终端,查看挂载点目录,然后查看宿主机上的文件,还是存在,说明容器删除并不影响 bind 映射

D. 释放空间

(2)-v 创建绑定卷
A. 使用-v 方式创建容器
创建 nginx 容器,并将宿主机 /testbindmount3 目录挂载至容器 /usr/share/nginx/html 目录。
![]()
注意:如果 testbindmount3 目录不存在,启动不会报错,这是 -v 和 --mount 方式的区别。
B. 查看挂载信息


C. 进入容器的终端,查看挂载点目录,和在宿主机上查看里面都是没有文件

D. 在宿主机上添加 index.html

E. 删除容器,然后查看宿主机上的文件,还是存在,说明容器删除并不影响 bind 映射

(3)绑定卷共享
A. 启动两个绑定卷,都绑定到宿主机的同一个目录

B. 修改 index.html

C. 访问 2 个页面可以看到相应内容一样


可以看到我们实现了容器间的数据共享。
D. 清理空间

七、临时卷 tmpfs
临时卷数据位于内存中,在容器和宿主机之外。
tmpfs 局限性:
- 不同于卷和绑定挂载,不能在容器之间共享 tmpfs 挂载。
- 这个功能只有在 Linux 上运行 Docker 时才可用。
1、创建卷
(1)方式一(指定 --tmpfs 创建)
A. 功能
完成临时卷映射。
B. 语法
--tmpfs /app
C. 样例
![]()
(2)方式二(--mount 指定参数创建)
A. 功能
完成目录映射。
B. 语法
--mount '<key>=<value>,<key>=<value>'
C. 关键参数
- type:类型表示 bind, volume, or tmpfs。
- destination,dst,target:挂载在容器中的路径。
- tmpfs-size:tmpfs 挂载的大小(以字节为单位),默认无限制。
- tmpfs-mode:tmpfs 的八进制文件模式。例如,700 或 0770,默认为 1777 或全局可写。
D. 样例
![]()
2、操作案例
(1)tmpfs 参数创建临时卷
A. 创建临时卷并启动容器
![]()
B. 进入容器

可以看到 nginx 里面的文件被覆盖了,也就是说 tmpfs 也会覆盖容器里面的文件。
C. 添加一个首页

D. 浏览器查看

E. 停止容器
![]()
F. 启动容器 再次进入

可以看到 tmpfs 的内容完全消失了,也就是说内容是存在内存里面的。

G. 清理空间

(2)mount 创建临时卷
A. 创建临时卷并启动容器
![]()
B. 进入容器

可以看到 nginx 里面的文件被覆盖了,也就是说 tmpfs 也会覆盖容器里面的文件。
C. 添加一个首页

D. 浏览器查看

E. 拷贝一个大文件到容器里面
dockerdemo-1.0-SNAPSHOT.jar 约 17M。
docker cp dockerdemo-1.0-SNAPSHOT.jar mynginx18:/
F. 拷贝文件到我们的卷目录
[root@VM-8-5-centos /]# docker exec -it mynginx18 bash
root@23a13a663511:/# cd /usr/share/nginx/html/
root@23a13a663511:/usr/share/nginx/html# ls -l
total 4
-rw-r--r-- 1 root root 39 Aug 16 14:44 index.html
root@23a13a663511:/usr/share/nginx/html# ls -l /
bin dev docker-entrypoint.sh
etc lib media opt root sbin sys tmp var
boot docker-entrypoint.d dockerdemo-1.0-SNAPSHOT.jar home lib64 mnt proc run srv test usr
root@23a13a663511:/usr/share/nginx/html# cp /dockerdemo-1.0-SNAPSHOT.jar .
cp: error writing './dockerdemo-1.0-SNAPSHOT.jar': No space left on device
超过了限制,空间限制为了 1m,会提示没有空间。
G. 停止容器
![]()
H. 启动容器再次进入

可以看到 tmpfs 的内容完全消失了,也就是说内容是存在内存里面的。
(3)tmpfs 失踪了
A. 实战目的
掌握临时卷的创建方式,了解临时卷的特殊存储方式。
B. 实战步骤
a. 创建一个普通的容器
b. 在容器里面写入一个文件 tmplabelbyxuyinli.txt
c. 在宿主机上查找文件

文件被找到了,是因为他在容器的可写层。
![]()
d. 创建一个临时卷
![]()
e. 进入容器在 /app 目录下创建 mynewlabel.txt

f. 在宿主机上查找 mynewlabel.txt
可以发现,文件找不到:
![]()
所以 tmpfs 的内容不是存储在我们的容器的可写层里面的。
g. 释放资源

八、综合实战-MySQL 灾难恢复
1、实战目的
掌握挂载卷的使用,将 mysql 的业务数据存储到外部。
2、实战步骤
使用 MySQL 5.7 的镜像创建容器并创建一个普通数据卷 mysql-data 用来保存容器中产生的数据。需要在容器中连接 MySQL 服务, 并创建数据库 test,并在在该数据库中创建一个简单的表并插入一些数据进来。
(1)准备镜像

如果不存在该镜像,则输入命令:docker pull mysql:5.7
(2)创建容器
![]()
-e 选项通过参数 MYSQL_ROOT_PASSWORD 来传递 MySQL 密码。
(3)查看容器挂载信息

(4)连接 MySQL 的 shell, 创建数据库,密码是 xyl@test


(5)在宿主机中查看 volume
可以看到容器中 MySQL 创建的数据库和表数据以及持久化到宿主机挂载的目录下:

(6)有一天莫名其妙停电了,然后服务器重启了
这个时候 MySQL 没有起来,然后此时发现磁盘空间不多了,于是把所有停止的容器都删除了,结果我们的 Mysql 也没有了。

(7)幸好数据还在,此时该如何恢复呢
再次启动运行命令,确保目录映射一致就能找回我们的数据。
![]()
(8)通过 mysql 的客户端进去,再次查看数据

可以看到,之前的数据还在:
![]()
(9)释放空间

九、常见问题
1、什么时候用 Volume、bind、tmpfs
- volume:volume 是 docker 的宿主机文件系统一部分,用于不需要规划具体目录的场景。
- bind:bind mount 完全是依赖于主机的目录结构和操作系统,用于目录需要提前规划,比如 mysql 的目录需要个空间大的,其他服务有不占用的时候,用 volume 就不太合适了。
- tmpfs:用于敏感文件存储,文件不想存储的宿主机和容器的可写层之中。
十、扩展思考 —— 存储卷在实际研发中带来了哪些问题
1、跨主机使用
docker 存储卷是使用其所在的宿主机上的本地文件系统目录,也就是宿主机有一块磁盘,这块磁盘并没有共享给其他的 docker 主机,容器在这宿主机上停止或删除,是可以重新再创建的,但是不能调度到其他的主机上,这也是 docker 本身没有解决的问题,所以 docker 存储卷默认就是 docker 所在主机的本地,但是可以自己搭建一个共享的 NFS 来存储 docker 存储的数据,也可以实现,但这个过程强依赖于运维人员的能力。所以未来应用的存储和数据往往分离,越来越多的分布式存储方案出现,如 s3 系列,nfs 等。
2、启动参数未知
容器有一个问题,一般与进程的启动不太一样,就是容器启动时选项比较多。如果下次再启动时,很容器会忘记它启动时的选项,所以最好有一个文件来保存容器的启动,这就是容器编排工具的作用。一般情况下,是使用命令来启动操作 docker,但是可以通过文件来读,也就读文件来启动,读所需要的存储卷等,但是它也只是操作一个容器,如果要几十上百个容器操作,就需要专业的容器编排工具。这种一般像开源的 k8s,各个云厂商也有自己的企业版编排软件。
3、复杂场景仍然需要运维
对于有状态要持久的集群化组件,如 mysql 的主从。部署维护一个 Mysql 主从需要运维知识、经验整合进去才能实现所谓的部署,扩展或缩容,出现问题后修复,必须要了解集群的规模有多大,有多少个主节点,有多少个从节点,主节点上有多少个库,这些都要一清二楚,才能修复故障,这些就强依赖于运维经验。这种复杂的场景往往还是需要人力,很难有完美的工具出现。





















