1.1 kubernetes 存储简介
容器中的存储都是临时的,当 Pod 重启以后,容器中的数据会丢失。而有些容器是要有状态的、或是数据有持久化的需求。
根据服务是否有状态,分为 3 种类型:
- 无状态应用:kubernetes 使用 ReplicaSet 保证应用的实例数量,如果某个 Pod 挂掉会立刻使用模板新创建一个 Pod 替代挂掉的 Pod,新创建的 Pod 和挂掉的 Pod 一模一样,这样的应用对数据存储没有要求。
- 普通有状态应用:确保 Pod 重启以后能够读取到之前的状态数据。这种类型的应用要求实现数据的持久化和共享,所以要实现容器外的存储方案。
- 有状态集群应用:在普通有状态应用的基础上(即:状态保存)还要实现集群管理。主要是 StatefulSet。
有状态应用的状态需要在 Pod 销毁和重建之间保持,那么这些状态需要在应用运行过程中持久化到存储里。Kubernetes 里持久化存储通过 Volume 抽象来实现,Volume 底层实现可以是云存储或本地存储。Kubernetes 里使用 PV(PersistentVolume)来表示存储资源,通过 PVC(PersistentVolumeClaim)来声明存储资源需求。PV/PVC 的关系类似于 Node/Pod,只不过一个描述的是存储资源,另一个描述的是计算资源。有状态应用的 Pod 需要绑定 PV,在 Pod 销毁和重建之间要保证绑定到之前的 PV,另外很多时候 Pod 的启动顺序有要求。
为了屏蔽底层存储的实现细节,更加方便地使用存储,kubernetes 从 1.0 版本引入 PersistentVolume 和 PersistentVolumeClaim 两个资源对象。然后从 1.4 版本开始引入 StorageClass 资源对象,主要用于标记存储资源特性和性能。到 1.6 版本完善了 StorageClass 和动态资源供应机制,实现了存储卷按需供应的功能。
kubernetes 存储部分有 4 个核心概念:
Volume
Volume 是最基本的存储抽象,支持多种类型,比如:本地存储卷 local、临时空目录 emptyDir、云服务等。Volume 可以直接被 Pod 使用,也可以被 PV 使用。
Volume 的生命周期和 Pod 绑定,如果 Pod 挂掉以后会由 kubelet 再次重启 Pod,存储在 Volume 中的数据依然还在。只有当 Pod 删除时,Volume 才会被清理,数据是否丢失取决于具体的卷类型,如果是 emptyDir 那么数据就会丢失,如果是 PV 的话数据就不会丢失。
直接使用 Volume 与 Pod 绑定,需要在声明 Pod 时定义各种存储细节,所以这样存在一定的耦合度。如果想要使用第三方存储,则需要先创建好对应的 Volume 然后再与 Pod 进行绑定,删除 Pod 时也要手动删除 Volume 资源。
后面介绍的三个概念可以帮助我们更加轻松自动化地创建 Volume。
PersistentVolume(缩写为:PV)
PersistentVolume 是 kubernetes 集群中的一个共享网络存储资源对象,是对底层共享网络存储的抽象,PV 由管理员创建和配置,它与共享存储的具体实现直接相关,通过插件式的机制完成与共享存储的对接,然后应用通过 PVC 来请求访问和使用。
PV 有独立于 Pod 的生命周期,是由 PV Controller 控制 PV/PVC 的生命周期。
PersistentVolumeClaim(缩写为:PVC)
PVC 是用户对存储资源 PV 的申请,可以申请特定的存储空间和访问模式。根据 PVC 中指定的条件 kubernetes 会动态寻找系统中的 PV 资源并进行绑定。
如果从 Storage Admin 和用户的角度看 PV 和 PVC:
- Storage Admin 负责创建和维护 PV。
- 用户只需要使用 PVC 声明存储大小和进入模式即可。
PV 和 PVC 匹配可以通过 StorageClassName、matchLabels 或是 matchExpressions 三种方式。
StorageClass
将存储资源定义为某种类别(Class),比如:快速存储、慢速存储、有数据冗余、无数据冗余等,用户根据 StorageClass 的描述直观获取各种存储资源的特性,然后根据应用对存储资源的需求去申请存储资源。
使用 StorageClass 的好处在于可以动态创建 PV,节省了时间(不需要手动创建 Volume 或声明 PV),还可以封装不同类型的存储以供 PVC 选用。
因此,推荐的做法是声明 Pod 使用 PVC,而 PVC 使用 StorageClass。
关系图
这 4 个概念之间的关系如下图所示:

kubernetes 存储主要应用于以下 3 个方面:
- 应用的基本配置文件读取、密码或密钥的管理等。
- 应用的状态存储、数据存取等。
- 不同应用间共享数据。
本部分内容更加侧重于后面两个方面的讲解。
1.2 kubernetes 存储架构与原理
kubernetes 存储设计的主要原则为:
- 遵循 kubernetes 整体架构:声明式定义。
- 易用性,尽可能多兼容各种存储平台:相比较于 Docker Volume 而言,实现插件化,并兼容用户自定义插件。
- 安全性:数据安全性、以及卷生命周期。
kubernetes 对容器持久化存储做了一层自己的抽象,这一层抽象主要作用在于:
- 提供卷生命周期管理。
- 提供“声明”式定义,将使用者和提供者分离。
- 提供存储类型定义。
但是从本质上来说,是实现了一件事情--让存储在容器里 ready。核心为 3 件事情:
- attach/detach
- provision/delete
- mount/unmount。
下面是 kubernetes 存储架构图:

kubernetes 存储的主要包括 4 大组件:
- Attach/Detach Controller
- PV Controller
- Volume Manager
- Volume Plugin
Attach/Detach Controller
是一个逻辑组件。运行在 Master 节点上,主要做一些网络块存储设备(block device)的 attach/detach。比如:rbd、cinder 块设备需要先挂载到主机上然后再执行 mount 操作。
这是一个非必须的 Controller,主要是为了在 attach 卷上支持 plugin headless 形态,可以在 Controller Manager 中使用参数禁用它。
它的主要作用在于:当从 API Server 中获取到,有卷声明的 Pod 与 Node 间的关系发生变化时,决定是调用 Volume Plugin 将这个 Pod 关联的卷 attach 到对应 Node 主机上,还是直接将卷从 Node 上 Detach 掉。
PV Controller
是一个逻辑组件。运行在 Master 节点上,主要做 provision/delete。
监听 API Server 中关于卷管理的资源对象更新,监控和管理集群中的 PV/PVC/SC 资源对象的生命周期,执行创建、删除、绑定、回收等操作,实现 PV/PVC 绑定。
PV 和 PVC 的状态分别有:
- PV:Pending, Available, Bound, Released, Failed。
- PVC:Pending, Bound, Lost。
Volume Manager
是一个逻辑组件。运行在 kubelet 里让存储 Ready 的组件,主要做 mount/unmount(attach/detach 可选)。用于协调 Attach/Detach Controller、PV Controller 和 Volume Plugin,最终实现将块设备从创建到挂载到 kubernetes 系统上指定目录的过程。
当 Pod 调度到 Node 上,根据 Pod Manager(在 kubelet 里)中 pod spec 声明的存储触发卷挂载操作。具体为:kubelet 监听到调度到该 Node 节点上 Pod 声明,将 Pod 缓存到 Pod Manager 中,Volume Manager 根据 Pod Manager 获取到 PV/PVC 的状态,分析出具体的 attach/detach、mount/unmount 操作,然后调用 Volume Plugin 进行具体的业务处理。
Volume Plugin
是一个基础组件。存储提供的扩展接口,包含各类存储提供者的 plugin 实现。
实现自定义的插件可以通过 FlexVolume 或是 Container Storage Interface(CSI),更推荐使用后者。
经过上面的分析,也可以使用如下的图示来表示 kubernetes 存储架构:

kubernetes 挂载卷的基本过程
- 用户创建 Pod 包含一个 PVC。
- Pod 被分配到节点 NodeA。
- Kubelet 等待 Volume Manager 准备设备。
- PV Controller 调用相应 Volume Plugin(in-tree 或者 out-of-tree) 创建持久化卷并在系统中创建 PV 对象以及其与 PVC 的绑定 (Provision)。
- Attach/Detach Controller 或者 Volume Manager 通过 Volume Plugin 实现块设备挂载 (Attach)。
- Volume Manager 等待设备挂载完成,将卷挂载到节点指定目录 (mount):/var/lib/kubelet/plugins/kubernetes.io/aws-ebs/mounts/vol-xxxxxxxxxxxxxxxxx。
- Kubelet 在被告知设备准备好后启动 Pod 中的容器,利用 Docker –v 等参数将已经挂载到本地的卷映射到容器中 (volume mapping)。
1.3 存储卷(Volume)
Pod 里的容器是无状态的,一旦重启,之前保存在内存里的状态都将会丢失,一切回到初始状态。使用 Pod 运行有状态应用时,必须要使用外部的存储卷来把状态持久化保存起来,以便在 Pod 重新调度之后可以恢复到之前的状态。存储卷的生命周期长于 Pod,需要集群管理员手动创建和删除,而不像 Pod 那样可以被自动调度。另外存储卷也可用于同一个 Pod 里的多个容器之间共享文件。
一个 Pod 可以挂载多个存储卷,一个存储卷也可以被同时挂载到多个 Pod 里,这样 Pod 之间也可以共享文件。在部署配置文件里,通过属性 spec.volumes 和 spec.containers.volumeMounts 来分别指定存储卷以及它被挂载到容器里的路径。
Kubernetes 支持以下类型的存储卷:
临时存储(Temp)
- 用于存储临时数据的简单空目录:emptyDir。
本地化的暂存(Local Ephermeral)
- 用于将目录从工作节点的文件系统挂载到 Pod 中:hostPath。
- 通过检出 Git 仓库的内容来初始化的卷:gitRepo。
- 本地存储卷:local。
- 用于将 Kubernetes 部分资源和集群信息公开给 Pod 的特殊类型卷:configMap、secret、downwardAPI。
网络化的持久存储(Networked Persistent)
- 用于挂载云服务商提供的特定存储类型:gcePersistentDisk(谷歌高性能型存储磁盘卷)、awsElasticBlockStore(AmazonWeb 服务弹性块存储卷)、azureDisk(Microsoft Azure 磁盘卷)。
- 用于挂载其他类型的网络存储:cephfs、iscsi、flocker、glusterfs、quobyte、rbd、vsphereVolume、scaleIO。
自定义存储卷(Others)
Kubernetes 是开放的,如果没有合适的,可以自己实现。
- 可以自己开发驱动程序(卷插件)在节点上挂载卷:FlexVolume(不过这是一个 alpha 特性,将来可能会改变)。
- 容器存储接口,在 kubernetes 和外部存储系统之间建立一套标准的存储管理接口提供存储服务:Container Storage Interface(CSI)。
1.4 临时存储(emptyDir)
如果 Pod 挂载了 emptyDir 类型的卷,当 Pod 分配到 Node 上时,会创建 emptyDir。只要 Pod 运行在 Node 节点上,emptyDir 就会一直存在,容器挂掉不会使 emptyDir 丢失数据,如果 Pod 不在该 Node 节点上了(如:Pod 被删除或是发生迁移),emptyDir 就会被删除。
emptyDir 可以在如下几种场景中使用:
- 临时空间,比如基于磁盘的合并排序。
- 遇到崩溃事件可以设置检查点作为临时存储恢复未执行完毕的长计算。
- 保存临时文件。
新建 redis.yaml 文件,并向其中写入如下代码:
apiVersion: v1
kind: Pod
metadata:
name: redis
spec:
containers:
- name: redis
image: redis
volumeMounts:
- name: redis-storage # 挂载到容器中的卷的名称为 redis-storage
mountPath: /data/redis # 挂载到容器中的目录为 /data/redis
volumes: # 定义存储卷
- name: redis-storage
emptyDir: {}
执行创建:
$ kubectl create -f redis.yaml
pod/redis created
查看创建出来的 pod 的详细信息:
$ kubectl describe pod redis
Name: redis
...
Volumes:
redis-storage:
Type: EmptyDir (a temporary directory that shares a pod's lifetime)
Medium:
SizeLimit: <unset>
default-token-k75dn:
Type: Secret (a volume populated by a Secret)
SecretName: default-token-k75dn
Optional: false
QoS Class: Burstable
进入容器查看实际的卷挂载路径:
$ kubectl exec -it redis bash
root@redis:/data# ls /data/redis
root@redis:/data#
默认情况下,emptyDir 可以使用由 Node 节点提供的任意类型的后端存储。如果需要使用 tmpfs 作为存储资源,只需要在定义时增加字段 emptyDir.medium=Memory 即可(需要注意:如果 Node 节点重启,emptyDir 中的数据依然会全部丢失)。
1.5 使用 Node 的文件系统(hostPath)
映射 Node 文件系统中的文件或目录到 Pod 中,可以通过 type 字段设置类型,比如:DirectoryOrCreate(如果目录不存在就创建)、Directory(目录)、FileOrCreate(如果文件不存在就创建)、File(文件)、Socket(UNIX 套接字)、CharDevice(字符设备)、BlockDevice(块设备)。可以将已有的文件或目录挂载到容器中,或新建文件、目录。
使用场景有:
- 当运行的容器需要访问 Docker 内部结构时,比如使用 hostPath 映射 /var/lib/docker 到容器。
- 当在容器中运行 cAdvisor 时,使用 hostPath 映射 /dev/cgroups 到容器。
需要注意的是:
- 如果使用 Deployment 中的 pod Template 创建副本时,即使使用相同的 Node 目录,可能目录内容也不同,因为不同 Node 节点上文件的内容可能不同。
- 在 Node 上创建的目录只有 root 用户有写权限,可以将程序运行在 privileged container 上,或修改 Node 上文件的权限。
新建 busybox.yaml 文件,并向其中写入如下内容:
apiVersion: v1
kind: Pod
metadata:
name: busybox
spec:
containers:
- image: busybox
name: busybox
command: ["sleep", "36000"]
volumeMounts:
- name: busybox-storage
mountPath: /data/busybox
volumes:
- name: busybox-storage
hostPath:
path: /tmp # 在 Node 节点上的目录
type: Directory # 可选字段,这里为目录
执行创建:
$ kubectl create -f busybox.yaml
pod/busybox created
$ kubectl get pod busybox -o wide
NAME READY STATUS RESTARTS AGE IP NODE NOMINATED NODE READINESS GATES
busybox 1/1 Running 0 6m 10.20.138.49 kubesphere02 <none> <none>
查看创建出来的 pod 的详细信息:
$ kubectl describe pod/busybox
...
Mounts:
/data/busybox from busybox-storage (rw)
/var/run/secrets/kubernetes.io/serviceaccount from default-token-k75dn (ro)
...
Volumes:
busybox-storage:
Type: HostPath (bare host directory volume)
Path: /tmp
HostPathType: Directory
default-token-k75dn:
Type: Secret (a volume populated by a Secret)
SecretName: default-token-k75dn
Optional: false
QoS Class: Burstable
现在进入 busybox 容器,并在 /data/busybox 目录下新建 test.log 测试文件,然后退出:
$ kubectl exec -it busybox sh
/ # echo 'user-test' > /data/busybox/test.log
/ # exit
进入 kubesphere02 节点查看文件是否存在:
[root@kubesphere02 ~]# cd /tmp/
[root@kubesphere02 tmp]# ls test.log
test.log
[root@kubesphere02 tmp]# cat test.log
user-test
现在将 busybox pod 删除掉:
$ kubectl delete pod busybox
pod "busybox" deleted
[root@kubesphere02 tmp]# cat test.log
user-test
当使用 hostPath 类型存储卷时,即使 Pod 被删除掉,卷中的数据依然是存在的。
1.6 本地存储卷(local)
顾名思义,本地存储卷提供的是本地存储资源。类似于 Docker 里在宿主机上创建的 Volume。由于本地存储卷是跟节点绑定的,无法在节点之间迁移,所以调度 Pod 的时候会依据 Pod 依赖的存储卷来找到对应的节点,然后把 Pod 调度到该节点上。
下面是使用本地存储卷的配置示例:
apiVersion: v1
kind: PersistentVolume
metadata:
name: example-pv
spec:
capacity:
storage: 100Gi
accessModes:
- ReadWriteOnce
persistentVolumeReclaimPolicy: Delete
storageClassName: local-storage
local:
path: /mnt/disks/ssd1
评论区