一、Docker卷与持久化数据
数据主要分为两类,持久化的与非持久化的。
持久化数据是需要保存的数据。例如客户信息、财务、预定、审计日志以及某些应用日志数据。非持久化数据是不需要保存的那些数据。
两者都很重要,并且 Docker 均有对应的支持方式。
每个 Docker 容器都有自己的非持久化存储。非持久化存储自动创建,从属于容器,生命周期与容器相同。这意味着删除容器也会删除全部非持久化数据。
如果希望自己的容器数据保留下来(持久化),则需要将数据存储在卷上。卷与容器是解耦的,从而可以独立地创建并管理卷,并且卷并未与任意容器生命周期绑定。最终效果即用户可以删除一个关联了卷的容器,但是卷并不会被删除。
对于微服务设计模式来说,容器是不错的选择。通常与微服务挂钩的词有暂时以及无状态。所以,微服务就是无状态的、临时的工作负载,同时容器即微服务。因此,我们经常会轻易下结论,认为容器就是用于临时场景。
但这种说法是错误的,而且是大错特错!
容器与非持久数据
毫无疑问,容器擅长无状态和非持久化事务。
每个容器都被自动分配了本地存储。默认情况下,这是容器全部文件和文件系统保存的地方。之前我们可能听过一些非持久存储相关的名称,如本地存储、GraphDriver 存储以及 SnapShotter 存储。
总之,非持久存储属于容器的一部分,并且与容器的生命周期一致,容器创建时会创建非持久化存储,同时该存储也会随容器的删除而删除
在 Linux 系统中,该存储的目录在 /var/lib/docker/<storage-driver>/ 之下,是容器的一部分。
如果在生产环境中使用 Linux 运行 Docker,需要确认当前存储驱动(GraphDriver)与 Linux 版本是否相符。下面列举了一些指导建议。
- RedHat Enterprise Linux:Docker 17.06 或者更高的版本中使用 Overlay2 驱动。在更早的版本中,使用 Device Mapper 驱动。这适用于 Oracle Linux 以及其他 Red Hat 相关发行版。
- Ubuntu:使用 Overlay2 或者 AUFS 驱动。如果正在使用 Linux4.x 或者更高版本的内核,建议使用 Overlay2。
- SUSE Linux Enterprise Server:使用 Btrfs 存储驱动。
- Windows:Windows 只有一种驱动,已经默认设置。
上述清单只作为建议。随着时间发展,Overlay2 驱动正在逐渐流行,可能在未来会成为大多数平台上的推荐存储驱动。如果使用 Docker 企业版(EE),并且有技术支持合约,建议通过咨询获取最新的兼容矩阵。
默认情况下,容器的所有存储都使用本地存储。所以默认情况下容器全部目录都是用该存储。
容器与持久化数据
在容器中持久化数据的方式推荐采用卷。
总体来说,用户创建卷,然后创建容器,接着将卷挂载到容器上。卷会挂载到容器文件系统的某个目录之下,任何写到该目录下的内容都会写到卷中。即使容器被删除,卷与其上面的数据仍然存在。
如下图所示,Docker 卷挂载到容器的 /code 目录。任何写入 /code 目录的数据都会保存到卷当中,并且在容器删除后依然存在。
上图中,/code 目录是一个 Docker 卷。容器其他目录均使用临时的本地存储。卷与目录 /code 之间采用带箭头的虚线连接,这是为了表明卷与容器是非耦合的关系。
在集群节点间共享存储
Docker 能够集成外部存储系统,使得集群间节点共享外部存储数据变得简单
例如,独立存储 LUN 或者 NFS 共享可以应用到多个 Docker 主机,因此无论容器或者服务副本运行在哪个节点上,都可以共享该存储。
下图展示了位于共享存储的卷被两个 Docker 节点共享的场景。接下来这些 Docker 节点可以将共享卷应用到容器之上。
构建这样的环境需要外部存储系统的相关知识,并了解应用如何从共享存储读取或者写入数据。这种配置主要关注数据损坏(Data Corruption)。
基于上图,设想下面的场景:
Node 1 上的容器 A 在共享卷中更新了部分数据。但是为了快速返回,数据实际写入了本地缓存而不是卷中。此时,容器 A 认为数据已经更新。但是,在 Node 1 的容器 A 将缓存数据刷新并提交到卷前,Node 2 的容器 B 更新了相同部分的数据,但是值不同,并且更新方式为直接写入卷中。
此时,两个容器均认为自己已经将数据写入卷中,但实际上只有容器 B 写入了。容器 A 会在稍后将自己的缓存数据写入缓存,覆盖了 Node 2 的容器 B 所做的一些变更。但是 Node 2 上的容器 B 对此一无所知。数据损坏就是这样发生的。
为了避免这种情况,需要在应用程序中进行控制。
生产环境中使用Docker的过程中,往往需要对数据进行持久化,或者需要在多个容器之间进行数据共享,这必然涉及容器的数据管理操作。
容器中管理数据主要有两种方式:
- 数据卷(Data Volumes):容器内数据直接映射到本地主机环境;
- 数据卷容器(Data Volume Containers):使用特定容器维护数据卷。
二、数据卷
数据卷是一个可供容器使用的特殊目录,它将主机操作系统目录直接映射进容器,类似于Linux中的mount操作。
数据卷可以提供很多有用的特性,如下所示:
- 数据卷可以在容器之间共享和重用,容器间传递数据将变得高效方便;
- 对数据卷内数据的修改会立马生效,无论是容器内操作还是本地操作;
- 对数据卷的更新不会影响镜像,解耦了应用和数据;
- 卷会一直存在,直到没有容器使用,可以安全地卸载它。
我们可以在创建容器的时候,将宿主机的目录与容器内的目录进行映射,这样就可以通过修改宿主机某个目录的文件从而去影响容器。而且这个操作是双向绑定的,也就是说容器内的操作也会影响到宿主机,实现备份功能。
但是容器被删除的时候,宿主机的数据卷内容并不会被删除,因为底层是通过拷贝实现的。如果多个容器挂载同一个目录,其中一个容器被删除,其他容器的内容也不会受到影响,同理,底层是拷贝实现的。
容器与宿主机之间的数据卷属于引用的关系,数据卷是从外界挂载到容器内部中的,所以可以脱离容器的生命周期而独立存在,正是由于数据卷的生命周期并不等同于容器的生命周期,在容器退出或者删除以后,数据卷仍然不会受到影响,数据卷的生命周期会一直持续到没有容器使用它为止。
创建和管理数据卷
Usage: docker volume COMMAND
Manage volumes
Commands:
create 创建新卷。默认情况下,新卷创建使用 local 驱动,但是可以通过 -d 参数来指定不同的驱动。
inspect 查看卷的详细信息。可以使用该命令查看卷在 Docker 主机文件系统中的具体位置。
ls 列出本地 Docker 主机上的全部卷。可以通过 -q 参数仅显示数据卷名
prune 删除未被容器或者服务副本使用的全部卷。会删除未装入到某个容器或者服务的所有卷,所以谨慎使用!
rm 删除一个或多个数据卷;两种删除命令都不能删除正在被容器或者服务使用的卷。
删除所有的数据卷:
docker volume rm $(docker volume ls -q)
或 docker volume rm `docker volume ls -q`
或 docker volume ls -q | xargs docker volume rm
或 docker volume rm `docker volume ls | awk 'NR!=1 {print $2}'`
使用下面的命令创建名为 myvolume 的新卷
[root@docker ~]# docker volume create myvolume
默认情况下,Docker 创建新卷时采用内置的 local 驱动。恰如其名,本地卷只能被所在节点的容器使用。使用 -d 参数可以指定不同的驱动。
第三方驱动可以通过插件方式接入。这些驱动提供了高级存储特性,并为 Docker 集成了外部存储系统。下图展示的就是外部存储系统被用作卷存储。驱动集成了外部存储系统到 Docker 环境当中,同时能使用其高级特性。
截止到目前为止,已经存在 25 种卷插件,涵盖了块存储、文件存储、对象存储等。
- 块存储:相对性能更高,适用于对小块数据的随机访问负载。目前支持 Docker 卷插件的块存储例子包括 HPE 3PAR、Amazon EBS 以及 OpenStack 块存储服务(Cinder)。
- 文件存储:包括 NFS 和 SMB 协议的系统,同样在高性能场景下表现优异。支持 Docker 卷插件的文件存储系统包括 NetApp FAS、Azure 文件存储以及 Amazon EFS。
- 对象存储:适用于较大且长期存储的、很少变更的二进制数据存储。通常对象存储是根据内容寻址,并且性能较低。支持 Docker 卷驱动的例子包括 Amazon S3、Ceph 以及 Minio。
卷已经创建成功,可以通过 docker volume ls 命令进行查看,还可以使用 docker volume inspect VOLUME_NAME 命令查看详情。
[root@docker ~]# docker volume ls
DRIVER VOLUME NAME
local myvolume
[root@docker ~]# docker volume inspect myvolume
[
{
"CreatedAt": "2021-06-19T23:05:55+08:00",
"Driver": "local",
"Labels": {},
"Mountpoint": "/var/lib/docker/volumes/myvolume/_data",
"Name": "myvolume",
"Options": {},
"Scope": "local"
}
]
# inspect 命令输出中的 Driver 和 Scope 都是 local。这意味着卷使用默认 local 驱动创建,
# 只能用于当前 Docker 主机上的容器。Mountpoint 属性说明卷位于 Docker 主机上的位置。
使用 local 驱动创建的卷在 Docker 主机上均有其专属目录,在 Linux 中位于 /var/lib/docker/volumes 目录下,这意味着可以在 Docker 主机文件系统中查看卷,甚至在 Docker 主机中对其进行读取数据或者写入数据操作。
有两种方式删除 Docker 卷。
docker volume prune
docker volume rm VOLUME_NAME
docker volume prune 会删除未装入到某个容器或者服务的所有卷,所以谨慎使用!docker volume rm VOLUME_NAME 允许删除指定卷。两种删除命令都不能删除正在被容器或者服务使用的卷。
在运行容器的同时创建数据卷
在用 docker run 或 docker create 命令的时候,使用 -v 选项可以在容器内创建一个数据卷。多次重复使用 -v 选项可以创建多个数据卷。
用法:
docker run [OPTIONS] -v [宿主机目录1:]容器目录1 -v 宿主机目录2:容器目录2 ... 镜像名
# Docker挂载数据卷的默认权限是读写(rw),用户也可以通过ro指定为只读
docker run [OPTIONS] -v [宿主机目录1:]容器目录1:ro -v 宿主机目录2:容器目录2:ro ... 镜像名
# 如果宿主机目录不存在,Docker会自动创建。容器内目录不存在,Docker也会自动创建。
# 目录挂载操作可能会出现权限不足的提示。这是因为 CentOS7 中的安全模块 SELinux 把权限禁掉了,在 docker run 时通过 --privileged=true 给该容器加权限来解决挂载的目录没有权限的问题。
# 查看容器在宿主机上挂载的数据卷位置
docker inspect CONTAINER -f "{{.Mounts}}"
(一)匿名挂载
匿名挂载只需要写容器目录即可,宿主机对应的目录会在 /var/lib/docker/volume/ 下生成一个随机字符串的目录。
用法:
docker run [OPTIONS] -v 容器目录路径1 [-v 容器目录路径2 ...] 镜像名
# -v 后只写容器内的路径,不需要写容器外的路径,默认会在/var/lib/docker/volume/ 下生成一个随机字符串的数据卷目录。
# 启动容器并匿名挂载数据卷
[root@docker ~]# docker run --name mycentos -di -v /usr/local/data centos:7
1859506822dc5035e2e7aa535d7c1ff2d5db922cecc028091364a2b25d9b3ed2
# 查看该容器在宿主机上挂载的数据卷位置
[root@docker ~]# docker inspect mycentos -f "{{.Mounts}}"
[{volume e58e5b93fe13e6c3dbc601f13abe731904bcd37f7fa0157ae11c4a03d6a77946 /var/lib/docker/volumes/e58e5b93fe13e6c3dbc601f13abe731904bcd37f7fa0157ae11c4a03d6a77946/_data /usr/local/data local true }]
# 查看宿主机的数据卷
[root@docker ~]# docker volume ls
DRIVER VOLUME NAME
local e58e5b93fe13e6c3dbc601f13abe731904bcd37f7fa0157ae11c4a03d6a77946
[root@docker ~]# docker volume inspect e58e5b93fe13e6c3dbc601f13abe731904bcd37f7fa0157ae11c4a03d6a77946
[
{
"CreatedAt": "2021-06-19T12:33:06+08:00",
"Driver": "local",
"Labels": null,
"Mountpoint": "/var/lib/docker/volumes/e58e5b93fe13e6c3dbc601f13abe731904bcd37f7fa0157ae11c4a03d6a77946/_data",
"Name": "e58e5b93fe13e6c3dbc601f13abe731904bcd37f7fa0157ae11c4a03d6a77946",
"Options": null,
"Scope": "local"
}
]
# 进入宿主机挂载目录进行查看
[root@docker ~]# cd /var/lib/docker/volumes/e58e5b93fe13e6c3dbc601f13abe731904bcd37f7fa0157ae11c4a03d6a77946/
[root@docker e58e5b93fe13e6c3dbc601f13abe731904bcd37f7fa0157ae11c4a03d6a77946]# ls
_data
[root@docker e58e5b93fe13e6c3dbc601f13abe731904bcd37f7fa0157ae11c4a03d6a77946]# cd _data/
[root@docker _data]# ls
# 在容器的挂载目录中进行写入操作
[root@docker ~]# docker exec -it mycentos /bin/bash
[root@1859506822dc /]# cd /usr/local/data
[root@1859506822dc data]# touch test.txt
[root@1859506822dc data]# ls
test.txt
# 在宿主机的挂载目录可以看到实时同步了
[root@docker _data]# ls
test.txt
# 在宿主机挂载目录进行写入操作
[root@docker _data]# touch test1.txt
[root@docker _data]# ls
test1.txt test.txt
# 在容器挂载目录下同样可以实时看到也是实时同步的
[root@1859506822dc data]# ls
test.txt test1.txt
# 删除容器
[root@docker ~]# docker rm -f mycentos
mycentos
# 本地数据卷还存在
[root@docker ~]# docker volume ls
DRIVER VOLUME NAME
local e58e5b93fe13e6c3dbc601f13abe731904bcd37f7fa0157ae11c4a03d6a77946
# 删除
[root@docker ~]# docker volume rm e58e5b93fe13e6c3dbc601f13abe731904bcd37f7fa0157ae11c4a03d6a77946
e58e5b93fe13e6c3dbc601f13abe731904bcd37f7fa0157ae11c4a03d6a77946
[root@docker ~]# docker volume ls
DRIVER VOLUME NAME
# 同时 /var/lib/docker/volume/ 下的对应文件夹也会被删除
(二)具名挂载
具名挂载就是给宿主机的数据卷自定义名称并且对应的目录还是会在 /var/lib/docker/volume/ 下生成。
用法:
docker run [OPTIONS] -v 宿主机数据卷名称1:容器目录路径1 [-v 宿主机数据卷名称2:容器目录路径2 ...] 镜像名
# 宿主机数据卷名称,是在宿主机上的数据卷名称,不需要写宿主机的绝对路径,默认会在/var/lib/docker/volume/目录下生成名为 ‘宿主机数据卷名称’ 的目录
[root@docker ~]# docker run --name mycentos1 -di -v mycentos1_data:/usr/local/data centos:7
32263a17a697cc13d03edd65fae993042b49dcd62ee8151e29fe9a58deabf3ec
[root@docker ~]# docker inspect mycentos1 -f "{{.Mounts}}"
[{volume mycentos1_data /var/lib/docker/volumes/mycentos1_data/_data /usr/local/data local z true }]
[root@docker ~]# ll -d /var/lib/docker/volumes/mycentos1_data/_data
drwxr-xr-x. 2 root root 6 Jun 19 13:29 /var/lib/docker/volumes/mycentos1_data/_data
(三)指定目录挂载
自定义宿主机数据卷的路径
用法:
docker run [OPTIONS] -v 宿主机目录路径1:容器目录路径1 [-v 宿主机目录路径2:容器目录路径2 ...] 镜像名
# 此时指定的宿主机目录为自定义的绝对路径
[root@docker test]# docker run --name mycentos2 -di -v /tmp/test:/etc centos:7
df6ea6a8c3ef2bed228060c0863b9e35a7dc30442ac7c50db96ae0c02c6476a0
[root@docker test]# docker inspect mycentos2 -f "{{.Mounts}}"
[{bind /tmp/test /etc true rprivate}]
[root@docker test]# ll -d /tmp/test/
drwxr-xr-x. 2 root root 54 Jun 19 13:38 /tmp/test/
[root@docker test]# cd /tmp/test/
[root@docker test]# ls
hostname hosts resolv.conf
(四)只读/读写
# 只读 readonly。只能通过修改宿主机内容实现对容器的数据管理,容器内对该数据卷只有读权限,没有写权限。
docker run [OPTIONS] -v [宿主机目录:]容器目录:ro 镜像名
# 读写,默认。宿主机和容器可以双向操作(读写)数据。
docker run [OPTIONS] -v [宿主机目录:]容器目录:rw 镜像名
三、数据卷容器
如果用户需要在多个容器之间共享一些持续更新的数据,最简单的方式是使用数据卷容器。数据卷容器也是一个容器,但是它的目的是专门用来提供数据卷供其他容器挂载。
就是从另一个容器当中挂载容器中已经创建好的数据卷。
用法:
docker run [OPTIONS] --volumes-from 被继承的容器名 镜像名
# 被继承的容器必须指定了有效的数据卷挂载
首先,创建一个数据卷容器 dbdata,并在其中创建一个数据卷挂载到/dbdata
[root@docker ~]# docker run -it -v /dbdata --name dbdata centos:7
# 查看/ dbdata目录
[root@3c73ee8ab64d /]# ll -d dbdata/
drwxr-xr-x. 2 root root 6 Jun 19 06:00 dbdata/
然后,可以在其他容器中使用--volumes-from来挂载dbdata容器中的数据卷,例如创建db1和db2两个容器,并从dbdata容器挂载数据卷
[root@docker ~]# docker run -it --volumes-from dbdata --name db1 centos:7
[root@929ae0879d14 /]# ll -d dbdata/
drwxr-xr-x. 2 root root 6 Jun 19 06:00 dbdata/
[root@docker ~]# docker run -it --volumes-from dbdata --name db2 centos:7
[root@56c6a0ad1545 /]# ll -d /dbdata/
drwxr-xr-x. 2 root root 6 Jun 19 06:00 /dbdata/
此时,容器db1和db2都挂载同一个数据卷到相同的/ dbdata目录。三个容器任何一方在该目录下的写入,其他容器都可以看到。
[root@docker ~]# docker ps
CONTAINER ID IMAGE COMMAND CREATED STATUS PORTS NAMES
56c6a0ad1545 centos:7 "/bin/bash" About a minute ago Up About a minute db2
929ae0879d14 centos:7 "/bin/bash" About a minute ago Up About a minute db1
3c73ee8ab64d centos:7 "/bin/bash" 3 minutes ago Up 3 minutes dbdata
# 此时在dbdata容器中创建一个test文件
[root@docker ~]# docker exec -it dbdata /bin/bash
[root@3c73ee8ab64d /]# cd /dbdata/
[root@3c73ee8ab64d dbdata]# echo dbdata > test
[root@3c73ee8ab64d dbdata]# cat test
dbdata
# 在 db1 和 db2 容器内查看
[root@929ae0879d14 /]# cat dbdata/test
dbdata
[root@56c6a0ad1545 /]# cat dbdata/test
dbdata
可以多次使用 --volumes-from 参数来从多个容器挂载多个数据卷。还可以从其他已经挂载了容器卷的容器来挂载数据卷。
其实,三个容器内的 /dbdata 目录均挂载在宿主机相同目录下
[root@docker ~]# docker volume ls
DRIVER VOLUME NAME
local ff44855a24bc350d0358a9e07bdc1b52714f98fb016798054d3cbaddfbbc01ed
[root@docker ~]# docker inspect dbdata -f "{{.Mounts}}"
[{volume ff44855a24bc350d0358a9e07bdc1b52714f98fb016798054d3cbaddfbbc01ed /var/lib/docker/volumes/ff44855a24bc350d0358a9e07bdc1b52714f98fb016798054d3cbaddfbbc01ed/_data /dbdata local true }]
[root@docker ~]# docker inspect db1 -f "{{.Mounts}}"
[{volume ff44855a24bc350d0358a9e07bdc1b52714f98fb016798054d3cbaddfbbc01ed /var/lib/docker/volumes/ff44855a24bc350d0358a9e07bdc1b52714f98fb016798054d3cbaddfbbc01ed/_data /dbdata local true }]
[root@docker ~]# docker inspect db2 -f "{{.Mounts}}"
[{volume ff44855a24bc350d0358a9e07bdc1b52714f98fb016798054d3cbaddfbbc01ed /var/lib/docker/volumes/ff44855a24bc350d0358a9e07bdc1b52714f98fb016798054d3cbaddfbbc01ed/_data /dbdata local true }]
[root@docker ~]# cd /var/lib/docker/volumes/ff44855a24bc350d0358a9e07bdc1b52714f98fb016798054d3cbaddfbbc01ed/_data
[root@docker _data]# ls
test
[root@docker _data]# cat test
dbdata
注意:
使用 --volumes-from 参数所挂载数据卷的容器自身并不需要保持在运行状态。
如果删除了挂载的容器(包括dbdata、db1和db2),数据卷并不会被自动删除。如果要删除一个数据卷,必须在删除最后一个还挂载着它的容器时显式使用 docker rm -v 命令来指定同时删除关联的容器。
所以,数据卷解决了有状态服务需要持久化数据的痛点问题