在实际生产环境中,Docker 容器通常用于运行各种应用服务。每个容器实例都是基于镜像标准化创建,具备独立、隔离的运行环境。 然而,容器的文件系统本质上是临时性的:当容器被删除、重建或升级时,容器内部产生的所有数据都将随之丢失。这种设计有利于环境一致性和弹性管理,但对于需要持久化保存的重要业务数据而言,就不可行了。
为此,Docker 提供了数据卷(Volume)机制,专门用于实现数据持久化与容器生命周期的解耦。通过数据卷,用户可以将需要保存的数据存储在主机文件系统的特定位置,即便容器发生变更或被销毁,相关数据依然可以被保留和访问。 数据卷不仅提升了数据的安全性,还支持多容器间的数据共享与协作,是容器化部署中数据管理的核心组件。

在 Docker 的世界里,我们首先要学会区分两种数据。一种是临时数据,比如应用程序在运行时产生的临时缓存文件,这些文件在程序停止后就失去了价值。 另一种是永恒数据(或称持久化数据),这是我们真正关心的数据,例如用户的数据库记录、网站的上传内容、重要的日志文件等。
Docker 为这两种不同需求都提供了巧妙的解决方案。
每个 Docker 容器在启动时,都会在只读的镜像之上覆盖一个薄薄的、可读写的“存储层”。你可以把它想象成一个随容器附赠的“速记本”。 当你的应用程序需要在容器内写入文件时,比如记录一些临时日志或创建临时文件,它实际上是在这个速记本上写写画画。这非常方便,因为它不需要任何额外的配置。
但关键在于,这个速记本的生命周期与容器是完全绑定的。一旦容器被删除,这个速记本以及上面所有的记录都会被一同销毁。因此,它只适合存放那些无关紧要的临时数据。对于需要长期保存的“永恒数据”,我们绝不能依赖它。
为了解决数据持久化的问题,Docker 隆重推出了数据卷(Volume)。数据卷就像我们前面提到的那个可以随身携带的“保险箱”,它是一个独立于任何容器的存在。 你可以随时创建、查看、删除数据卷,而这一切操作都不会影响到任何容器。反之,删除一个正在使用数据卷的容器,数据卷本身和它里面的数据也依然会安然无恙。
当我们让一个容器使用数据卷时,Docker 会像变魔术一样,将这个“保险箱”挂载到容器内部的一个指定目录上。之后,应用程序向这个目录里写入的任何数据,实际上都被直接存入了数据卷中,从而安全地“活”在容器之外。 下面的图清晰地展示了它们之间的关系:
上图中,容器内的 /app/data 目录就是一个挂载点。所有进出这个目录的数据,都会被直接导向 Docker 主机上由数据卷管理的物理位置,从而实现了数据与容器生命周期的解耦。
为了方便我们对数据卷进行管理,Docker 提供了一整套专门的命令,统称为 docker volume。
数据卷在 Docker 生态系统中被视为一等公民,意味着它们享有特殊的地位和优先级。
现在,我们来实际操作一下,看看如何创建一个数据卷。假设我们有一个应用程序需要一个专用的数据存储空间,我
们可以通过以下命令来创建一个名为 my-app-data 的数据卷。这个数据卷将为我们的应用程序提供一个独立且持久的存储空间,确保数据的安全性和持久性。
|$ docker volume create my-app-data my-app-data
如果你需要查看当前主机上所有的数据卷:
|$ docker volume ls DRIVER VOLUME NAME local my-app-data
要看某个数据卷的“详细资料”,比如它在主机上的实际存储位置,可以使用 inspect 命令:
|$ docker volume inspect my-app-data [ { "CreatedAt": "2023-10-27T10:00:00Z", "Driver": "local", "Labels": {}, "Mountpoint": "/var/lib/docker/volumes/my-app-data/_data", "Name": "my-app-data", "Options": null, "Scope": "local" } ]
Mountpoint 字段揭示了它的“真身”所在。
删除一个数据卷也很简单:
|$ docker volume rm my-app-data
当然,如果一个数据卷正在被某个容器使用,Docker 是不允许你删除它的,这是一种安全保护机制。
现在,让我们把数据卷挂载到容器里。我们使用 --mount 参数来完成这个操作。
首先,我们启动一个 Alpine 容器,并告诉 Docker:“请为我创建一个名为 bizvol 的数据卷,并将它挂载到容器的 /data 目录下。”
|$ docker run -it --name my-container --mount source=bizvol,target=/data alpine
这里有一个非常方便的特性:如果 source 指定的数据卷 bizvol 不存在,Docker 会非常智能地自动为我们创建它。你无需预先手动创建。
现在我们进入了容器的交互式终端。让我们在 /data 目录里写点东西。
|/# echo "这是需要永久保存的重要信息" > /data/secret.txt /# cat /data/secret.txt 这是需要永久保存的重要信息
很好,文件已经写入。现在,让我们输入 exit 退出并删除这个容器。
|$ docker rm -f my-container
容器消失了,但我们的数据卷 bizvol 和里面的数据还在吗?当然在!我们可以启动一个全新的容器,并挂载同一个数据卷来验证一下。
|$ docker run --rm --mount source=bizvol,target=/app/storage alpine cat /app/storage/secret.txt 这是需要永久保存的重要信息
我们发现新容器成功读取到了旧容器留下的数据。这就是数据卷的力量:数据永存,容器常新。
默认情况下,我们创建的数据卷都由 local 驱动管理,意味着它们只能被创建它们的那台 Docker 主机上的容器访问。这就像一个只能在本地使用的移动硬盘。
但如果我们的应用跑在一个由多台服务器组成的集群上呢?Docker 通过卷驱动(Volume Drivers) 插件机制解决了这个问题。你可以把它想象成给 Docker 安装一个“网盘客户端”,比如 NFS、AWS EBS 或其他云存储服务的驱动。
安装了相应的驱动后,你就可以创建能够被集群中任何一台主机上的容器访问的共享数据卷。这为构建高可用的有状态服务提供了强大的基础。
注意数据安全!
当多个容器同时向同一个共享数据卷写入数据时,可能会发生意想不到的后果,这被称为“数据竞争”或“数据损坏”。例如,容器A正在写入一个文件,还没写完,容器B也开始写入同一个文件,结果可能导致文件内容错乱。解决这个问题通常需要在应用程序层面设计合理的读写锁机制,确保同一时间只有一个应用在修改数据。
本部分我们学习了 Docker 处理数据的两种方式。对于那些无需长期保留的数据,容器自带的临时存储层足以应对。 而对于任何你珍视的、需要跨越容器生命周期的数据,数据卷都是不二之选。 它将数据与容器的短暂生命解耦,为我们在动态的容器世界里构建稳定、可靠的有状态应用铺平了道路。