Docker 持久存储介绍(十三)
一、Docker 数据存储
我们都知道 Docker 的数据可以存在容器的可写层,但是也存在以下几点不足:
- 当该容器不再运行时,数据将不会持久存储,如果另一个进程需要它,就很难将数据从容器中取出。
- 容器的可写层与 Docker Host 在容器运行时紧密耦合,你不能轻易地把数据移到别的地方。
- 写入容器的可写层需要一个 storage driver 来管理。storage driver 使用 Linux 内核提供一个文件系统。与使用直接写入宿主文件系统的 volume 相比,这种额外的抽象降低了性能。
Docker 提供了三种不同的方式将数据从 Docker Host 挂载到 Docker 容器,并实现数据的读取和存储:volumes、bind mounts、tmpfs 。
无论我们选择使用哪种类型的挂载,数据从容器中看起来都一样的,它在容器的文件系统中作为目录或单个文件展示。
我们可以通过数据存储在 Docker Host 的方式来简单的了解这三种挂载方式的不同,如下图:
- Volumes 存储在 Docker Host 文件系统的一个路径下,这个路径是由 Docker 来进行管理,路径默认是 /var/lib/docker/volumes/,非 Docker 的进程不能去修改这个路径下面的文件,所以说 Volumes 是持久存储数据最好的一种方式。
- Bind mounts 可以存储在 Docker Host 文件系统的任何位置,它们甚至可能是重要的系统文件或目录,非 Docker 的进程或者 Docker 容器可能随时对其进行修改,存在潜在的安全风险。
- Tmpfs 只存储在 Docker Host 的系统内存中,不会写入到系统的文件系统中,不会持久存储。
所有说使用 volumes 是我们非常推荐的一种方式。
二、Bind mount
1、详细介绍
相对于 volume,bind mount 具有有限的功能。我们使用 bind mount 时,host 上的文件或目录被挂载到容器中。挂载时需要我们指定文件或目录在 host 上的完整路径。
bind mount 是非常高效的,但它依赖 host 的文件系统的目录结构。如果打算部署新的 Docker 应用,我们可以考虑使用 volume 而命名,不然你不能使用 Docker CLI 命令直接管理 bind mount。
使用 bind mount 的一个缺点是,我们可以通过在容器中运行的进程更改 host 的文件系统,包括创建、修改或删除重要的系统文件或目录。这会严重影响系统的安全,甚至影响 host 上面非 Docker 的进程。
2、如何使用
之前我们使用 bind mount 可以使用-v
或者--volume
,这个参数在单容器的情况下使用,在 swarm 集群中使用--mount
,从 Docker 17.06 之后,我们可以统一使用参数--mount
。
对于新接触 Docker 的我们来说建议使用--mount
,老司机可以继续使用-v
,但是我们还是建议使用--mount
。
-v or --volume 语法
它有三部分组成,使用:
进行分割,这些字段必须以正确的顺序排列,并且每个字段的含义不明显。
- 第一个字段是 Docker Host 上的一个文件或者目录。
- 第二个字段是将要挂载到容器上的一个文件或者目录。
- 第三个字段是可选的,用来增加一些附加选项,比如 ro,consistent,delegated,cached,z,and Z。
--mount 语法
它由一组键值对组成,由,
进行分割,每个值为 <key>=<value>
。
Key | Value |
---|---|
type | bind、volume、tmpfs,如不指定,默认是 volume |
source/src | Docker Host 上的一个文件或者目录 |
destination/dst/target | 被挂载容器上的一个文件或者目录 |
readonly | 没有参数,只写这个词即可 |
bind-propagation | rprivate、private、rshared、shared、rslave、slave |
consistency | consistent、delegated、cached,只在 Mac 系统上生效 |
两者区别
使用-v
的时候,如果在 Docker Host 不存在要挂载的文件或者目录,Docker 将会自动进行创建,通常是一个目录。
使用--mount
的时候,如果在 Docker Host 不存在要挂载的文件或者目录,Docker 不会自动创建目录,并生成一个错误。
3、使用场景
- 把 host 中的配置文件共享给 host 上面的容器。容器为什么自带 DNS 解析呢,那是因为默认情况下 host 把 /etc/resolv.conf 挂载到它上面的容器里面。
- 在 Docker Host 上面的开发环境和容器直接共享程序的源代码或者构建要素。例如,你可以挂载一个 Maven 目录到一个容器中,每当你在 Docker Host 重新建立 maven 项目,容器都可以直接获取你重新构建的 maven 项目。
- 我们可以将源代码目录 mount 到容器中,在 host 中修改代码就能看到应用的实时效果。
- 将 mysql 容器的数据放在 bind mount 里,这样 host 可以方便地备份和迁移数据。
- 只需要向容器添加文件,不希望覆盖整个目录。
4、使用案例
存在目录 bind mount
比如我们想把 Docker Host 的目录source/html/
挂载到 nginx 容器的/usr/share/nginx/html/
,我们对 html 目录的更改希望可以立刻在容器 html 目录生效,这样我们就可以非常方便的修改网页的文件了。
docker run -d \
-it \
--name devtest \
-p 80:80 \
--mount type=bind,source="$(pwd)"/html,target=/usr/share/nginx/html/ \
nginx:latest
使用命令docker inspect devtest
来查看挂载是否正确挂载。
"Mounts": [
{
"Type": "bind",
"Source": "/root/sample/html",
"Destination": "/usr/share/nginx/html",
"Mode": "",
"RW": true,
"Propagation": "rprivate"
}
],
从里面可以出这是一个 bind mount,并且是只读挂载。
我们在 html 目录下创建一个 index.html,并且写入内容为“This is a bind mount test!”。并且访问本地的 80 端口查看结果。
从这里例子之中我们可以看出如果我们挂载到容器的目录中有文件,文件会被我们的源地址文件进行覆盖。
我们把容器销毁掉,查看一下我们创建的文件是否还存在。
docker stop devtest
docker rm devtest
我们发现文件还是存在的,可见,即使容器没有了,bind mount 也还在。这也合理,bind mount 是 host 文件系统中的数据,只是借给容器用用,哪能随便就删了啊。
只读挂载
另外,bind mount 时还可以指定数据的读写权限,默认是可读可写,可指定为只读:
docker run -d \
-it \
--name devtest \
-p 80:80 \
--mount type=bind,source="$(pwd)"/html,target=/usr/share/nginx/html/,readonly\
nginx:latest
查看挂载详情,看看是不是只读模式。
"Mounts": [
{
"Type": "bind",
"Source": "/root/sample/html",
"Destination": "/usr/share/nginx/html",
"Mode": "",
"RW": false,
"Propagation": "rprivate"
}
],
我们命令docker exec -ti devtest bash
进入到容器内部,修改文件测试一下。
readonly 设置了只读权限,在容器中是无法对 bind mount 数据进行修改的。只有 host 有权修改数据,提高了安全性。
单文件挂载
除了制定目录外,我们也可以指定单个文件进行覆盖,如下:
docker run -d \
-it \
--name devtest \
-p 80:80 \
--mount type=bind,source="$(pwd)"/html/index.html,target=/usr/share/nginx/html/index.html \
nginx:latest
三、Volume
1、详细介绍
Volume 完全由 Docker 来进行管理,比如 volume 的创建,我们可以使用命令 docker volume create
来简单的创建一个 volume,当容器或者服务创建的时候,Docker 也可以自动的创建一个 volume。
当我们创建了一个 volume,它存储在 Docker Host 的存储目录下。当我们把 volume 挂载入容器时,此目录就是挂载到容器中的目录。这类似于 bind mount 的工作方式,不同的是 volume 是由 Docker 来管理并且和 Docker Host 的核心功能进行隔离。
一个给定的 volume 可以同时挂载到多个容器中。当没有容器的使用 volume 时, volume 对 Docker 仍然是可用的并且不会被自动删除。我们可以使用命令docker volume prune
来删除一个已经不使用的 volume。
我们在挂载 volume 时,可以对其命名,也可以是默认随机生成的名字。如果我们没有指定名称,当 volume 第一次挂载到一个容器时,Docker 会用一个随机字符串对其进行命名,这样可以保证 volume 在 Docker Host 的唯一性。
Volume 还支持使用 volume drivers,它允许您将数据存储挂载到远程主机或云提供商上等。
Volumes 对比 bind mounts 具备以下几点有点:
- Volumes 的备份和迁移更加容易。
- 可以使用 Docker CLI 或者 Docker API 管理 volumes。
- Volumes 既可以在 Linux 的容器中使用,也可以在 Windows 的容器中使用。
- Volumes 在多容器中共享更加的安全。
- Volume drivers 允许我们把数据存储在远程主或云提供商。
2、使用语法
我们推荐使用--mount
,所有这里我们只写它的使用方法。
Key | Value |
---|---|
type | bind、volume、tmpfs ,如不指定,默认是 volume |
source/src | Docker Host 上的一个文件或者目录 |
destination/dst/target | 被挂载容器上的一个文件或者目录 |
readonly | 没有参数,只写这个词即可 |
volume-opt | 可以指定更多的附加参数 |
3、使用场景
使用容器技术,volume 是最推荐的一种持久存储数据的方式。volume 的一些使用场景如下:
- 当我们需要在多个正在运行的容器之间共享数据时,我们需要volume 。如果我们没有明确指定创建它,那么它第一次装入容器时就会创建一个 volume。当容器停止或删除掉,volume 仍然存在。多个容器可以同时读写一个 volume。只有当我们明确指定要删除某个 volume 时,它才会被删除。
- 当我们需要把容器的数据永久存储在一个远程主机或者一个云服务器上,我们需要 volume。
- 当我们的 Docker Host 无法保证可以提供一个目录或者文件来作为数据存储时,我们也需要 volume,它可以减少我们对配置文件的依赖。
- 当我们需要备份数据,或者恢复数据,以及需要把数据从一个 Docker Host 迁移到另外一个 Docker Host 的时候,volume 是我们最好的一个选择,我们可以停掉正在使用 volume 的容器,然后把 volume 的目录备份下来即可,volume 的目录一般在
/var/lib/docker/volumes/<volume-name>
下。
4、使用案例
不像 bind mount,我们首先需要创建一个 volume。
docker volume create my_vol
列出 volumes:
root@ubuntu:~# docker volume ls
DRIVER VOLUME NAME
local my_vol
查看指定卷的详细信息:
root@ubuntu:~# docker volume inspect my_vol
[
{
"CreatedAt": "2017-12-07T10:27:26+08:00",
"Driver": "local",
"Labels": {},
"Mountpoint": "/var/lib/docker/volumes/my_vol/_data",
"Name": "my_vol",
"Options": {},
"Scope": "local"
}
]
删除卷:
docker volume rm my_vol
使用无数据 volume 启动容器
我们查看一下刚刚创建的 volume 里面是否有数据
root@ubuntu:~# ll /var/lib/docker/volumes/my_vol/_data
total 8
drwxr-xr-x 2 root root 4096 Dec 7 10:27 ./
drwxr-xr-x 3 root root 4096 Dec 7 10:27 ../
我们看到里面并没有数据,那我们启动容器查看一下。
docker run -d \
-it \
-p 80:80 \
--name devtest \
--mount source=my_vol,target=/usr/share/nginx/html \
nginx:latest
使用命令docker inspect devtest
查看一下挂载详情。
"Mounts": [
{
"Type": "volume",
"Name": "my_vol",
"Source": "/var/lib/docker/volumes/my_vol/_data",
"Destination": "/usr/share/nginx/html",
"Driver": "local",
"Mode": "z",
"RW": true,
"Propagation": ""
}
],
我们再次查看一下 Source 目录。
root@ubuntu:~# ll /var/lib/docker/volumes/my_vol/_data
total 16
drwxr-xr-x 2 root root 4096 Dec 7 09:33 ./
drwxr-xr-x 3 root root 4096 Dec 6 20:18 ../
-rw-r--r-- 1 root root 537 Nov 21 22:28 50x.html
-rw-r--r-- 1 root root 612 Nov 21 22:28 index.html
我们可以看到 volume 的内容跟容器原有 /usr/share/nginx/html 完全一样,因为我们挂载的 volume 是刚刚创建没有数据的,容器原有的数据会被复制到 volume 中,我们同样的可以对其进行修改操作,直接反映到容器中。
我们删掉容器查看一下 volume 的数据是否被删除。
docker stop devtest
docker rm devtest
再次进行查看。
root@ubuntu:~# ll /var/lib/docker/volumes/my_vol/_data
total 16
drwxr-xr-x 2 root root 4096 Dec 7 10:13 ./
drwxr-xr-x 3 root root 4096 Dec 6 20:18 ../
-rw-r--r-- 1 root root 537 Nov 21 22:28 50x.html
-rw-r--r-- 1 root root 31 Dec 7 10:13 index.html
我们可以看到,数据没有被删除。
使用有数据 volume 启动容器
我们接着上个 volume 进行测试,我们知道里面已经存在文件 index.html,我们更改里面的内容,并且把 50x.html 删掉。
root@ubuntu:~# ll /var/lib/docker/volumes/my_vol/_data
total 12
drwxr-xr-x 2 root root 4096 Dec 7 10:16 ./
drwxr-xr-x 3 root root 4096 Dec 6 20:18 ../
-rw-r--r-- 1 root root 31 Dec 7 10:13 index.html
root@ubuntu:~# cat /var/lib/docker/volumes/my_vol/_data/index.html
This is a volume mount test!
确认好 volume 之后,我们启动容器。
docker run -d \
-it \
-p 80:80 \
--name devtest \
--mount source=my_vol,target=/usr/share/nginx/html \
nginx:latest
然后访问一下容器。
root@ubuntu:~# curl localhost
This is a volume mount test!
我们可以看到,当我们的 volume 里面有数据的时候,容器内的数据就被 volume 覆盖了,同样的,当我们删除容器之后,volume 里面的数据会依然存在的。
不提前创建 volume 启动容器
之前的情况都说我们提前创建好 volume 进行挂载的,这次我们不提前创建,直接指定,看看会出现什么情况。
docker run -d \
-it \
-p 80:80 \
--name devtest \
--mount source=my_vol2,target=/usr/share/nginx/html \
nginx:latest
我们可以看到,也创建成功了,这次我们对 volume 的命名为 my_vol2。
我们使用名称查看一下 volume 的情况。
root@ubuntu:~# docker volume ls
DRIVER VOLUME NAME
local 15a731acddee296080b56ddd5faf27748bdfbc422ce2e6b9574ca755e878f434
local aaf128a2672ffe8994bd080c83bcd4540796a47fd404a8c85e2e1f48f3086855
local e157aff9c4db4bcf935484b99f119dbe8faeac2de6408197f25b6f1ea798975c
local my_vol
local my_vol2
我们可以看到 Docker 给我们自动创建了一个 volume,那我们使用docker inspect devtest
查看一下挂载详情。
"Mounts": [
{
"Type": "volume",
"Name": "my_vol2",
"Source": "/var/lib/docker/volumes/my_vol2/_data",
"Destination": "/usr/share/nginx/html",
"Driver": "local",
"Mode": "z",
"RW": true,
"Propagation": ""
}
],
我们猜测,这种情况应该和第一种空 volume 挂载类似,volume 里面的内容应该是容器复制过来的,我们查看一下是否这样的。
root@ubuntu:~# ll /var/lib/docker/volumes/my_vol2/_data
total 16
drwxr-xr-x 2 root root 4096 Dec 7 10:38 ./
drwxr-xr-x 3 root root 4096 Dec 7 10:38 ../
-rw-r--r-- 1 root root 537 Nov 21 22:28 50x.html
-rw-r--r-- 1 root root 612 Nov 21 22:28 index.html
情况确实如我们所说,从这里我们可以看出,如果使用 bind mount,我们的源目录必须存在,不然docker 会报错,然而我们使用 volume,如果源不存在,docker 会为我们进行创建。
这是因为 bind mount 挂载的路径并不是 docker 进行管理的,他没有权限随便创建目录,然后 volume 是 docker 进行管理的,它可以在自己的存储目录下面创建 volume。
当我们想把容器内的数据导出来时,使用这种方式非常方便。
只读模式挂载 volume
在某些情况下,我们使用多容器进行挂载的时候,我们不允许容器对 volume 里面的数据进行修改,这样可以保证所有的容器挂载的是相同的 volume。
docker run -d \
-it \
-p 80:80 \
--name=nginxtest \
--mount source=nginx-vol,destination=/usr/share/nginx/html,readonly \
nginx:latest
使用命令docker inspect nginxtest
查看一下挂载情况。
"Mounts": [
{
"Type": "volume",
"Name": "nginx-vol",
"Source": "/var/lib/docker/volumes/nginx-vol/_data",
"Destination": "/usr/share/nginx/html",
"Driver": "local",
"Mode": "z",
"RW": false,
"Propagation": ""
}
],
同样的我们可以看到容器的数据被复制到了 volume 里面,我们进入容器,修改文件看看。
root@ubuntu:~# docker exec -ti nginxtest bash
root@28f1d32e08be:/# echo "nginxtest" > /usr/share/nginx/html/index.html
bash: /usr/share/nginx/html/index.html: Read-only file system
可以看到,容器内部是不能修改 volume 里面的数据的。
到这里我们简单对比一下 bind mount 和 volume 的不同点。
不同点 | bind mount | volume |
---|---|---|
Source位置 | 可以任意指定 | /var/lib/docker/volumes/... |
空 source | 覆盖掉容器的内容 | 容器内数据复制到 volume |
是否支持单个文件 | 支持 | 不支持,只能是目录 |
权限控制 | 读写或者只读 | 读写或者只读 |
移植性 | 弱,与 host path 绑定 | 强,无需指定 host 目录 |
四、tmpfs
1、详细介绍
tmpfs 不在磁盘上持久存储,也不在 Docker Host 容器里面存储,他存储在 host 的内存中,它可以在容器的整个生命周期内被容器所使用。
2、使用场景
当你不需要持久保留数据在 host 或容器内。这可能是出于安全原因,或者是提升容器的性能,比如我们的程序需要写入很多不需要存储的状态数据时,我们就会使用 tmpfs。
3、使用语法
同样的,我们可以在单容器的情况下使用--tmpfs
,并且不能指定参数,在集群的情况下使用--mount
,可以指定一些参数,具体如下:
Key | Value |
---|---|
type | bind、volume、tmpfs,如不指定,默认为 volume |
destination/dst/target | 容器中的路径 |
tmpfs-type/tmpfs-mode | 一些附加参数 |
4、使用案例
使用 tmpfs 启动容器。
docker run -d \
-it \
-p 80:80 \
--name tmptest \
--mount type=tmpfs,destination=/usr/share/nginx/html \
nginx:latest
容器对目录所有的读写操作都在内存中。
我们也可以指定 tmpfs 的权限情况。
docker run -d \
-it \
-p 80:80 \
--name tmptest \
--mount type=tmpfs,destination=/usr/share/nginx/html,tmpfs-mode=1770 \
nginx:latest