数据是应用程序重要的产出,所以很好的管理和存储数据,是对应用程序劳动结果的尊重。特别是在大数据时代,所有的数据都是重要的资产,保护好数据是每个开发者必须掌握的技能。我们知道,在 Docker 里,容器运行的文件系统处于沙盒环境中,与外界其实是隔离的,那么我们又要如何在 Docker 中合理的通过文件与外界进行数据交换呢?在这一小节中,我们就来介绍 Docker 中与文件数据有关的内容。
Docker 容器中的文件系统于我们这些开发使用者来说,虽然有很多优势,但也有很多弊端,其中显著的两点就是:
当然,Docker 很好的解决了这些问题,这主要还是归功于 Docker 容器文件系统是基于 UnionFS。由于 UnionFS 支持挂载不同类型的文件系统到统一的目录结构中,所以我们只需要将宿主操作系统中,文件系统里的文件或目录挂载到容器中,便能够让容器内外共享这个文件。
由于通过这种方式可以互通容器内外的文件,那么文件数据持久化和操作容器内文件的问题就自然而然的解决了。
同时,UnionFS 带来的读写性能损失是可以忽略不计的,所以这种实现可以说是相当优秀的。
基于底层存储实现,Docker 提供了三种适用于不同场景的文件系统挂载方式:Bind Mount、Volume 和 Tmpfs Mount。
要将宿主操作系统中的目录挂载到容器之后,我们可以在容器创建的时候通过传递 -v
或 --volume
选项来指定内外挂载的对应目录或文件。
$ sudo docker run -d --name nginx -v /webapp/html:/usr/share/nginx/html nginx:1.12
使用 -v
或 --volume
来挂载宿主操作系统目录的形式是 -v <host-path>:<container-path>
或 --volume <host-path>:<container-path>
,其中 host-path 和 container-path 分别代表宿主操作系统中的目录和容器中的目录。这里需要注意的是,为了避免混淆,Docker 这里强制定义目录时必须使用绝对路径,不能使用相对路径。
我们能够指定目录进行挂载,也能够指定具体的文件来挂载,具体选择何种形式来挂载,大家可以根据具体的情况来选择。
当挂载了目录的容器启动后,我们可以看到我们在宿主操作系统中的文件已经出现在容器中了。
$ sudo docker exec nginx ls /usr/share/nginx/html
index.html
在 docker inspect
的结果里,我们可以看到有关容器数据挂载相关的信息。
$ sudo docker inspect nginx
[
{
## ......
"Mounts": [
{
"Type": "bind",
"Source": "/webapp/html",
"Destination": "/usr/share/nginx/html",
"Mode": "",
"RW": true,
"Propagation": "rprivate"
}
],
## ......
}
]
在关于挂载的信息中我们可以看到一个 RW 字段,这表示挂载目录或文件的读写性 ( Read and Write )。实际操作中,Docker 还支持以只读的方式挂载,通过只读方式挂载的目录和文件,只能被容器中的程序读取,但不接受容器中程序修改它们的请求。在挂载选项 -v
后再接上 :ro
就可以只读挂载了。
$ sudo docker run -d --name nginx -v /webapp/html:/usr/share/nginx/html:ro nginx:1.12
由于宿主操作系统文件挂载在权限允许的情况下能够挂载任何目录或文件,这给系统的安全性造成了一定的隐患,所以我们在使用 Bind Mount 的时候,一定要特别注意挂载的外部目录选择。当然,在保证安全性的前提下,有几种常见场景非常适合使用这种挂载方式。
Tmpfs Mount 是一种特殊的挂载方式,它主要利用内存来存储数据。由于内存不是持久性存储设备,所以其带给 Tmpfs Mount 的特征就是临时性挂载。
与挂载宿主操作系统目录或文件不同,挂载临时文件目录要通过 --tmpfs
这个选项来完成。由于内存的具体位置不需要我们来指定,这个选项里我们只需要传递挂载到容器内的目录即可。
$ sudo docker run -d --name webapp --tmpfs /webapp/cache webapp:latest
容器已挂载的临时文件目录我们也可以通过 docker inspect
命令查看。
$ sudo docker inspect webapp
[
{
## ......
"Tmpfs": {
"/webapp/cache": ""
},
## ......
}
]
挂载临时文件首先要注意它不是持久存储这一特性,在此基础上,它有几种常见的适应场景。
除了与其他虚拟机工具近似的宿主操作系统目录挂载的功能外,Docker 还创造了数据卷 ( Volume ) 这个概念。数据卷的本质其实依然是宿主操作系统上的一个目录,只不过这个目录存放在 Docker 内部,接受 Docker 的管理。
在使用数据卷进行挂载时,我们不需要知道数据具体存储在了宿主操作系统的何处,只需要给定容器中的哪个目录会被挂载即可。
我们依然可以使用 -v
或 --volume
选项来定义数据卷的挂载。
$ sudo docker run -d --name webapp -v /webapp/storage webapp:latest
数据卷挂载到容器后,我们可以通过 docker inspect
看到容器中数据卷挂载的信息。
$ sudo docker inspect webapp
[
{
## ......
"Mounts": [
{
"Type": "volume",
"Name": "2bbd2719b81fbe030e6f446243386d763ef25879ec82bb60c9be7ef7f3a25336",
"Source": "/var/lib/docker/volumes/2bbd2719b81fbe030e6f446243386d763ef25879ec82bb60c9be7ef7f3a25336/_data",
"Destination": "/webapp/storage",
"Driver": "local",
"Mode": "",
"RW": true,
"Propagation": ""
}
],
## ......
}
]
这里我们所得到的信息与绑定挂载有所区别,除了 Type 中的类型不一样之外,在数据卷挂载中,我们还要关注一下 Name 和 Source 这两个信息。
其中 Source 是 Docker 为我们分配用于挂载的宿主机目录,其位于 Docker 的资源区域 ( 这里是默认的 /var/lib/docker ) 内。当然,我们并不需要关心这个目录,一切对它的管理都已经在 Docker 内实现了。
为了方便识别数据卷,我们可以像命名容器一样为数据卷命名,这里的 Name 就是数据卷的命名。在我们未给出数据卷命名的时候,Docker 会采用数据卷的 ID 命名数据卷。我们也可以通过 -v <name>:<container-path>
这种形式来命名数据卷。
sudo docker run -d --name webapp -v appdata:/webapp/storage webapp:latest
由于 -v
选项既承载了 Bind Mount 的定义,又参与了 Volume 的定义,所以其传参方式需要特别留意。前面提到了,-v
在定义绑定挂载时必须使用绝对路径,其目的主要是为了避免与数据卷挂载中命名这种形式的冲突。
虽然与绑定挂载的原理差别不大,但数据卷在许多实际场景下你会发现它很有用。
数据卷的另一大作用是实现容器间的目录共享,也就是通过挂载相同的数据卷,让容器之间能够同时看到并操作数据卷中的内容。这个功能虽然也可以通过绑定挂载来实现,但通过数据卷来操作会更加的舒适、简单。
由于数据卷的命名在 Docker 中是唯一的,所以我们很容易通过数据卷的名称确定数据卷,这就让我们很方便的让多个容器挂载同一个数据卷了。
$ sudo docker run -d --name webapp -v html:/webapp/html webapp:latest
$ sudo docker run -d --name nginx -v html:/usr/share/nginx/html:ro nginx:1.12
我们使用 -v
选项挂载数据卷时,如果数据卷不存在,Docker 会为我们自动创建和分配宿主操作系统的目录,而如果同名数据卷已经存在,则会直接引用。
如果有朋友觉得这样对数据卷的操作方式还不够直接和准确,我们还可以通过 docker volume
下的几个命令专门操作数据卷。
通过 docker volume create
我们可以不依赖于容器独立创建数据卷。
$ sudo docker volume create appdata
通过 docker volume ls
可以列出当前已创建的数据卷。
$ sudo docker volume ls
DRIVER VOLUME NAME
local html
local appdata
虽然数据卷的目的是用来持久化存储数据的,但有时候我们也难免有删除它们以释放空间的需求。直接去 Docker 的目录下删除显然不是好的选择,我们应该通过 Docker 对数据卷的管理命令来删除它们。
我们可以直接通过 docker volume rm
来删除指定的数据卷。
$ sudo docker volume rm appdata
在删除数据卷之前,我们必须保证数据卷没有被任何容器所使用 ( 也就是之前引用过这个数据卷的容器都已经删除 ),否则 Docker 不会允许我们删除这个数据卷。
对于我们没有直接命名的数据卷,因为要反复核对数据卷 ID,这样的方式并不算特别友好。这种没有命名的数据卷,通常我们可以看成它们与对应的容器产生了绑定,因为其他容器很难使用到它们。而这种绑定关系的产生,也让我们可以在容器删除时将它们一并删除。
在 docker rm
删除容器的命令中,我们可以通过增加 -v
选项来删除容器关联的数据卷。
sudo docker rm -v webapp
如果我们没有随容器删除这些数据卷,Docker 在创建新的容器时也不会启用它们,即使它们与新创建容器所定义的数据卷有完全一致的特征。也就是说,此时它们已经变成了孤魂野鬼,纯粹的占用着硬盘空间而又不受管理。
此时我们可以通过 docker volume rm
来删除它们,但前提时你能在一堆乱码般的数据卷 ID 中找出哪个是没有被容器引用的数据卷。
为此,Docker 向我们提供了 docker volume prune
这个命令,它可以删除那些没有被容器引用的数据卷。
$ sudo docker volume prune -f
Deleted Volumes:
af6459286b5ce42bb5f205d0d323ac11ce8b8d9df4c65909ddc2feea7c3d1d53
0783665df434533f6b53afe3d9decfa791929570913c7aff10f302c17ed1a389
65b822e27d0be93d149304afb1515f8111344da9ea18adc3b3a34bddd2b243c7
## ......
在数据卷的基础上,我们有一种相对新颖的用法,也就是数据卷容器。所谓数据卷容器,就是一个没有具体指定的应用,甚至不需要运行的容器,我们使用它的目的,是为了定义一个或多个数据卷并持有它们的引用。
创建数据卷容器的方式很简单,由于不需要容器本身运行,因而我们找个简单的系统镜像都可以完成创建。
$ sudo docker create --name appdata -v /webapp/storage ubuntu
在使用数据卷容器时,我们不建议再定义数据卷的名称,因为我们可以通过对数据卷容器的引用来完成数据卷的引用。而不设置数据卷的名称,也避免了在同一 Docker 中数据卷重名的尴尬。
之前我们提到,Docker 的 Network 是容器间的网络桥梁,如果做类比,数据卷容器就可以算是容器间的文件系统桥梁。我们可以像加入网络一样引用数据卷容器,只需要在创建新容器时使用专门的 --volumes-from
选项即可。
$ sudo docker run -d --name webapp --volumes-from appdata webapp:latest
引用数据卷容器时,不需要再定义数据卷挂载到容器中的位置,Docker 会以数据卷容器中的挂载定义将数据卷挂载到引用的容器中。
虽然看上去数据卷容器与数据卷的使用方法变化不大,但最关键的就在于其真正隐藏了数据卷的配置和定义,我们只需要通过数据卷容器的名称来使用它。这些细节的隐藏,意味着我们能够更轻松的实现容器的迁移。
由于数据卷本身就是宿主操作系统中的一个目录,我们只需要在 Docker 资源目录里找到它就可以很轻松的打包、迁移、恢复了。虽然这么做相对其他虚拟化方案来说已经很简单了,但在 Docker 里还不是最优雅的解决方式。
利用数据卷容器,我们还能够更方便的对数据卷中的数据进行迁移。
数据备份、迁移、恢复的过程可以理解为对数据进行打包,移动到其他位置,在需要的地方解压的过程。在数据打包之前,我们先建立一个用来存放打包文件的目录,这里我们使用 /backup
作为例子。
要备份数据,我们先建立一个临时的容器,将用于备份的目录和要备份的数据卷都挂载到这个容器上。
$ sudo docker run --rm --volumes-from appdata -v /backup:/backup ubuntu tar cvf /backup/backup.tar /webapp/storage
在这条命令中,除了挂载的配置外,我们再注意几个选项。通过 --rm
选项,我们可以让容器在停止后自动删除,而不需要我们再使用容器删除命令来删除它,这对于我们使用一些临时容器很有帮助。在容器所基于的镜像之后,我们还看到了一串命令,也就是 tar cvf /backup/backup.tar /webapp/storage
,其实如果我们在镜像定义之后接上命令,可以直接替换掉镜像所定义的主程序启动命令,而去执行这一条命令。在很多场合下,我们还能通过这个方法干很多不同的事情。
在备份后,我们就可以在 /backup 下找到数据卷的备份文件,也就是 backup.tar 了。
如果要恢复数据卷中的数据,我们也可以借助临时容器完成。
$ docker run --rm --volumes-from appdata -v /backup:/backup ubuntu tar xvf /backup/backup.tar -C /webapp/storage --strip
在这条命令中,除了挂载的配置外,我们再注意几个选项。通过 --rm
选项,我们可以让容器在停止后自动删除,而不需要我们再使用容器删除命令来删除它,这对于我们使用一些临时容器很有帮助。在容器所基于的镜像之后,我们还看到了一串命令,也就是 tar cvf /backup/backup.tar /webapp/storage
,其实如果我们在镜像定义之后接上命令,可以直接替换掉镜像所定义的主程序启动命令,而去执行这一条命令。在很多场合下,我们还能通过这个方法干很多不同的事情。
在备份后,我们就可以在 /backup 下找到数据卷的备份文件,也就是 backup.tar 了。
如果要恢复数据卷中的数据,我们也可以借助临时容器完成。
$ docker run --rm --volumes-from appdata -v /backup:/backup ubuntu tar xvf /backup/backup.tar -C /webapp/storage --strip
恢复的过程与备份的过程类似,只不过把打包的命令转换为解包的命令而已
上面我们讲到了使用 -v
选项来挂载存在容易混淆的问题,其主要原因是挂载的方式和配置随着 Docker 的不断发展日渐丰富,而 -v
选项的传参方式限制了它能使用的场景。
其实在 Docker 里为我们提供了一个相对支持丰富的挂载方式,也就是通过 --mount
这个选项配置挂载。
$ sudo docker run -d --name webapp webapp:latest --mount 'type=volume,src=appdata,dst=/webapp/storage,volume-driver=local,volume-opt=type=nfs,volume-opt=device=<nfs-server>:<nfs-path>' webapp:latest
在 --mount
中,我们可以通过逗号分隔这种 CSV 格式来定义多个参数。其中,通过 type 我们可以定义挂载类型,其值可以是:bind,volume 或 tmpfs。另外,--mount
选项能够帮助我们实现集群挂载的定义,例如在这个例子中,我们挂载的来源是一个 NFS 目录。
由于在实际开发中,-v
基本上足够满足我们的需求,所以我们不常使用相对复杂的 --mount
选项来定义挂载,这里我们只是将它简单介绍,供大家参考。