kubernetes中的Local Persistent Volume

什么是Local Persistent Volumes

kubernetes支持多种卷类型,主要分两种。一种是远端存储,一种是本地存储。大部分情况业务使用远端存储,这是为了让持久化的数据与计算节点彼此独立,即使计算节点宕机也不会影响到数据。但本地存储相比远端存储可以避免网络 IO 开销,拥有更高的读写性能。这里的 Local PV 指的就是利用机器上的磁盘来存放业务需要持久化的数据。(分布式文件系统和数据库一直是 Local PV 的主要用例)

这跟hostPath有什么区别

hostPath是一种volume,可以让pod挂载宿主机上的一个文件或目录(如果挂载路径不存在,则创建为目录或文件并挂载)。

最大的不同在于调度器是否能理解磁盘和node的对应关系,一个使用hostPath的pod,当他被重新调度时,很有可能被调度到与原先不同的node上,这就导致pod内数据丢失了。而使用Local PV的pod,总会被调度到同一个node上(否则就调度失败)。

如何使用Local PV

首先 需要创建StorageClass

1
2
3
4
5
6
kind: StorageClass
apiVersion: storage.k8s.io/v1
metadata:
name: local-storage
provisioner: kubernetes.io/no-provisioner
volumeBindingMode: WaitForFirstConsumer

注意到这里volumeBindingMode字段的值是WaitForFirstConsumer。这意味着kubernetes的pv控制器会将这类pv的binding延迟,直到有一个使用了对应PVC的Pod被创建出来且该Pod被调度完毕。这时候才会将PV和PVC进行binding,并且这时候pv的选择会结合调度的node和pv的NodeAffinity。

手动创建PV。但是必须要注意的是,Local PV必须要填写NodeAffinity

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
apiVersion: v1
kind: PersistentVolume
metadata:
name: example-pv
spec:
capacity:
storage: 100Gi
volumeMode: Filesystem
accessModes:
- ReadWriteOnce
persistentVolumeReclaimPolicy: Delete
storageClassName: local-storage
local:
path: /mnt/disks/ssd1
nodeAffinity:
required:
nodeSelectorTerms:
- matchExpressions:
- key: kubernetes.io/hostname
operator: In
values:
- k8s-n1

上面定义的local字段,指定了它是一个Local Persistent Volume;而path字段,指定的是这个PV对应的磁盘的路径。而这个磁盘存在于k8s-n1节点上,也就意味着pod使用这个pv就必须运行在这个节点上。

注意:目前,Local PV 的本地持久存储允许我们直接使用节点上的一块磁盘、一个分区或者一个目录作为持久卷的存储后端,但暂时还不提供动态配置支持,也就是说:你得先把 PV 准备好。(一台主机就创建一个PV,在nodeAffinity写不同的节点 k8s-n1、 k8s-n2、 k8s-n3),其他内容和一个普通 PV 无异,只是多了一个 nodeAffinity。

接下来可以创建各种workload,记得要在workload的模板中声明volumeClaimTemplates

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
apiVersion: apps/v1
kind: StatefulSet
metadata:
name: local-test
spec:
serviceName: "local-service"
replicas: 3
selector:
matchLabels:
app: local-test
template:
metadata:
labels:
app: local-test
spec:
containers:
- name: test-container
image: k8s.gcr.io/busybox
command:
- "/bin/sh"
args:
- "-c"
- "sleep 100000"
volumeMounts:
- name: local-vol
mountPath: /usr/test-pod
volumeClaimTemplates:
- metadata:
name: local-vol
spec:
accessModes: [ "ReadWriteOnce" ]
storageClassName: "local-storage"
resources:
requests:
storage: 100Gi

注意到这里volumeClaimTemplates.spec.storageClassNamelocal-storage,即我们一开始创建的storageclass实例的名字。

使用Local PV的pod的调度流程

上面这个statefulset创建后,控制器会为其创建对应的PVC,并且会为PVC查找符合条件的PV,但是由于我们在local-storage中配置了WaitForFirstConsumer,所以控制器不会处理pvc和pv的bind,此时pvc状态处于pending状态;

同时,调度器在调度该pod时,predicate算法中也会根据PVC的要求去找到可用的PV,并且会过滤掉“与Local PV的nodeAffinity”不匹配的node。最终,调度器发现:

  • pv:example-pv满足了pvc的要求;
  • node:k8s-n1节点满足了pv:example-pv的nodeAffinity要求。
  • 于是乎调度器尝试将pv和pvc bind起来,并且对pod进行重新调度。

重新调度pod时调度器发现pod的pvc资源得到了满足(都bound了pv),且bound的pv的nodeAffinity与node:k8s-n1匹配。于是将pod调度到node:k8s-n1上。完成调度。

延迟绑定的问题

当创建一个PVC后,一般来说kube-controller-manager会立即为它寻找一个合适的PV进行绑定。但是,这种即时绑定对于Local-PV会有一些问题。

我们来看这样子的一个场景:假设k8s集群有三台主机A、B、C,每台主机上有一块空磁盘,我们创建了三个Local-PV pv-a、pv-b和pv-c,分别关联到主机A、B、C的磁盘。然后我们要发布一个Pod,而且限制了这个Pod只能调度到B、C主机上。Pod中使用了一个PVC,假设是即时绑定,那么在创建Pod前,该PVC就已经绑定好了一个PV,而假设该PVC绑定的PV所关联的磁盘刚好又在A主机上,那么根据Local-PV的NodeAffinity,该Pod只能被调度到A主机上。那么这个时候,Pod就会找不到合适的主机,最终的结果就是调度失败。

所以,为了解决这个问题。我们可以为Local-PV定义一个StorageClass,声明绑定策略为延迟绑定,如下:

上面的WaitForFirstConsumer的意思是:当创建一个local类型的PVC时,不马上进行绑定,而是等待第一个使用它的Pod被创建后等待调度时,由kube-scheduler对PVC进行绑定。

接下来,我们再来看上面的场景:

当创建一个local类型的PVC后,kube-controller-manager不会为PVC绑定一个PV。然后我们再创建Pod,使用该PVC。此时,kube-scheduler准备对这个Pod进行调度,但发现它使用了一个local类型的PVC,于是kube-scheduler会先为这个PVC绑定一个PV。kube-scheduler发现Pod中限制了其只能被调度到B、C主机中,于是会为该PVC在pv-b与pv-c中选择一个进行绑定,假设为pv-b。绑定后,那么Pod也会被调度到主机B上。

通过这种延迟绑定机制,可以尽量避免Pod调度失败的机率。

如何创建Local PV

  • 在机器上创建目录: mkdir -p /mnt/disks/ssd1
  • 在机器上执行命令,将某个卷挂载到该目录:mount -t /dev/vdc /mnt/disks/ssd1
  • 在集群中创建对应的storageClass. 参见上文。
  • 手动创建本地卷的PV,或者通过provisioner去自动创建。手动创建的模板见上文。

如何删除Local PV

对于已经被bind并被pod使用的Local PV,删除一定要按照流程来 , 要不然会删除失败:

  • 删除使用这个pv的pod
  • 从node上移除这个磁盘(按照一个pv一块盘)
  • 删除pvc
  • 删除pv

注意事项

  • 使用local pv时必须定义nodeAffinity,Kubernetes Scheduler需要使用PV的nodeAffinity描述信息来保证Pod能够调度到有对应local volume的Node上。
  • 创建local PV之前,你需要先保证有对应的storageClass已经创建。并且该storageClass的volumeBindingMode必须是WaitForFirstConsumer以标识延迟Volume Binding。
  • 节点上local volume的初始化需要我们人为去完成(比如local disk需要pre-partitioned, formatted, and mounted. 共享存储对应的Directories也需要pre-created),并且人工创建这个local PV,当Pod结束,我们还需要手动的清理local volume,然后手动删除该local PV对象。因此,persistentVolumeReclaimPolicy只能是Retain
  • Local PV在生产中使用,也是需要谨慎的,毕竟它本质上还是使用的是节点上的本地存储,如果没有相应的存储副本机制,那意味着一旦节点或者磁盘异常,使用该volume的Pod也会异常,甚至出现数据丢失,除非你明确知道这个风险不会对你的应用造成很大影响或者允许数据丢失。

Local volumes的最佳实践

  • 对于需要强 IO 隔离的场景,推荐使用整块磁盘作为 Volume
  • 对于需要容量隔离的场景,推荐使用分区作为 Volume
  • 避免在集群中重新创建同名的Node(无法避免时需要先删除通过 Affinity 引用该 Node 的 PV)
  • 对于文件系统类型的本地存储,推荐使用 UUID (如 ls -l /dev/disk/by-uuid)作为系统挂载点
  • 对于无文件系统的块存储,推荐生成一个唯一 ID 作软链接(如 /dev/dis/by-id)。这可以保证 Volume 名字唯一,并不会与其他 Node 上面的同名 Volume 混淆

一个关于local volume功能局限性问题的讨论

通过实验发现一处问题,就是我们在定义PVC时是指定的申请50Mi的空间,而实际挂载到测试容器上的存储空间是495.8M,刚好是我们在某个node节点上挂载的一个文件系统的全部空间。

为什么会这样呢?这就是我们所使用的这个local persistent volume外部静态配置器的功能局限性所在了。它不支持动态的PV空间申请管理。

也就是说,虽然我们可以通过local volume manager配置PV,我们省去了手写PV YAML文件的痛苦。但是我们任然不会分配到自己理想的磁盘大小,如果node是500G的磁盘空间,在申请PVC申请50M那个任然会挂在500G的磁盘上来,这样导致磁盘空间浪费。如果申请的磁盘空间小了,当使用一段时候后发现不够又怎么办呢?这里推荐采用LVM方式;

那如果以前给某容器分配的一个存储空间不够用了怎么办?

  • 给大家的一个建议是使用Linux下的LVM(逻辑分区管理)来管理每个node节点上的本地磁盘存储空间。
  • 创建一个大的VG分组,把一个node节点上可以使用的存储空间都放进去;
  • 按未来一段时间内的容器存储空间使用预期,提前批量创建出一部分逻辑卷LVs,都挂载到自动发现目录下去;
  • 不要把VG中的存储资源全部用尽,预留少部分用于未来给个别容器扩容存储空间的资源;
  • 使用lvextend为特定容器使用的存储卷进行扩容;

kubernetes中的Local Persistent Volume
https://system51.github.io/2019/08/26/kubernetes-local-persistent-volume/
作者
Mr.Ye
发布于
2019年8月26日
许可协议