Skip to content

管理和存储数据

数据是应用程序重要的产出,所以很好的管理和存储数据,是对应用程序劳动结果的尊重。特别是在大数据时代,所有的数据都是重要的资产,保护好数据是每个开发者必须掌握的技能。我们知道,在 Docker 里,容器运行的文件系统处于沙盒环境中,与外界其实是隔离的,那么我们又要如何在 Docker 中合理的通过文件与外界进行数据交换呢?在这一小节中,我们就来介绍 Docker 中与文件数据有关的内容。

数据管理实现方式

Docker 容器中的文件系统于我们这些开发使用者来说,虽然有很多优势,但也有很多弊端,其中显著的两点就是:

  • 沙盒文件系统是跟随容器生命周期所创建和移除的,数据无法直接被持久化存储。
  • 由于容器隔离,我们很难从容器外部获得或操作容器内部文件中的数据。

当然,Docker 很好的解决了这些问题,这主要还是归功于 Docker 容器文件系统是基于 UnionFS。由于 UnionFS 支持挂载不同类型的文件系统到统一的目录结构中,所以我们只需要将宿主操作系统中,文件系统里的文件或目录挂载到容器中,便能够让容器内外共享这个文件。

由于通过这种方式可以互通容器内外的文件,那么文件数据持久化和操作容器内文件的问题就自然而然的解决了。

同时,UnionFS 带来的读写性能损失是可以忽略不计的,所以这种实现可以说是相当优秀的。

挂载方式

基于底层存储实现,Docker 提供了三种适用于不同场景的文件系统挂载方式:Bind MountVolumeTmpfs Mount

  • Bind Mount 能够直接将宿主操作系统中的目录和文件挂载到容器内的文件系统中,通过指定容器外的路径和容器内的路径,就可以形成挂载映射关系,在容器内外对文件的读写,都是相互可见的。

  • Volume 也是从宿主操作系统中挂载目录到容器内,只不过这个挂载的目录由 Docker 进行管理,我们只需要指定容器内的目录,不需要关心具体挂载到了宿主操作系统中的哪里。

  • 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 的时候,一定要特别注意挂载的外部目录选择。当然,在保证安全性的前提下,有几种常见场景非常适合使用这种挂载方式。

  • 当我们需要从宿主操作系统共享配置的时候。对于一些配置项,我们可以直接从容器外部挂载到容器中,这利于保证容器中的配置为我们所确认的值,也方便我们对配置进行监控。例如,遇到容器中时区不正确的时候,我们可以直接将操作系统的时区配置,也就是 /etc/timezone 这个文件挂载并覆盖容器中的时区配置。

  • 当我们需要借助 Docker 进行开发的时候。虽然在 Docker 中,推崇直接将代码和配置打包进镜像,以便快速部署和快速重建。但这在开发过程中显然非常不方便,因为每次构建镜像需要耗费一定的时间,这些时间积少成多,就是对开发工作效率的严重浪费了。如果我们直接把代码挂载进入容器,那么我们每次对代码的修改都可以直接在容器外部进行。

挂载临时文件目录

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

恢复的过程与备份的过程类似,只不过把打包的命令转换为解包的命令而已。

另一个挂载选项

上面我们讲到了使用 -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 选项来定义挂载,这里我们只是将它简单介绍,供大家参考。

留言互动

在本节中,我们介绍了关于如何在 Docker 中管理和存储数据的方法。这里给大家留一道实践题:

结合我们所提到三种挂载方式各自的适用场景,分别尝试使用它们进行数据挂载。

欢迎大家通过留言的方式说出你的实践之路。我会选出有代表性的优质留言,推荐给大家。

同时,如果你对 Docker 中的数据挂载还有不理解的地方,或者对其有独特的见解,可以加入到这本小册的官方微信群中,参与对相关问题的讨论。

文章来源于自己总结和网络转载,内容如有任何问题,请大佬斧正!联系我