k8s中的Volume体系与存储架构
一、容器中使用Volume
1、作为文件系统挂载
容器中通过volumeMounts字段使用Volume作为文件系统挂载:
(1)name字段指定使用哪个Volume;
(2)mountPath字段指定Volume在容器中的挂载路径;
(3)readOnly字段指定挂载的Volume是否只读;
(4)多个容器共享Volume时,可以隔离不同容器在Volume上数据存储的路径
subPath直接指定子目录的名字,
subPathExpr则指定${XXX},通过环境变量获取子目录的名字
这两个字段都默认为空,不能同时配置
(5)通过mountPropagation配置挂载传播
None(默认):此Volume挂载不会接收到任何后续挂载到该Volume(或子目录)下的挂载
HostToContainer:此Volume挂载将会接收到任何后续挂载到该Volume(或子目录)下的挂载
Bidirectional:类似HostToContainer,但容器创建的Volume挂载都将传播回主机和所有使用相同Volume的容器
三种挂载传播机制分别对应了Linux内核mount namespace的private、rslave、rshared传播机制
2、作为块设备挂载
容器中通过volumeDevices字段使用Volume作为块设备挂载
(1)name字段指定使用哪个Volume
(2)devicePath指定volume在容器中的挂载路径
二、Pod中声明Volume
Pod在spec.volumes中声明挂载了哪些Volume,VolumeSource大体分为以下几类:
1、本地存储
(1)emptyDir:在Pod创建过程中创建的一个临时目录,这个目录里的数据随着Pod删除也会被清除
存储介质包括内存(基于RAM的temfs)、磁盘等、巨页三种
目前也支持设置SizeLimit
存储介质为内存的Volume会同时受SizeLimit和Pod内所有容器总内存limit的限制,都没设的话就会限制为主机内存的一半
(2)hostPath:宿主机上的一个路径,在Pod删除之后还存在。路径如果是软链接的话会自动转向实际路径。
种类包括Directory、File、Socket、CharDevice、BlockDevice五种
考虑到Directory、File在主机上可能不存在,可以设置为DirectoryOrCreate、FileOrCreate
2、网络存储
如NFS、CephFS、RBD、 ISCSI等
3、云厂商提供的存储
例如在vSphere平台上部署Kubernetes时,可以使用vSphereVolume作为后端存储
在GCE可使用GCEPersistentDisk
在AWS上可以使用AWSElasticBlockStore
在Azure可以使用AzureFile
......
4、配置文件
(1)DownwardAPI可以将Pod API对象的部分字段以临时文件的形式注入到容器中
(2)可以将ConfigMap/Secret以持久化Volume的形式挂载到容器中,让容器中的程序可以通过POSIX接口来访问配置数据;也支持仅将它们的一部分以临时文件的形式注入到容器中
(3)Projected可以将配置信息的一部分,以临时文件的形式注入到容器中
包括Configmap、Secret、DownwardAPI、ServiceAccount Token四种
例如:将Pod的metadata.labels中的内容,以labels这个文件名挂载到mountPath下
volumes:
- name: podinfo
projected:
sources:
- downwardAPI:
items:
- path: "labels"
fieldRef:
fieldPath: metadata.labels
5、CSI和FlexVolume
CSIVolumeSource的数据结构:
- Driver
根据Driver name指定由哪个CSI Plugin来处理该Volume
- ReadyOnly
- FsType
希望挂载的文件系统种类,如ext4、xfs等
- VolumeAttribute
用于附加参数,如PV定义的是OSS,就可以在这里定义bucket、访问的地址等信息
- NodePublishSecretRef
指定一个Secret的Name
调用CSI Plugin的NodePublishVolume/NodeUnpublishVolume接口时,使用该Secret传递敏感信息
FlexVolumeSource的数据结构:
- Driver
- ReadyOnly
- FsType
- SecretRef
- Options
由于是在Pod中声明的,所以其实是一种临时Volume。Volume需要和Pod一起生成,并且作为Pod的一部分,和Pod一起终结。
6、PVC
Pod中声明的Volume,生命周期与Pod相同。k8s又引入了PV(Persistent Volumes)概念,它可以将存储和计算分离,通过不同的组件来管理存储资源和计算资源,解耦Pod和Volume之间生命周期的关联。
当Pod删除后,它使用的PV仍然存在,还可以被新建的Pod复用。
用户只需在PVC(PersistentVolumeClaim)中声明自己需要的Resources、AccessModes等需求,而不需要关心存储实现细节。
PV才是存储的实际信息的承载体。PV和对应的后端存储信息交由集群管理员统一运维和管控,安全策略更容易控制。
PersistentVolumeSpec的数据结构
-
Capacity
使用map[ResourceName]resource.Quantity来描述资源量
一般只设置一对ResourceName为storage、resource.Quantity为xxx Gi的kv,表明创建的存储大小
- PersistentVolumeSource
该PV真正的Source
和Pod中直接声明Volume所支持的Source种类几乎一样(只是少了emptyDir、配置文件的四种、Ephemeral和自己)
- AccessModes
PV访问策略控制列表,表明创建出来的存储的访问方式
ReadWriteOnce只允许单node上的Pod访问
ReadOnlyMany允许多个node的Pod只读访问
ReadWriteMany允许多个node上的Pod读写访问
- VolumeMode
Volume将被如何挂载,包括Block和Filesystem两种
- ClaimRef
记录着PVC的绑定信息
- PersistentVolumeReclaimPolicy
表明该Volume被release后(即与之绑定的PVC被删除后)的回收策略
Delete:Volume被released后直接删除,需要Volume plugin支持
Retain:默认策略,由系统管理员来手动管理该Volume。
- StorageClassName
- MountOptions
传递挂载选项的String切片
PS:传递过程不会做验证,如果Volume plugin不支持某个Options,则分配操作可能会失败。
- NodeAffinity
限制了可以访问该Volumes的node,对使用该Volume的Pod调度有影响
PersistentVolumeClaimSpec的数据结构
- AccessModes
- Selector
通过标签选择器选择一组PV
- VolumeName
直接指定要绑定的PV名
- Resources
包括request和limit两种,使用map[ResourceName]resource.Quantity来描述资源需求量
- StorageClassName
- VolumeMode
-
DataSource
快照体系
k8s引入了VolumeSnapshotContent/VolumeSnapshot/VolumeSnapshotClass体系,以支持存储快照功能。不过目前只有CSI支持。
(1)首先创建VolumeSnapshotClass
apiVersion: snapshot.storage.k8s.io/v1a1pha1
kind: VolumeSnapshotClass
metadata:
name: disk-snapshotclass
snapshotter: xxxxxx
(2)通过Volumesnapshot声明创建存储快照:
apiVersion: snapshot.storage.k8s.io/v1a1pha1
kind: VolumeSnapshot
metadata:
name:disk-snapshot
namespace: xxx
spec:
snapshotClassName: disk-snapshotclass
source:
name: disk-pvc
kind: PersistentVolumeClaim
.spec.snapshotClassName指定了使用的VolumeSnapshotClass
.spec.source.name指定了作为数据源的PVC
提交VolumeSnapshot对象后,集群中的相关组件会找到数据源PVC对应的PV存储,对这个PV存储做一次快照。并自动创建Volumesnapshotcontent对象。
Volumesnapshotcontent对象中记录了云存储厂商返回的snapshot的ID、作为数据源的PVC
删除VolumeSnapshot后Volumesnapshotcontent也会自动删除
(3)数据恢复时,将PVC对象的DataSource指定为VolumeSnapshot对象。
这样当PVC提交之后,会由集群中的相关组件找到dataSource所指向的VolumeSnapshotContent,然后新创建对应的存储以及PV,将存储快照数据恢复到新的PV中。
绑定
通过kube-controller-manager中的PV Controller将PV和合适的PVC绑定到一起,绑定有三种方式:
(1)静态产生方式 - Static Volume Provisioning
集群管理员事先规划用户怎样使用存储,并预分配一些存储(即预创建一些 PV)
用户在使用存储的时候,需要先提交自己的存储需求(即PVC)
K8s内部相关组件会根据PVC的size和accessMode,判断它跟静态创建的哪个PV匹配,然后绑定到一起。
注:一个PV可以设置多个访问策略。PVC与PV绑定时,PV Controller会优先找到AccessModes列表最短并且匹配PVC AccessModes列表的PV集合,然后从该集合中找到Capacity最小且符合PVC size需求的PV对象
之后,用户通过Pod使用存储时,可以在volumes里声明要用哪个PVC。Pod通过该PVC找到绑定的PV。
(2)动态产生方式 - Dynamic Volume Provisioning
集群管理员不预分配PV,而是预先准备StoregeClass(创建PV的模板),它包含了创建某种具体类型(块存储、文件存储等)的PV所需要的参数信息。
StorageClass为管理员提供了描述存储 "类" 的方法,其数据结构:
-
metav1.TypeMeta
-
metav1.ObjectMeta
- Provisioner
指定了使用哪个Volume Plugin的Provisioner(存储分配器)来进行PV的provision/delete操作。
其中,Internal Provisioner的名称前缀为kubernetes.io并打包在Kubernetes中(由PV Controller进行此工作)
Volume Plugin
|
Internal Provisioner
|
Config Example
|
AWSElasticBlockStore
|
✓
|
|
AzureFile
|
✓
|
|
AzureDisk
|
✓
|
|
CephFS
|
-
|
-
|
Cinder
|
✓
|
|
FC
|
-
|
-
|
FlexVolume
|
-
|
-
|
Flocker
|
✓
|
-
|
GCEPersistentDisk
|
✓
|
|
Glusterfs
|
✓
|
|
iSCSI
|
-
|
-
|
Quobyte
|
✓
|
|
NFS
|
-
|
-
|
RBD
|
✓
|
|
VsphereVolume
|
✓
|
|
PortworxVolume
|
✓
|
|
ScaleIO
|
✓
|
|
StorageOS
|
✓
|
|
Local
|
-
|
-
ReclaimPolicy
由SC动态创建出来的PV将使用该ReclaimPolicy
-
AllowVolumeExpansion
若设置为true,且底层Volume Plugin支持卷扩展,可以编辑PVC对象来扩大(不能缩小)Volume大小
-
MountOptions
由SC动态创建的PV将使用该MountOptions
-
VolumeBindingMode
控制了卷绑定和动态分配的发生时间:
Immediate模式表示一旦创建了PVC,也就完成了卷的动态分配和绑定。对于由于拓扑限制而非集群所有节点可达的存储后端,PV会在不知道Pod调度要求的情况下分配、绑定。
WaitForFirstConsumer模式将延迟PV的分配和绑定,直到使用该PVC的Pod被创建。PV再根据Pod的调度结果分配和绑定。
-
AllowedTopologies
指定了允许的拓扑结构
VolumeBindingMode配置为WaitForFirstConsumer模式的情况下一般无需再配置此字段,除非除了Pod调度,动态创建的PV也有拓扑位置的限制。
此时需要同时配置WaitForFirstConsumer和AllowedTopologies,这样既限制了动态创建的PV要能被这个可用区访问、也限制了Pod在选择node时要落在这个可用区内。
-
Parameters
以key:values的形式描述属于Volume的参数,最多可定义512个,总长度不超过256KB
PV分配器可以接受哪些参数,需要后端存储的提供者提供。
该产生方式下,PVC配置方式不变,只需在PVC中指定需要使用的StoregeClass模板。
此时,若PVC找不到相应的PV,就会用指定的StorageClass模板去动态创建PV,并将PVC和PV进行绑定;存在一个满足条件的PV时,就会直接使用现有的PV。
(3)静态产生方式下满足PV的拓扑限制
Local PV一般通过静态创建的方式,先要声明PV对象,在PV对象中通过nodeAffinity来给这个PV加上拓扑限制,限制其只能在单node上访问
apiVersion: v1
kind: PersistentVolume
metadata:
name: pv-local
spec:
capacity:
storage: 5Gi
volumeMode: Filesystem
accessModes:
- ReadWriteOnce
persistentVolumeReclaimPolicy: Delete
storageClassName: local-storage
local:
path: /data
nodeAffinity:
required:
nodeSelectorTerms:
- matchExpressions:
- key: kubernetes.io/hostname
operator: In
values:
- cn-beijing.192.168.1.147
虽然是静态产生方式,仍然需要创建SC
kind: StorageClass
apiVersion: storage.k8s.io/v1
metadata:
name:lacal-storage
provisioner: kubernetes.io/noprovisioner
volumeBindingMode:WaitFirstConsumer
provisioner指定为no-provisioner,目的是告诉PV Controller遇到该PVC时无需进行任何操作
VolumeBindingMode需要设置为WaitForFirstConsumer。用户提交PVC后,PV Controller找到相应的SC,发现BindingMode是延迟绑定,就不会做任何事情。
当真正使用这个PVC的Pod,在调度的时恰好调度在符合PV拓扑要求的节点上之后,Pod里使用的PVC才会真正与PV做绑定。这样就能保证最终创建出来的Pod能访问这块Local PV。
PV和PVC的状态流转:
创建PV对象后,它会暂时处于pending状态。等真正的PV创建好之后,它处在available状态(可以使用)。
创建PVC对象后,它会暂时处于pending状态,直到PV和PVC就结合到一起,此时两者都处在bound状态。
当用户在使用完PVC将其删除后,PV处于released状态,依赖于ReclaimPolicy配置被删除或保留。在默认的Retain策略下,保留的PV无法从released状态回到available状态(即无法被一个新的PVC绑定),需要:
- 新建一个PV对象,把之前的released的PV的相关字段的信息填到新的PV对象里面,这个PV就可以结合新的PVC了;
- 删除Pod之后不删除PVC对象,这样给PV绑定的PVC还存在。下次Pod使用的时候,就可以直接通过PVC去复用。(K8s中的StatefulSet管理的Pod带存储的迁移就是通过这种方式)。
- 从PV的spec.claimRef字段删除PVC绑定信息,即可重新释放PV从而达到available。
处于Bound状态的PVC,与其关联的PV被删除后,变成Lost状态;重新与PV绑定后变成Bound状态。
7、ephemeral
区别于Persistent Volumes,k8s又提出了Ephemeral Volumes
其类似emptyDir,可以为每个Pod提供临时的数据存放目录
由于是通过volumeClaimTemplate声明的,其实是一种通用临时卷,可以由所有支持PV的存储所提供
spec:
volumes:
- name: scratch-volume
ephemeral:
volumeClaimTemplate:
metadata:
labels:
type: my-frontend-volume
spec:
accessModes: [ "ReadWriteOnce" ]
storageClassName: "scratch-storage-class"
resources:
requests:
storage: 1Gi
三、Volume挂载流程
(1)PV Controller
负责PV/PVC的绑定、生命周期管理,并根据需求进行数据卷的Provision/Delete操作。
主要有两个实现逻辑:
①ClaimWorker实现PVC的状态迁移:
②VolumeWorker实现PV的状态迁移:
(2)Attach/Detach Controller
负责存储设备的Attach/Detach操作,将设备挂载到目标节点
有两个核心对象:
下图是AD Controller实现的逻辑框图:
对于使用了CSI Plugin的PV/PVC,PV Controller和AD Controller不会进行Provision/Delete和Attach/Detach,只会进行一些辅助工作,详见container storage interface
(3)Volume manager
Volume Manager实际是Kubelet中众多Manager中的一个。它主要是用来做本节点Volume的Attach/Detach/Mount/Unmount操作、卷设备的格式化
它逻辑上和AD Controller类似,一样包含有desireStateofWorld以及actualStateofWorld,通过desiredStateOfWorldPopulator进行数据的同步、通过Reconciler进行接口的调用。
若Kubelet启动参数"--enable-controller-attach-detach" 为True,Attach/Detach操作由AD Controller来控制;若为False,则由Volume Manager来做(一般不需要,后续可能删除此功能)。
下图是 Volume Manager 实现逻辑图:
(4)Volume Plugins
PV Controller、AD Controller、Volume Manager主要是进行操作的调用,而具体操作则是由Volume Plugins实现的。
根据源码的位置可将Volume Plugins分为In-Tree和Out-of-Tree两类:
-
Out-of-Tree类的Volume Plugins的代码放在Kubernetes内部,和Kubernetes一起发布、管理与迭代,缺点是迭代速度慢、灵活性差;
-
Out-of-Tree类的Volume Plugins的代码独立于Kubernetes,它是由存储商提供实现的,目前主要有Flexvolume和CSI两种实现机制。通过抽象接口将不同存储的driver实现从k8s代码仓库中剥离,是社区主推的一种实现网络存储插件的方式。
Volume Plugins的发现
Kubernetes会在PV Controller、AD Controller以及Volume Manager中做插件管理。通过VolumePlguinMg对象进行管理。主要包含Plugins和Prober两个数据结构。
Plugins主要是用来保存Plugins列表的一个对象,而Prober是一个探针,用于发现新的Plugin
下图是插件管理的整个过程:
参考资料:
[1] https://kubernetes.io/docs/home/
[2] https://edu.aliyun.com/roadmap/cloudnative
[3] 郑东旭《Kubernetes源码剖析》