五、Docker 核心技术:容器数据持久化之数据卷
Docker 容器本身是无状态且生命周期短暂的。当一个容器被删除时,它在可写层产生的所有数据都会随之消失。这对于需要持久化存储数据的应用 (如数据库、日志系统、用户上传内容) 来说是不可接受的。为了解决这个问题,Docker 提供了多种数据持久化方案,其中最重要、最推荐的就是数据卷
一、什么是容器数据卷
数据卷是宿主机文件系统中一个特殊的目录,它由 Docker 管理 (/var/lib/docker/volumes/
目录下),并可以直接映射到一个或多个容器的指定目录下。
数据卷的核心优势:
数据持久化:数据卷的生命周期独立于任何容器。即使所有使用该数据卷的容器都被删除,数据卷及其中的数据依然存在。
数据共享:多个容器可以同时挂载同一个数据卷,从而实现容器间的数据共享和同步。
高性能:数据卷绕过了容器的联合文件系统 (UnionFS),直接读写宿主机的文件系统,具有接近原生的I/O性能。
易于管理:Docker 提供了专门的命令 (docker volume ...
) 来创建、查看、删除数据卷,便于备份、迁移和恢复。
二、数据卷的使用
在创建或运行容器时,我们主要使用 -v
或 --mount
标志来挂载数据卷。-v
语法更简洁,--mount
语法更明确,推荐在生产环境和复杂场景下使用 --mount
。
1. 匿名挂载
如果你在 -v
标志中只指定容器内的路径,Docker 会自动创建一个匿名的数据卷,并将其挂载到该路径。
语法:
-v /path/in/container
代码案例:
docker run -d -P --name nginx-anon -v /usr/share/nginx/html nginx
- 这个命令会创建一个新的、名字是随机哈希值的数据卷,并挂载到容器的
/usr/share/nginx/html
目录。 - 我们可以通过
docker inspect
查看这个匿名数据卷的具体信息。
docker inspect nginx-anon
在输出的 "Mounts"
部分,你会看到类似这样的信息:
"Mounts": [{"Type": "volume","Name": "a1b2c3d4...", // 随机生成的长哈希值"Source": "/var/lib/docker/volumes/a1b2c3d4.../_data","Destination": "/usr/share/nginx/html",...}
]
- 缺点:匿名挂载的数据卷名称不直观,难以管理和复用。
2.具名挂载
这是最推荐的数据卷使用方式。你可以为数据卷指定一个有意义的名称,方便后续的引用、共享和管理。
语法:
-v
方式:volume-name:/path/in/container
--mount
方式:type=volume,source=volume-name,target=/path/in/container
代码案例:
步骤一:创建具名数据卷 (可选,Docker会在挂载时自动创建)
docker volume create my-nginx-data
步骤二:使用具名数据卷运行容器
- 使用
-v
标志:
docker run -d -P --name nginx-named-v -v my-nginx-data:/usr/share/nginx/html nginx
- 使用
--mount
标志 (推荐):
docker run -d -P --name nginx-named-mount --mount type=volume,source=my-nginx-data,target=/usr/share/nginx/html nginx
- 在这两个例子中,名为
my-nginx-data
的数据卷被挂载到了容器的/usr/share/nginx/html
目录。 - 现在,你可以删除并重建
nginx-named-v
或nginx-named-mount
容器,但只要你重新挂载my-nginx-data
数据卷,网站的数据 (如index.html
) 就会保持不变。
数据卷管理命令:
# 列出所有数据卷
docker volume ls# 查看某个数据卷的详细信息
docker volume inspect my-nginx-data# 删除一个数据卷 (前提是没有容器正在使用它)
docker volume rm my-nginx-data# 删除所有不再被任何容器使用的悬空数据卷 (dangling volumes)
docker volume prune
3. 数据卷 vs 绑定挂载
除了由 Docker 管理的数据卷,Docker 还支持另一种强大的挂载方式——绑定挂载。它允许我们将宿主机上任意的一个文件或目录直接映射到容器中。
绑定挂载语法 (使用 -v
):
-v /path/on/host:/path/in/container
与数据卷的核心区别及选择:
特性 | 数据卷 | 绑定挂载 |
---|---|---|
管理方 | 由 Docker 管理,位于 Docker 的专用存储区域 (/var/lib/docker/volumes/ )。 | 由用户管理,可以是宿主机文件系统中的任意路径。 |
可移植性 | 高。数据卷的定义与宿主机的目录结构无关,便于在不同环境中迁移。 | 低。依赖于宿主机上特定的目录结构,不易迁移。 |
性能 | 在 Linux 上通常性能更高,因为它为数据I/O进行了优化。 | 性能也很好,但可能受宿主机文件系统权限等因素影响。 |
权限 | Docker 自动处理权限。 | 可能存在宿主机与容器内用户权限不匹配的问题。 |
适用场景 | 推荐用于生产环境和所有需要持久化应用数据的场景,如数据库文件、应用日志等。 | 适用于开发环境,如将源代码目录挂载到容器中进行实时代码调试;或共享宿主机的配置文件到容器。 |
代码示例:直观感受数据卷与绑定挂载的行为差异
这个示例将清晰地展示数据卷独立于容器生命周期的特性,以及绑定挂载下宿主机与容器的实时同步。
场景一:使用数据卷的持久性演示
- 运行一个容器,使用具名数据卷并写入数据:
docker run -d --name vol-test-container -v my-persistent-data:/data ubuntu sleep infinity
docker exec vol-test-container sh -c "echo 'This data is in a volume' > /data/message.txt"
- 此时,名为
my-persistent-data
的数据卷中已经包含了message.txt
文件。
- 在容器内删除文件,验证宿主机数据卷不受影响:(为模拟容器内误操作)
docker exec vol-test-container rm /data/message.txt
验证容器内文件已删除
docker exec vol-test-container ls /data
(此时应无输出)
关键点:此时
my-persistent-data
这个数据卷本身在宿主机上仍然包含message.txt
文件。容器的删除操作仅仅是在容器的可写层记录了“该文件已删除”的标记,并未真正删除数据卷中的源文件。
- 删除容器,然后创建一个新容器挂载同一个数据卷:
docker stop vol-test-container
docker rm vol-test-container# 创建一个全新的容器,挂载之前的数据卷
docker run --name vol-test-checker -it -v my-persistent-data:/data ubuntu
- 在新容器中查看数据:
当你进入vol-test-checker
容器的交互式终端后,查看/data
目录:
# 在 vol-test-checker 容器的shell中执行
ls /data
# 输出应为:message.txtcat /data/message.txt
# 输出应为:This data is in a volume
结论:这个实验有力地证明了,数据卷中的数据是独立且持久的。即使容器内的文件被看似“删除”,或者整个容器被删除,数据卷中的原始数据也安然无恙,可以被新的容器重新挂载和使用。
场景二:使用绑定挂载的实时同步演示
- 在宿主机上创建一个目录和文件:
mkdir -p ./host-data
echo "Initial data from host" > ./host-data/sync.txt
- 运行一个容器,将宿主机目录绑定挂载到容器中:
docker run -d --name bind-test-container -v $(pwd)/host-data:/data ubuntu tail -f /dev/null
tail -f /dev/null
是一个让容器保持运行的技巧。- 现在,宿主机的
./host-data
目录与容器的/data
目录实时同步。
3. 验证容器内可以看到宿主机文件:
docker exec bind-test-container cat /data/sync.txt
# 输出应为:Initial data from host
4. 在宿主机上修改文件内容:
echo "Host updated the file" >> ./host-data/sync.txt
- 在容器内立即查看变化:
docker exec bind-test-container cat /data/sync.txt
# 输出现在应包含两行:
# Initial data from host
# Host updated the file
- 在容器内删除文件:
docker exec bind-test-container rm /data/sync.txt
- 在宿主机上验证文件是否也被删除:
ls ./host-data/
# (此时应无输出,文件已被删除)
结论:绑定挂载建立了宿主机和容器之间文件系统的直接链接。任何一方对挂载目录中内容的修改或删除,都会立即、真实地反映在另一方。这种实时同步的特性使其非常适合开发时共享源代码。
4. 综合案例:使用具名数据卷持久化 MySQL 数据
这个案例将演示如何创建一个 MySQL 容器,并将其数据目录 /var/lib/mysql
持久化到一个具名数据卷中,从而实现数据库数据的安全存储。
步骤一:创建具名数据卷
为了清晰管理,我们先创建一个名为 mysql-data
的数据卷。
docker volume create mysql-data
步骤二:运行 MySQL 容器并挂载数据卷
我们将运行一个 MySQL 8.0 容器,并设置 root 密码。
docker run -d \
--name my-mysql \
-p 3306:3306 \
-e MYSQL_ROOT_PASSWORD=123456 \
--mount type=volume,source=mysql-data,target=/var/lib/mysql \
mysql:8.0
-d
: 后台运行容器。--name my-mysql
: 为容器命名。-p 3306:3306
: 将宿主机的 3306 端口映射到容器的 3306 端口。-e MYSQL_ROOT_PASSWORD=...
: 通过环境变量设置 MySQL 的 root 用户密码。--mount ...
: 核心部分。将我们创建的mysql-data
数据卷挂载到容器内部存放数据库文件的标准路径/var/lib/mysql
。
步骤三:验证数据持久化
- 进入容器并创建数据
使用docker exec
进入正在运行的 MySQL 容器,并登录到数据库。
docker exec -it my-mysql mysql -uroot -pmysecretpassword
在 MySQL 命令行中,创建一个新的数据库和表,并插入一些数据。
CREATE DATABASE testdb;
USE testdb;
CREATE TABLE users (id INT PRIMARY KEY, name VARCHAR(50));
INSERT INTO users VALUES (1, 'Alice'), (2, 'Bob');
EXIT;
主机也能正常连接上
- 删除容器
现在,我们模拟一次容器故障或升级,删除这个 MySQL 容器。
docker stop my-mysql
docker rm my-mysql
此时,容器已经不存在了。
- 重新创建容器,挂载同一个数据卷
我们再次运行一个 MySQL 容器,使用相同的命令,确保它挂载的还是mysql-data
这个数据卷。
docker run -d \
--name my-mysql-new \
-p 3306:3306 \
-e MYSQL_ROOT_PASSWORD=mysecretpassword \
--mount type=volume,source=mysql-data,target=/var/lib/mysql \
mysql:5.7
- 验证数据是否恢复
等待新容器my-mysql-new
完全启动后,再次进入这个新容器。
docker exec -it my-mysql-new mysql -uroot -pmysecretpassword
在 MySQL 命令行中,检查我们之前创建的数据库和数据是否存在。
USE testdb;
SELECT * FROM users;
你会看到输出:
+----+-------+
| id | name |
+----+-------+
| 1 | Alice |
| 2 | Bob |
+----+-------+
这证明了,即使容器被删除,存储在数据卷中的数据也被完美地保留了下来,并在新容器中得以恢复。
总结: 容器数据卷是实现Docker数据持久化的首选方案。通过使用具名数据卷,我们可以安全地存储应用数据,解耦数据与容器的生命周期,并轻松实现数据的共享、备份和恢复,为在生产环境中运行有状态应用提供了坚实的基础。
练习题
题目一:创建与查看数据卷
写出一条命令,创建一个名为 app-config
的具名数据卷,然后写出另一条命令来查看这个数据卷的详细信息。
题目二:匿名挂载
写出一条命令,以后台模式运行一个 ubuntu
容器,并为容器内的 /data
目录进行匿名挂载。
题目三:具名挂载 (使用 -v
)
写出一条命令,以后台模式运行一个名为 my-redis
的 redis
容器,并使用 -v
标志将一个名为 redis-data
的具名数据卷挂载到容器的 /data
目录。
题目四:具名挂载 (使用 --mount
)
使用 --mount
标志重写上一题的命令,实现完全相同的效果。
题目五:绑定挂载
写出一条命令,运行一个临时的、交互式的 alpine
容器,并将宿主机当前目录下的 app
子目录 (假设为 ./app
) 绑定挂载到容器的 /app
目录。容器启动后执行 ls /app
命令。
题目六:数据共享
- 首先,运行一个名为
writer-container
的busybox
容器,将一个名为shared-volume
的数据卷挂载到/shared
。容器启动后,向/shared/message.txt
文件写入 “Hello from writer”。 - 然后,运行另一个名为
reader-container
的busybox
容器,同样挂载shared-volume
数据卷到/shared
,并读取/shared/message.txt
文件的内容。
(请分别写出这两个docker run
命令)
题目七:数据卷清理
写出一条命令,可以一次性删除所有当前未被任何容器使用的Docker数据卷。
题目八:数据卷数据备份
假设 mysql-data
数据卷中包含了重要的数据库文件,你希望对其进行备份。请描述一种简单的、利用另一个临时容器来备份该数据卷中所有文件到宿主机 /backup
目录的思路或命令。
答案与解析
答案一:
创建数据卷:
docker volume create app-config
查看详细信息:
docker volume inspect app-config
解析:
docker volume create
用于创建具名数据卷,docker volume inspect
用于查看其元数据,包括在宿主机上的实际存储路径。
答案二:
docker run -d --name ubuntu-anon -v /data ubuntu
解析:
-v
标志后只跟了容器内的路径/data
,这会触发Docker创建一个匿名数据卷并挂载到此路径。
答案三:
docker run -d --name my-redis -v redis-data:/data redis
解析:
-v
标志使用[volume_name]:[container_path]
的格式来进行具名挂载。如果redis-data
数据卷不存在,Docker会自动创建它。
答案四:
docker run -d --name my-redis --mount type=volume,source=redis-data,target=/data redis
解析:
--mount
标志使用更明确的键值对语法。type=volume
指定类型,source
指定数据卷名称,target
指定容器内路径。
答案五:
docker run --rm -it --mount type=bind,source=$(pwd)/app,target=/app alpine ls /app
解析:
--mount type=bind
指定了绑定挂载。source=$(pwd)/app
表示宿主机当前工作目录下的app
目录。--rm
使容器退出后自动删除,-it
提供交互式终端。容器启动后直接执行ls /app
命令。
答案六:
- 运行
writer-container
:
docker run --name writer-container -v shared-volume:/shared busybox sh -c "echo 'Hello from writer' > /shared/message.txt"
- 运行
reader-container
:
docker run --name reader-container -v shared-volume:/shared busybox cat /shared/message.txt
解析: 两个容器都挂载了同一个具名数据卷
shared-volume
。第一个容器向卷中写入文件,第二个容器可以立即读取到这个文件,实现了数据共享。
答案七:
docker volume prune
解析:
docker volume prune
是一个方便的命令,用于清理不再被任何(包括已停止的)容器引用的数据卷,可以释放磁盘空间。
答案八:
docker run --rm \
--mount type=volume,source=mysql-data,target=/dbdata,readonly \
--mount type=bind,source=/backup,target=/backup_host \
ubuntu \
tar czvf /backup_host/mysql-backup-$(date +%Y%m%d).tar.gz -C /dbdata .
日期:2025年9月5日
专栏:Docker教程