内容简介:在kubernetes 1.14版本中,首先:kubernetes提供了一套卷插件(volume plugin)标准,使得k8s集群的工作负载可以使用多种块存储和文件存储。大部分磁盘插件都使用了远程存储,这是为了让持久化的数据与计算节点彼此独立,但远程存储通常无法提供本地存储那么强的读写性能。有了LPV 插件,kubernetes负载现在可以用同样的volume api,在容器中使用本地磁盘。
什么是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更新)。
以上就是本文的全部内容,希望对大家的学习有所帮助,也希望大家多多支持 码农网
猜你喜欢:本站部分资源来源于网络,本站转载出于传递更多信息之目的,版权归原作者或者来源机构所有,如转载稿涉及版权问题,请联系我们。
Remote
Jason Fried、David Heinemeier Hansson / Crown Business / 2013-10-29 / CAD 26.95
The “work from home” phenomenon is thoroughly explored in this illuminating new book from bestselling 37signals founders Fried and Hansson, who point to the surging trend of employees working from hom......一起来看看 《Remote》 这本书的介绍吧!