kubernetes中的local persistent volume

什么是Local Persistent Volumes

在kubernetes 1.14版本中, Local Persistent Volumes(如下简称LPV)已变为正式版本(GA),LPV的概念在1.7中被首次提出(alpha),并在1.10版本中升级到beat版本。如今用户终于能够在生产环境中使用LPV的功能和API了。node

首先:Local Persistent Volumes表明了直接绑定在计算节点上的一块本地磁盘。git

kubernetes提供了一套卷插件(volume plugin)标准,使得k8s集群的工做负载可使用多种块存储和文件存储。大部分磁盘插件都使用了远程存储,这是为了让持久化的数据与计算节点彼此独立,但远程存储一般没法提供本地存储那么强的读写性能。有了LPV 插件,kubernetes负载如今能够用一样的volume api,在容器中使用本地磁盘。github

这跟hostPath有什么区别

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

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

如何使用LPV

首先 须要建立StorageClass缓存

kind: StorageClass
apiVersion: storage.k8s.io/v1
metadata:
  name: local-storage
provisioner: kubernetes.io/no-provisioner
volumeBindingMode: WaitForFirstConsumer

注意到这里volumeBindingMode字段的值是WaitForFirstConsumer。这种bindingmode意味着:app

kubernetes的pv控制器会将这类pv的binding延迟,直到有一个使用了对应pvc的pod被建立出来且该pod被调度完毕。这时候才会将pv和pvc进行binding,而且这时候pv的选择会结合调度的node和pv的nodeaffinity。ide

接下来,提早准备好的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。 (1.10前k8s是将nodeAffinity做为annotation记录到PV中,1.10起将其独立为一个字段)

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.storageClassNamelocal-storage,即咱们一开始建立的storageclass实例的名字。

使用LPV的pod的调度流程

上面这个statefulset建立后,控制器会为其建立对应的PVC,而且会为PVC查找符合条件的PV,可是因为咱们在local-storage中配置了WaitForFirstConsumer,因此控制器不会处理pvc和pv的bind;

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

  • pv:example-pv知足了pvc的要求;
  • node:example-node知足了pv:example-pv的nodeAffinity要求。

因而乎调度器尝试将pv和pvc bind起来,而且对pod进行从新调度。

从新调度pod时调度器发现pod的pvc资源获得了知足(都bound了pv),且bound的pv的nodeAffinity与node:example-node匹配。因而将pod调度到node:example-node上。完成调度。

如何建立LPV

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

如何删除LPV

对于已经被bind并被pod使用的LPV,删除必定要按照流程来 , 要否则会删除失败:

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

LPV延迟绑定部分的代码解读

全部的关键在于volumeBinder这个结构,它继承了SchedulerVolumeBinder接口,包括:

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 。这个函数里,咱们调用了volumeBinderAssumePodVolumes方法。

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,调度器会向volumeBinderBindQueue中写入一个用例。这个队列会被一个worker轮询,并进行对应的工做。

什么工做呢? BindPodVolumes

BindPodVolumes

调度器在Run起来的时候,会启动一个协程,反复执行bindVolumesWorker。在这个worker中咱们能够看到,他尝试从volumeBinderBindQueue中取出任务,进行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更新)。

pv控制器无论吗?

建立PVC后,pv控制器会有一个worker:syncUnboundClaim去管理未bind的pvc。这个worker中,对于spec.VolumeName不为空的pvc,会去进行bind操做,确保pv和pvc绑定起来;对于spec.VolumeName为空的pvc,会去检查是否延迟绑定,并查找集群中适合该pvc的pv(这里没有node的概念,因此在查找时更多地是根据selector和AccessModes去过滤)。能够在

func findMatchingVolume(
    claim *v1.PersistentVolumeClaim,
    volumes []*v1.PersistentVolume,
    node *v1.Node,
    excludedVolumes map[string]*v1.PersistentVolume,
    delayBinding bool) (*v1.PersistentVolume, error)

中找到过滤的逻辑。这里咱们只要知道:对于延迟绑定的pvc,咱们会过滤掉全部的pv,并最后发出一个WaitForFirstConsumer的event结束worker。

可见,pv控制器对于延迟调度的pvc听任自流了。咱们在findMatchingVolume方法中也能够看到官方的一段注释:

if node == nil && delayBinding {
    // PV controller does not bind this claim.
    // Scheduler will handle binding unbound volumes
    // Scheduler path will have node != nil
    continue
}

总结本地盘的使用流程

图片描述

图片描述

待补充: pv控制器、CSI的工做机制

相关文章
相关标签/搜索