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的工做機制

相關文章
相關標籤/搜索