【Docker】Docker 学习 03 | Docker 的 volume
1. 引入
在 docker 的基本知识讲解中,提到了 docker 镜像是由一层一层文件系统构成的。这一系列文件系统是一系列的只读层
。当我们创建一个容器的时候,Docker 会读取镜像(只读),并在镜像的顶部再添加一层读写层。
这种读写层和只读层的组合被称为联合文件系统
(Union File System / Unoin FS),结构抽象如下表所示,以防图片加载不出来。
层级 | 说明 |
---|---|
Running Container | 正在运行的容器 |
Storage Driver | 存储驱动层 |
write layer | 读写层 |
image layer | 镜像只读层 |
HOST | 宿主机 |
如果正在运行的容器修改了现有的文件,这些文件会被拷贝出底层的只读层,放到最顶部的容器读写层中,读写层中文件的未修改版本仍然存放在镜像的只读层中。
层 | 文件 | 说明 |
---|---|---|
读写层 | app.ini | 当出现写操作的时候,从只读层中拷贝到读写层 |
只读层 | app.ini |
当基于相同的镜像创建第二个容器时,还是会创建一个没有任何数据修改的全新容器。在之前的容器中的任何修改只会保留在原有容器中,实现了容器和镜像的隔离。
这种读写层的操作带来了以下的问题:
- 当容器不再存在的时候,数据不持久化;
- 如果另外一个进程需要使用容器内的数据,难以将其从容器内取出;
- 容器的可写层与容器当前运行的宿主机紧密相连,难以将其移动到另外一台主机上;
- 写入容器的可写层需要存储驱动
Storage Dirver
来管理这个文件系统,存储驱动提供了一个使用 Linux 内核的联合文件系统;与直接将数据写入宿主机的文件系统的方式,这种额外的抽象层降低了性能。
为了能持久化这些修改过的数据,并且能够很容易实现容器间进行数据的共享,docker 提出了 volume 的概念,同时也提供了多种数据持久化的方式。
2. docker 提供的持久化策略
docker 提供两种文件持久化的策略,分别是 volume 和 mount,其中 mount 还分为 bind mount
(将容器内路径和宿主机的文件路径绑定)和 tmpfs mount
(数据只存在于宿主机的内存中)。
通过 volume 和 bind mount 持久化的文件都可以称之为 docker 的数据卷。数据卷是在容器默认的联合文件系统之外的文件或目录,它可以在宿主机上直接被访问。即便容器删除,数据卷中的内容也不会丢失。
tips:
- Volumes are stored in a part of the host filesystem which is managed by Docker (
/var/lib/docker/volumes/
on Linux). Non-Docker processes should not modify this part of the filesystem. Volumes are the best way to persist data in Docker. - Bind mounts may be stored anywhere on the host system. They may even be important system files or directories. Non-Docker processes on the Docker host or a Docker container can modify them at any time.
- tmpfs mounts are stored in the host system’s memory only, and are never written to the host system’s filesystem.
下文将对这三种不同的文件持久化方式进行测试
3. volumes
3.1. 测试:自动创建的 volume
以 mysql:5.7
镜像为例,下面是一个创建容器的命令
1 | docker run -d \ |
创建容器之前,先看看当前系统上的 docker volume 有哪些
1 | $ docker volume ls |
执行了这个命令后,mysql 容器被创建且正常运行,再次查看当前系统上的 docker volume,可以发现多了一个新的 volume。
1 | DRIVER VOLUME NAME |
进入新创建的这个 docker volume 在宿主机上的路径,看看这里面有什么东西
1 | ❯ sudo ls -al /var/lib/docker/volumes/0cd65f1432c653ec08d7d8c3c50f645b97e0f26d139f1debb0c7fead3dafdfa4/_data |
如果你对 MySQL 比较熟悉,应该就能认出来,这就是 MySQL 在 /var/lib/mysql
中存放的数据,我们可以做个简单的验证,使用如下命令,直接链接到这个新创建的容器的 MySQL 命令行中。
1 | docker exec -it testMysql mysql -uroot -p123456 |
我们在 MySQL 里面创建一个 testdb 数据库和一个 stu 表
1 | -- 创建数据库 |
操作完成后,exit 退出容器,再次查看刚刚的 volume 目录。可以看到多了一个名为 testdb
的文件夹。
1 | ❯ sudo ls -al /var/lib/docker/volumes/0cd65f1432c653ec08d7d8c3c50f645b97e0f26d139f1debb0c7fead3dafdfa4/_data |
查看该文件夹,能看到刚刚我们创建的 stu 表的本地文件。可见这就是 MySQL 的本地路径。如果你在宿主机上直接使用 apt 安装一个 MySQL/MariaDB,也可以在宿主机的 /var/lib/mysql
中看到类似的文件。
1 | ❯ sudo ls -al /var/lib/docker/volumes/0cd65f1432c653ec08d7d8c3c50f645b97e0f26d139f1debb0c7fead3dafdfa4/_data/testdb |
通过 docker inspect testMysql
命令,可以查询到这个容器的配置详情,其中的 Mount 部分就有刚刚看到的 volume,其中 Source
字段就是这个 volume 在宿主机上的路径,Destination
字段是 volume 对应的容器内路径。
1 | { |
再去找找 MySQL 容器的 dockerfile,也可以在里面看到一行关于 volume 的配置
1 | VOLUME /var/lib/mysql |
由此可见,对于创建容器,如果没有在 run 命令中主动 mount 某个 volume 或路径时,docker 会自动创建一个随机命名的 volume(保持唯一性),并将容器内的路径和这个 volume 绑定。
另外,一个 volume 的只能对应容器内的一个路径。如果容器在 dockerfile 中指定了多个不同路径的 volume,则 Docker 也会创建多个 volume 与之对应。
3.2. 测试:主动指定 volume
我们可以在 run 命令中指定容器路径和某个 volume 进行绑定,也可以写入一个 volume 的名字,在创建容器的同时创建这个 volume。
下面这两种创建方式,都会在 /var/lib/docker/volumes
中创建一个名为 test_mysql_2
的 volume。
1 | # 创建volume |
1 | # 创建容器的时候直接创建volume |
执行命令后,可以看到新创建出来的 volume
1 | ❯ docker volume ls |
通过 docker inspect testMysql2
命令,可以看到 Mount 中的信息
1 | { |
3.3. 绑定 volume 的权限选项
这里能发现字段 Mode
有变化,由空串变成了小写的 z
。这个是什么意思呢?
z
(小写):代表绑定的目录由多个容器共享,其他容器也可以挂载这个 volume;Z
(大写):代表绑定的目录由单个容器私有,其他容器无法挂载;
在使用 -v
绑定某个路径的时候,可以在路径后面再添加一个选项,来指定权限和绑定模式。方式如下,在容器内路径后再追加一个冒号
1 | -v volume名字或宿主机路径:容器内路径:[权限选项] |
权限的可选项有四种,默认情况下,给定的是 rw 读写权限。
- 大写 Z
- 小写 z
- ro(只读)
- rw(读写)
其中:z
和:Z
选项是和 SELinux 有关的,具体可以参考官方文档和
在 Ubuntu 上,SELinux 工具集默认应该是没有启用的。
注意,如果你使用:Z
(大写)选项绑定了宿主机中诸如 /
、/usr
、/home
的目录,你可能会因为权限问题,直接无法使用宿主机!使用该选项的时候需要慎重!
4. mount
4.1. bind mount
4.1.1. 说明
bind mount 是 docker 早期就已经存在的数据持久化方式,其支持将容器的内的路径映射到某个宿主机上的路径,实现容器和宿主机文件的同步。绑定挂载直接使用了宿主机的文件系统,性能更佳。
绑定路径在 docker run 命令中和 volume 类似,都可以使用 -v
选项来实现
1 | -v 宿主机路径:容器内路径 |
下面是一个示例
1 | # 使用root用户,在宿主机上创建路径 |
这样这个 docker 安装的 MySQL 容器内的所有数据都会被写入宿主机的 /data/mysql
路径中,我们可以直接备份这个路径实现对 MySQL 数据的保留。
4.1.2. 源路径的说明
注意绑定挂载时 -v
选项中的路径和 volume 的区别。我们知道,在 Linux 命令行中,直接输入一个目录 / 文件的名称, 会默认是当前路径下的内容。
1 | cd folder 等价于 cd ./folder |
而在 docker run 命令的 -v
选项中,源路径 source 输入直接为某个目录名的时候,会认为是 volume 的名称!而不是当前路径下的文件!
假设我们当前运行 docker run 的终端路径中有一个 folder 文件夹,我们想将这个文件夹映射到 docker 容器内的 /data
路径,推荐的写法如下(推荐使用绝对路径来设置源主机上的路径)。
1 | -v ${PWD}/folder:/data |
错误的写法如下,直接写一个 folder 会以之为名创建一个新的 volume,不符合我们的需要!
1 | -v folder:/data |
这一点在新建容器的时候一定要注意!个人推荐维持一个原则,即使用 bind mount 的时候一定要用绝对路径来设置宿主机上的文件路径。
4.1.3. bind mount 的弊端
绑定挂载也有弊端
- Bind mounts allow access to sensitive files One side effect of using bind mounts, for better or for worse, is that you can change the host filesystem via processes running in a container, including creating, modifying, or deleting important system files or directories. This is a powerful ability which can have security implications, including impacting non-Docker processes on the host system.
翻译过来就是,绑定挂载(特别是以读写方式挂载)会让 docker 容器有权限修改宿主机的任何文件,甚至包括宿主机的系统文件。存在安全性问题。
这一点在 Linux 中国关于 SELinux 的文章中就有介绍,比如我们将宿主机的 /
路径直接绑定到容器的 /test
路径中时,使用 docker exec
进入这个容器的终端,我们会拥有容器内的 root 权限(即可以对当前登录的这个容器内的文件做任意修改),此时就直接可以通过编辑容器内的 /test
路径,来删除 / 修改宿主机上的重要文件。
4.1.4. docker run 的 –mount 选项
除了 -v
选项,还可以用 --mount
选项来挂载数据卷,效果一致。
1 | docker run -d \ |
mount 选项中,绑定的选项都用参数名写出来了,相对来说会更好理解,但是命令也变得复杂了。
key | value |
---|---|
type | bind/volume/tmpfs |
source/src | docker host 上的一个目录或文件 |
destination/dst/target | 容器内的一个目录或文件 |
readonly | 挂载为只读 |
option | 额外选项 |
如果需要指定 readonly,直接在 target 后面添加该选项即可。添加了只读选项后,容器内对于这个路径就只能读,不能写入了。
1 | --mount type=bind,source=/data/mysql,target=/var/lib/mysql,readonly |
当使用 mount 选项来绑定 volume 的时候,可以省略 type,此时 docker 会自动以 source 写入的字符串作为 volume 的名字,创建一个新 volume 并与当前容器进行绑定。
1 | docker run -d \ |
4.2. tmpfs mount
当容器为了性能原因,需要高频读写某些缓存文件(比如 jellyfin 镜像就有一个 cache 目录的 volume,内部是一些缓存文件),或者为了安全性考虑不打算将一些数据写入磁盘的时候,我们可以使用 tmpfs mount,将指定的路径绑定到宿主机的内存上。
对于 nginx 容器而言,其默认会有一个 nginx 的欢迎页面,存放在 /usr/share/nginx/html
路径中,这个欢迎页面可能会被经常的读取,占用空间也不大,所以我们可以将其放入内存中。
可以使用 mount 选项来进行绑定
1 | # 直接绑定 |
也可以使用 tmpfs 选项来绑定
1 | docker run -d -it \ |
更多相关的参数,可以参考 docker 的官方文档 storage/tmpfs。
4.3. bind mount 和 volume 的区别
docker 官方其实一直都推荐我们使用 volume 来实现数据持久化,而不是使用 bind mount。来看看二者的区别吧。
区别 | bind mount | volume |
---|---|---|
source 位置 | 任意指定 | /var/lib/docker/volumes |
source 路径为空 | 覆盖容器中的内容 | 容器内数据复制到 volume |
权限控制 | 读写 / 只读 | 读写 / 只读 |
单个文件 | 支持 | 不支持,只能是目录 |
移植性 | 弱,与 hostpath 绑定 | 强,无需指定 hostpath |
5. 持久化和数据卷
数据卷的最大特点是它的生命周期独立于容器的生命周期,即便容器被删除,数据卷中的内容也不会被删除(tmpfs 除外,它的内容本来就没有写入磁盘)。当使用 docker rm
删除某个容器的时候,docker 并不会主动删除和容器关联的数据卷。
- 数据卷可在容器之间共享或重用数据。
- 数据卷的更改可以直接生效。
- 数据卷的生命周期一直持续到没有容器使用它为止。
- 对数据卷操作不会影响到镜像本身。
- 数据卷可以完成容器到宿主机、宿主机到容器以及容器到容器之间的数据共享。
可见数据卷的好处还是多多的。所以,当你打算删除某个数据卷的时候,一定要确保这个数据卷里面的文件是完全无用了!
6. 参考文档
- 最新
- 最热
- 最早
- 作者
点击重新获取 | 打开控制台