容器的文件系统是临时的——容器删除后,可写层中的所有数据都会丢失。如果你在容器中写入数据库文件、日志、用户上传内容,容器重启后这些数据就消失了。
Volume 是容器数据持久化的标准方案。它将宿主机的目录或文件挂载到容器的指定路径,绕过 OverlayFS 的可写层,直接读写宿主文件系统。Kubernetes 进一步通过 PV/PVC/StorageClass 体系实现了存储的动态供给和管理。
本章将深入容器存储的完整方案,从 Docker Volume 到 Kubernetes CSI。
容器存储的演进是一条从”本地挂载”到”动态供给”的路径。Docker 早期使用 device mapper 作为存储驱动,性能差且不稳定;后来转向 AUFS,但 AUFS 不在内核主线中;最终 overlay2 成为默认选择。Volume 最初只是简单的”绑定挂载”(-v /host/path:/container/path),Kubernetes 引入 PV/PVC/StorageClass 体系后,存储管理实现了质的飞跃——用户声明”我需要 10GB 存储”,StorageClass 自动从云厂商供给。2018 年,CSI(Container Storage Interface)标准化了存储插件接口,AWS EBS、Ceph、NFS 等存储后端可以通过统一的 CSI 插件接入 Kubernetes。容器存储从”手动挂载”到”声明式供给”,每一步演进都让运维更简单、应用更可移植。
前置知识
- Ch04 OverlayFS:容器文件系统:OverlayFS 是容器存储的核心——镜像层复用和可写层都依赖 OverlayFS
- Ch02 Linux Namespace 深入:Mount Namespace 让每个容器有独立的挂载视图
- Kubernetes 基础:PV/PVC/StorageClass 概念(Kubernetes 存储部分需要)
本章的 Kubernetes 存储部分(PV/PVC/CSI)与 Ch16 容器在 Kubernetes 中 有交叉,建议结合阅读。
一、容器存储的问题
1.1 可写层的局限
# 容器可写层的问题docker run -d --name db mysql# 在容器中写入数据...docker exec db mysql -e "CREATE DATABASE myapp;"
# 删除容器docker rm -f db
# 重新创建容器docker run -d --name db mysqldocker exec db mysql -e "SHOW DATABASES;"# myapp 数据库不见了!因为可写层随容器删除1.2 容器存储的层次
二、Docker Volume
2.1 三种挂载类型
| 挂载类型 | 数据持久性 | 写入性能 | 典型用途 |
|---|---|---|---|
| Volume (bind mount) | 宿主机目录 | 原生 | 开发环境、配置文件 |
| Volume (docker volume) | Docker 管理 | 原生 | 数据库持久化、共享存储 |
| tmpfs | 内存(不持久) | 最快 | 敏感数据、临时文件 |
| 类型 | 语法 | 说明 |
|---|---|---|
| 绑定挂载 | -v /host/path:/container/path | 挂载宿主机指定目录 |
| 命名卷 | -v myvol:/container/path | Docker 管理的持久卷 |
| 匿名卷 | -v /container/path | Docker 自动创建的临时卷 |
| tmpfs | --tmpfs /container/path | 内存文件系统 |
# 绑定挂载docker run -v /home/user/data:/app/data nginx
# 命名卷docker volume create mydatadocker run -v mydata:/app/data nginx
# 匿名卷docker run -v /app/data nginx
# tmpfs 挂载docker run --tmpfs /app/cache nginx
# 只读挂载docker run -v /home/user/config:/app/config:ro nginx2.2 Volume 的底层实现
Volume 的底层是 Linux 的 bind mount——将一个目录挂载到另一个路径:
# Docker Volume 的底层操作# 1. 创建卷目录mkdir -p /var/lib/docker/volumes/mydata/_data
# 2. 绑定挂载到容器的 Mount Namespacemount --bind /var/lib/docker/volumes/mydata/_data /var/lib/docker/overlay2/.../merged/app/data
# 3. 容器内看到的就是宿主机的目录docker exec mycontainer ls /app/data# 等同于 ls /var/lib/docker/volumes/mydata/_data2.3 Volume 的生命周期
2.4 Volume 驱动
Docker 支持 Volume 驱动插件,将数据存储到不同的后端:
# 查看已安装的 Volume 驱动docker volume ls
# 使用特定驱动创建卷docker volume create --driver local mydatadocker volume create --driver local --opt type=nfs --opt o=addr=192.168.1.1,rw --opt device=:/export/data nfs-vol
# 常见 Volume 驱动# local: 本地文件系统(默认)# nfs: NFS 网络存储# rexray: 多云存储# convoy: 分布式存储三、存储驱动
3.1 存储驱动的职责
存储驱动管理容器的文件系统层——如何叠加、如何写时复制、如何管理 whiteout:
| 存储驱动 | 技术 | 适用场景 |
|---|---|---|
| overlay2 | OverlayFS | 默认推荐 |
| fuse-overlayfs | FUSE OverlayFS | Rootless 容器 |
| devicemapper | Device Mapper | 旧系统 |
| btrfs | Btrfs 子卷 | Btrfs 文件系统 |
| zfs | ZFS 数据集 | ZFS 文件系统 |
| vfs | 目录复制 | 调试 |
3.2 存储驱动与 Volume 的关系
# 存储驱动管理的是容器的"层叠文件系统"# Volume 管理的是"持久化数据"
# 两者互不干扰:# - 存储驱动负责镜像层 + 可写层# - Volume 负责绕过可写层直接读写宿主文件系统
# 查看存储驱动docker info | grep "Storage Driver"# Storage Driver: overlay23.3 存储驱动性能对比
| 操作 | overlay2 | devicemapper | btrfs | zfs |
|---|---|---|---|---|
| 创建容器 | 快 | 慢 | 中 | 中 |
| 删除容器 | 快 | 慢 | 中 | 中 |
| 读取文件 | 快 | 中 | 快 | 快 |
| 写入新文件 | 快 | 中 | 快 | 快 |
| 修改已有文件 | 慢(copy-up) | 中 | 快 | 快 |
| 磁盘空间效率 | 高 | 中 | 高 | 高 |
修改大文件时,overlay2 的 copy-up 机制会复制整个文件。如果你的应用频繁修改大文件,应该使用 Volume 而非容器的可写层。
四、Kubernetes 存储
4.1 PV/PVC/StorageClass
Kubernetes 的存储体系由三个核心概念组成:
| 概念 | 角色 | 生命周期 |
|---|---|---|
| PV (PersistentVolume) | 存储资源 | 集群级别 |
| PVC (PersistentVolumeClaim) | 存储请求 | 命名空间级别 |
| StorageClass | 存储类型 | 集群级别 |
4.2 PVC 示例
# StorageClassapiVersion: storage.k8s.io/v1kind: StorageClassmetadata: name: fast-ssdprovisioner: kubernetes.io/aws-ebsparameters: type: gp3 fsType: ext4reclaimPolicy: DeletevolumeBindingMode: WaitForFirstConsumer
---
apiVersion: v1kind: PersistentVolumeClaimmetadata: name: myapp-dataspec: accessModes: [ReadWriteOnce] storageClassName: fast-ssd resources: requests: storage: 10Gi
---
apiVersion: v1kind: Podspec: containers: - name: myapp volumeMounts: - name: data mountPath: /app/data volumes: - name: data persistentVolumeClaim: claimName: myapp-data4.3 访问模式
| 访问模式 | 缩写 | 说明 |
|---|---|---|
| ReadWriteOnce | RWO | 单节点读写 |
| ReadOnlyMany | ROX | 多节点只读 |
| ReadWriteMany | RWX | 多节点读写 |
| ReadWriteOncePod | RWOP | 单 Pod 读写(Kubernetes 1.27+) |
五、CSI:容器存储接口
5.1 CSI 原理
CSI(Container Storage Interface)是 Kubernetes 的存储插件标准,类似于 CNI 之于网络:
5.2 CSI Driver 接口
| 接口 | 功能 | 调用者 |
|---|---|---|
| CreateVolume | 创建存储卷 | Controller |
| DeleteVolume | 删除存储卷 | Controller |
| ControllerPublishVolume | 将卷附加到节点 | Controller |
| ControllerUnpublishVolume | 将卷从节点分离 | Controller |
| NodeStageVolume | 将卷挂载到暂存路径 | Node |
| NodeUnstageVolume | 卸载暂存路径 | Node |
| NodePublishVolume | 将卷绑定到 Pod 路径 | Node |
| NodeUnpublishVolume | 解绑 Pod 路径 | Node |
5.3 CSI Driver 生命周期
CSI Driver 的每个接口在卷的完整生命周期中有明确的调用顺序:
5.4 常见 CSI Driver
| CSI Driver | 存储后端 | 特点 |
|---|---|---|
| aws-ebs-csi-driver | AWS EBS | AWS 块存储 |
| azure-disk-csi-driver | Azure Disk | Azure 块存储 |
| gce-pd-csi-driver | GCE PD | GCP 块存储 |
| ceph-csi | Ceph RBD/CephFS | 分布式存储 |
| local-path-provisioner | 本地磁盘 | 简单本地存储 |
| nfs-csi | NFS | 网络文件系统 |
| hostpath-csi | 宿主路径 | 开发测试 |
六、动手实践
6.1 Docker Volume 实验
#!/bin/bash# Docker Volume 实验
# 1. 创建命名卷docker volume create mydata
# 2. 查看卷详情docker volume inspect mydata# Mountpoint: /var/lib/docker/volumes/mydata/_data
# 3. 使用卷运行容器docker run -d --name writer -v mydata:/app/data alpine \ sh -c "echo 'Hello from writer' > /app/data/test.txt && sleep 3600"
# 4. 另一个容器读取同一卷docker run --rm -v mydata:/app/data alpine cat /app/data/test.txt# Hello from writer
# 5. 绑定挂载实验mkdir -p /tmp/host-dataecho "from host" > /tmp/host-data/file.txtdocker run --rm -v /tmp/host-data:/app/data alpine cat /app/data/file.txt# from host
# 6. 清理docker rm -f writerdocker volume rm mydata6.2 存储性能测试
#!/bin/bash# 对比 Volume vs 可写层的 IO 性能
echo "=== 可写层 IO 性能 ==="docker run --rm alpine sh -c " dd if=/dev/zero of=/tmp/test bs=1M count=100 2>&1 | tail -1 sync dd if=/tmp/test of=/dev/null bs=1M 2>&1 | tail -1"
echo ""echo "=== Volume IO 性能 ==="docker run --rm -v /tmp/vol-test:/data alpine sh -c " dd if=/dev/zero of=/data/test bs=1M count=100 2>&1 | tail -1 sync dd if=/data/test of=/dev/null bs=1M 2>&1 | tail -1"
# Volume 通常比可写层快 2-5x,因为绕过了 OverlayFS 的 copy-up七、容器存储性能优化
7.1 存储性能瓶颈定位
# 1. 测试容器可写层的 IO 性能docker run --rm alpine sh -c " dd if=/dev/zero of=/tmp/test bs=4k count=10000 oflag=dsync 2>&1 | tail -1"# 典型结果: ~50 MB/s (OverlayFS copy-up 开销)
# 2. 测试 Volume 的 IO 性能docker run --rm -v /tmp/vol-test:/data alpine sh -c " dd if=/dev/zero of=/data/test bs=4k count=10000 oflag=dsync 2>&1 | tail -1"# 典型结果: ~200 MB/s (直接写宿主文件系统)
# 3. 测试宿主机原生 IO 性能dd if=/dev/zero of=/tmp/host-test bs=4k count=10000 oflag=dsync 2>&1 | tail -1# 典型结果: ~250 MB/s7.2 存储性能优化策略
| 策略 | 说明 | 性能提升 |
|---|---|---|
| 使用 Volume | 绕过 OverlayFS copy-up | 2-5x |
| 使用 tmpfs | 内存文件系统 | 10-50x |
| 减少层数 | 合并 RUN 指令 | 减少查找开销 |
| 使用大页 | hugetlbfs | 减少页表开销 |
| 使用 IO 调度器 | none/mq-deadline | 减少延迟 |
7.3 Kubernetes 存储性能调优
# 使用本地存储获得最佳性能apiVersion: v1kind: PersistentVolumemetadata: name: local-fast-storagespec: capacity: storage: 100Gi accessModes: [ReadWriteOnce] persistentVolumeReclaimPolicy: Delete storageClassName: local-storage local: path: /mnt/fast-ssd nodeAffinity: required: nodeSelectorTerms: - matchExpressions: - key: kubernetes.io/hostname operator: In values: [node-1]7.4 CSI Driver 性能对比
| CSI Driver | 类型 | IOPS | 延迟 | 适用场景 |
|---|---|---|---|---|
| aws-ebs gp3 | 块存储 | 16,000 | <1ms | 通用 |
| aws-ebs io2 | 块存储 | 64,000 | <0.5ms | 数据库 |
| local-path | 本地磁盘 | 100,000+ | <0.1ms | 高性能 |
| ceph-rbd | 分布式块 | 10,000 | 1-5ms | 共享存储 |
| nfs | 网络文件 | 5,000 | 5-20ms | 文件共享 |
八、本章小结
上一章剖析了容器网络原理。
| 概念 | 职责 | 适用场景 |
|---|---|---|
| Docker Volume | 容器数据持久化 | 单机容器 |
| 存储驱动 | 文件系统层叠管理 | 容器运行时 |
| PV/PVC | 存储资源管理 | Kubernetes |
| StorageClass | 存储动态供给 | Kubernetes |
| CSI | 存储插件接口 | Kubernetes |
容器存储的核心原则是:数据不应存储在容器的可写层。无论是 Docker Volume 还是 Kubernetes PVC,都是将数据存储在容器生命周期之外。理解 Volume 的 bind mount 原理和 CSI 的插件机制,是构建可靠容器存储方案的基础。
支持与分享
如果这篇文章对你有帮助,欢迎支持作者或分享给更多人
部分信息可能已经过时






