共计 5044 个字符,预计需要花费 13 分钟才能阅读完成。
什么是 Local Persistent Volumes
在 kubernetes 1.14 版本中,Local Persistent Volumes
(以下简称 LPV)已变为正式版本(GA),LPV 的概念在 1.7 中被首次提出(alpha),并在 1.10 版本中升级到 beat 版本。现在用户终于可以在生产环境中使用 LPV 的功能和 API 了。
首先:Local Persistent Volumes
代表了直接绑定在计算节点上的一块本地磁盘。
kubernetes 提供了一套卷插件(volume plugin)标准,使得 k8s 集群的工作负载可以使用多种块存储和文件存储。大部分磁盘插件都使用了远程存储,这是为了让持久化的数据与计算节点彼此独立,但远程存储通常无法提供本地存储那么强的读写性能。有了 LPV 插件,kubernetes 负载现在可以用同样的 volume api,在容器中使用本地磁盘。
这跟 hostPath 有什么区别
hostPath 是一种 volume,可以让 pod 挂载宿主机上的一个文件或目录(如果挂载路径不存在,则 mkdir 创建为目录并挂载)。
最大的不同在于调度器能理解磁盘和 node 的对应关系,一个使用 hostPath 的 pod,当他被重新调度时,很有可能被调度到与原先不同的 node 上,这就导致 pod 内数据丢失了。而使用 LPV 的 pod,总会被调度到同一个 node 上(否则就调度失败)。
如何使用 LPV
首先 需要创建 StorageClass
kind: StorageClass
apiVersion: storage.k8s.io/v1
metadata:
name: local-storage
provisioner: kubernetes.io/no-provisioner
volumeBindingMode: WaitForFirstConsumer
注意到这里 volumeBindingMode
字段的值是WaitForFirstConsumer
。这种 bindingmode 意味着:
kubernetes 的 pv 控制器会将这类 pv 的 binding 或 provisioning(可以理解为动态 create)延迟,直到有一个使用了对应 pvc 的 pod 被创建出来且该 pod 被调度完毕。这时候才会将 pv 和 pvc 进行 binding,并且这时候 pv 的选择会结合调度的 node 和 pv 的 nodeaffinity。
接下来,提前准备好的 provisioner 会动态创建 PV。
$ kubectl get pv
NAME CAPACITY ACCESS MODES RECLAIM POLICY STATUS CLAIM STORAGECLASS REASON AGE
local-pv-27c0f084 368Gi RWO Delete Available local-storage 8s
local-pv-3796b049 368Gi RWO Delete Available local-storage 7s
local-pv-3ddecaea 368Gi RWO Delete Available local-storage 7s
LPV 的详细内容如下:
$ kubectl describe pv local-pv-ce05be60
Name: local-pv-ce05be60
Labels: <none>
Annotations: pv.kubernetes.io/provisioned-by=local-volume-provisioner-minikube-18f57fb2-a186-11e7-b543-080027d51893
StorageClass: local-fast
Status: Available
Claim:
Reclaim Policy: Delete
Access Modes: RWO
Capacity: 1024220Ki
NodeAffinity:
Required Terms:
Term 0: kubernetes.io/hostname in [my-node]
Message:
Source:
Type: LocalVolume (a persistent volume backed by local storage on a node)
Path: /mnt/disks/vol1
Events: <none>
当然,也可以不使用 provisioner,而是手动创建 PV。但是必须要注意的是,LPV 必须要填写 nodeAffinity。
apiVersion: v1
kind: PersistentVolume
metadata:
name: example-pv
spec:
capacity:
storage: 100Gi
# volumeMode field requires BlockVolume Alpha feature gate to be enabled.
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:
- example-node
接下来可以创建各种 workload,在 workload 的模板中生命 volumeClaimTemplates。
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: 368Gi
注意到这里 volumeClaimTemplates.spec.storageClassName
是local-storage
即我们一开始创建的 storageclass 实例的名字。
上面这个 statefulset 创建后,控制器会为其创建对应的 PVC,并 bind 到某个 pv。同时,调度器在调度该 pod 时,predicate 算法中也会过滤掉“与 LPV 的 affinity”不匹配的 node。
如何删除这个 pv
一定要按照流程来 , 要不然会删除失败
- 删除使用这个 pv 的 pod
- 从 node 上移除这个磁盘(按照一个 pv 一块盘)
- 删除 pvc
- 删除 pv
对 LPV 延迟绑定的代码解读
所有的关键在于 volumeBinder
这个结构 这个继承了接口,包括:
type SchedulerVolumeBinder interface {FindPodVolumes(pod *v1.Pod, node *v1.Node)
AssumePodVolumes(assumedPod *v1.Pod, nodeName string)
BindPodVolumes(assumedPod *v1.Pod) error
GetBindingsCache() PodBindingCache}
FindPodVolumes
了解调度器原理的应该知道,调度器的 predicate 算法,在调度 pod 时,会逐个 node 的去进行 predicate,以确认这个 node 是否可以调度。我们称之为预选阶段。
VolumeBindingChecker
是一个检查器,在调度器的算法工厂初始化的最后一步,会向工厂中注册检查算法,这样调度器在进行 predicate 时,最后一步会执行对 volumeBinding 的检查。我们看func (c *VolumeBindingChecker) predicate
方法就能看到,这里面执行了FindPodVolumes
,并且判断返回的几个值是否为 true,或 err 是否为空:
unboundSatisfied, boundSatisfied, err := c.binder.Binder.FindPodVolumes(pod, node)
boundSatisfied 为 false 表示 pod 绑定的 pv 与当前计算的 node 亲和性不过关。
unboundSatisfied 为 false 表示 pod 中申明的未 bound 的 pvc,在集群内的 pv 中找不到可以匹配的。
就这样,调度器会反复去重试调度,反复执行 FindPodVolumes
, 直到我们(或者 provisoner)创建出了 PV,比如这时新建的 PV,其 nodeAffinity 对应到了 node A。这次调度,在对 node A 进行 predicate 计算时,发现 pod 中申明的、未 bound 的 pvc,在集群中有合适的 pv,且该 pv 的 nodeAffinity 就是 node A,于是返回的unboundSatisfied
为 true,调度器最终找到了一个合适的 node。
那么,调度器接下来要对 pod 执行 assume,在对 pod assume 之前,调度器要先对 pod 中 bind 的 volume 进行 assume。见 func (sched *Scheduler) assumeAndBindVolumes(assumed *v1.Pod, host string) error
。这个函数里,我们调用了volumeBinder
的AssumePodVolumes
方法。
AssumePodVolumes
assume 是假设的意思,顾名思义,这个方法会先在调度器的缓存中,假定 pod 已经调度到 node A 上,对缓存中的 pv、pvc、binding 等资源进行更新,看是否能成功,它会返回一些讯息:
allBound, bindingRequired, err := sched.config.VolumeBinder.Binder.AssumePodVolumes(assumed, host)
allBound 为 true 表示所有的 pv、pvc,在缓存中已经是 bind。如果为 false,会最终导致本次调度失败。
bindingRequired 为 true 表示有一些 pv 需要和 pvc bind 起来。如果为 true,调度器会向 volumeBinder
的BindQueue
中写入一个用例。这个队列会被一个 worker 轮询,并进行对应的工作。
什么工作呢?BindPodVolumes
BindPodVolumes
调度器在 Run 起来的时候,会启动一个协程,反复执行 bindVolumesWorker。在这个 worker 中我们可以看到,他尝试从 volumeBinder
的BindQueue
中取出任务,进行BindPodVolumes
, 成功则该任务 Done,失败则报错重试。
阅读 BindPodVolumes
这个方法,很简单,从缓存中找到对应的 pod、pv、pvc 等内容,更新到 APIserver 中。
由于我们在 AssumePodVolumes
中已经更新了缓存,所以这里更新到 apiserver 的操作,会真正地将 pv 和 pvc bind 起来。
之后呢?
在 worker 中我们看到,如果 BindPodVolumes
成功,依然会构造一个 pod 调度失败的事件,并更新 pod 的状态为PodScheduled
, 这么做是为了将 pod 放回调度队列,让调度器再去调度一次。
我们假设 pod 中只申明了一个 LPV, 在刚刚描述的这次 BindPodVolumes
操作中已经在 apiserver 中对这个 LPV,和 pod 中的 pvc 进行了 bind。那么,下一次调度器调度 pod 时,在 AssumePodVolumes
时会发现已经allBound
, 调度器会继续后续的操作,最终 pod 被成功地调度(创建出 Binding 资源,apiserver 将 pod 的 nodeName 更新)。