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 的系统内存中,不会写入到系统的文件系统中,不会持久存储。
Bind mounts
bind mount自docker早期便开始为人们使用了,用于将host上的文件或目录被挂载到容器中。挂载时需要我们指定文件或目录在 host 上的完整路径。
如何使用
-v or --volume 语法
它有三部分组成,使用:进行分割,这些字段必须以正确的顺序排列,并且每个字段的含义不明显。
- 第一个字段是 Docker Host 上的一个文件或者目录。
- 第二个字段是将要挂载到容器上的一个文件或者目录。
- 第三个字段是可选的,用来增加一些附加选项,比如 ro.
--mount 语法
由多个键值对组成,以逗号分隔,每个键
key | value | |
---|---|---|
type | bind, volume, or tmpfs | |
source/src | Docker Host上的一个目录或者文件 | |
destination/dst/target | 被挂载容器上的一个目录或者文件 | |
readonly | 挂载为只读 | |
option | 其它选项 |
本地存在目录
比如我们想把 Docker Host 的目录source/html/挂载到 nginx 容器的/usr/share/nginx/html/,我们对 html 目录的更改希望可以立刻在容器 html 目录生效,这样我们就可以非常方便的修改网页的文件了。
docker run -d \
-it \
--name bindtest \
-p 80:80 \
--mount type=bind,source="$(pwd)"/html,target=/usr/share/nginx/html/ \
nginx:latest
使用命令docker inspect bindtest来查看挂载是否正确挂载。
"Mounts": [
{
"Type": "bind",
"Source": "/root/html",
"Destination": "/usr/share/nginx/html",
"Mode": "",
"RW": true,
"Propagation": "rprivate"
}
],
从里面可以出这是一个 bind mount,并且是读写挂载。
我们在 html 目录下创建一个 index.html,并且写入内容为“this is a bind test”。并且访问本地的 80 端口查看结果。
[root@tuling html]$ cat index.html
this is a bind test
[root@tuling html]$ curl http://localhost
this is a bind test
从这里例子之中我们可以看出如果我们挂载到容器的目录中有文件,文件会被我们的源地址文件进行覆盖。
我们把容器销毁掉,查看一下我们创建的文件是否还存在。
docker stop bindtest
docker rm bindtest
我们发现文件还是存在的,可见,即使容器没有了,bind mount 也还在。
只读挂载
另外,bind mount 时还可以指定数据的读写权限,默认是可读可写,可指定为只读:
docker run -d \
-it \
--name bindtest \
-p 80:80 \
--mount type=bind,source="$(pwd)"/html,target=/usr/share/nginx/html/,readonly\
nginx:latest
查看挂载详情,看看是不是只读模式。
"Mounts": [
{
"Type": "bind",
"Source": "/root/html",
"Destination": "/usr/share/nginx/html",
"Mode": "ro",
"RW": false,
"Propagation": "rprivate"
}
],
我们命令docker exec -ti bindtest bash进入到容器内部,修改文件测试一下。
[root@tuling ~]$ docker exec -ti bindtest bash
root@cb75eb777bf0:/$ ls
bin boot dev etc home lib lib64 media mnt opt proc root run sbin srv sys tmp usr var
root@cb75eb777bf0:/$ cd /usr/share/nginx/html/
root@cb75eb777bf0:/usr/share/nginx/html$ ls
index.html
root@cb75eb777bf0:/usr/share/nginx/html$ echo 111> index.html
bash: index.html: Read-only file system
readonly 设置了只读权限,在容器中是无法对 bind mount 数据进行修改的。只有 host 有权修改数据,提高了安全性。
单文件挂载 除了制定目录外,我们也可以指定单个文件进行覆盖,如下:
docker run -d \
-it \
--name bindtest \
-p 80:80 \
--mount type=bind,source="$(pwd)"/html/index.html,target=/usr/share/nginx/html/index.html \
nginx:latest
Volume
Volume 完全由 Docker 来进行管理,比如 volume 的创建,我们可以使用命令 docker volume create 来简单的创建一个 volume,当容器或者服务创建的时候,Docker 也可以自动的创建一个 volume。
我们首先需要创建一个 volume。
docker volume create my_vol
列出 volumes:
root@tuling:~$ docker volume ls
DRIVER VOLUME NAME
local my_vol
查看指定卷的详细信息:
root@tuling:~$ docker volume inspect my_vol
[
{
"CreatedAt": "2020-02-27T16:04:38+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@tuling:~$ ll /var/lib/docker/volumes/my_vol/_data
total 0
我们看到里面并没有数据,那我们启动容器查看一下。
docker run -d \
-it \
-p 80:80 \
--name bindtest \
--mount source=my_vol,target=/usr/share/nginx/html \
nginx:latest
使用命令docker inspect bindtest查看一下挂载详情。
"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@tuling:~$ ll /var/lib/docker/volumes/my_vol/_data
total 8
-rw-r--r-- 1 root root 494 Jan 21 21:36 50x.html
-rw-r--r-- 1 root root 612 Jan 21 21:36 index.html
我们可以看到 volume 的内容跟容器原有 /usr/share/nginx/html 完全一样,因为我们挂载的 volume 是刚刚创建没有数据的,容器原有的数据会被复制到 volume 中,我们同样的可以对其进行修改操作,直接反映到容器中。
我们删掉容器查看一下 volume 的数据是否被删除。
docker stop bindtest
docker rm bindtest
再次进行查看。
root@tuling:~$ ll /var/lib/docker/volumes/my_vol/_data
total 8
-rw-r--r-- 1 root root 494 Jan 21 21:36 50x.html
-rw-r--r-- 1 root root 612 Jan 21 21:36 index.html
可以看到,数据没有被删除。
使用有数据 volume 启动容器
我们接着上个 volume 进行测试,我们知道里面已经存在文件 index.html,我们更改里面的内容,并且把 50x.html 删掉。
root@tuling:~$ ll /var/lib/docker/volumes/my_vol/_data
total 4
-rw-r--r-- 1 root root 612 Jan 21 21:36 index.html
root@tuling:~$ echo "this is volume test" > /var/lib/docker/volumes/my_vol/_data/index.html
确认好 volume 之后,我们启动容器。
docker run -d \
-it \
-p 80:80 \
--name bindtest \
--mount source=my_vol,target=/usr/share/nginx/html \
nginx:latest
然后访问一下容器。
[root@tuling]$ curl http://localhost
this is volume test
我们可以看到,当我们的 volume 里面有数据的时候,容器内的数据就被 volume 覆盖了,同样的,当我们删除容器之后,volume 里面的数据会依然存在的。
不提前创建 volume 启动容器
之前的情况都说我们提前创建好 volume 进行挂载的,这次我们不提前创建,直接指定,看看会出现什么情况。
docker run -d \
-it \
-p 80:80 \
--name bindtest \
--mount source=my_vol2,target=/usr/share/nginx/html \
nginx:latest
我们可以看到,也创建成功了,这次我们对 volume 的命名为 my_vol2。
我们使用名称查看一下 volume 的情况。
root@tuling:~$ docker volume ls
DRIVER VOLUME NAME
local 1e97f99d822b0e377a5fbbd4c6e4183f618cff7da232a87610bfacf080b9776f
local f1bbefc4edf045c093e1512f0fa5db3a95ca30da254125696b9b5fb673d3b40e
local my_vol
local my_vol2
我们可以看到 Docker 给我们自动创建了一个 volume,那我们使用docker inspect bindtest查看一下挂载详情。
"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@tuling:~$ ll /var/lib/docker/volumes/my_vol2/_data
total 8
-rw-r--r-- 1 root root 494 Jan 21 22:36 50x.html
-rw-r--r-- 1 root root 612 Jan 21 22:36 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@tuling:~$ docker exec -ti nginxtest bash
root@a4592bbbfa32:/$ 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 | |
是否支持单个文件 | 支持 | 不支持,只能是目录 | |
权限控制 | 读写或者只读 | 读写或者只读 | |
移植性 | 弱,与hostpath 绑定 | 强,无需指定hostpath |
tmpfs
tmpfs 不在磁盘上持久存储,也不在 Docker Host 容器里面存储,他存储在 host 的内存中,它可以在容器的整个生命周期内被容器所使用。
使用场景
当你不需要持久保留数据在 host 或容器内。这可能是出于安全原因,或者是提升容器的性能,比如我们的程序需要写入很多不需要存储的状态数据时,我们就会使用 tmpfs。
使用语法
同样的,我们可以在单容器的情况下使用--tmpfs,并且不能指定参数,在集群的情况下使用--mount,可以指定一些参数,具体如下:
- type tmpfs
- destination/dst/target
使用 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