在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是一種volume,可讓pod掛載宿主機上的一個文件或目錄(若是掛載路徑不存在,則建立爲目錄或文件並掛載)。算法
最大的不一樣在於調度器是否能理解磁盤和node的對應關係,一個使用hostPath的pod,當他被從新調度時,頗有可能被調度到與原先不一樣的node上,這就致使pod內數據丟失了。而使用LPV的pod,總會被調度到同一個node上(不然就調度失敗)。api
首先 須要建立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.storageClassName
是local-storage
,即咱們一開始建立的storageclass實例的名字。
上面這個statefulset建立後,控制器會爲其建立對應的PVC,而且會爲PVC查找符合條件的PV,可是因爲咱們在local-storage
中配置了WaitForFirstConsumer
,因此控制器不會處理pvc和pv的bind;
同時,調度器在調度該pod時,predicate算法中也會根據PVC的要求去找到可用的PV,而且會過濾掉「與LPV的affinity」不匹配的node。最終,調度器發現:
example-pv
知足了pvc的要求;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
上。完成調度。
mkdir -p /mnt/disks/ssd1
mount -t /dev/vdc /mnt/disks/ssd1
對於已經被bind並被pod使用的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 }
瞭解調度器原理的應該知道,調度器的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
方法。
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
調度器在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更新)。
建立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 }