kubernetes中的local-persistent-volume

55次阅读

共计 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.storageClassNamelocal-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。这个函数里,我们调用了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 更新)。

正文完
 0