4.深入k8s:持久卷PV、PVC及其源碼分析


img

從一個例子入手PV、PVC

Kubernetes 項目引入了一組叫作 Persistent Volume Claim(PVC)和 Persistent Volume(PV)的 API 對象用於管理存儲卷。

下面舉個例子看看,這個例子來自《k8s in Action》:

apiVersion: v1
kind: PersistentVolumeClaim
metadata:
  name: mongodb-pvc
spec:
  resources:
    requests:
      storage: 1Gi
  accessModes:
  - ReadWriteOnce
  storageClassName: ""

yaml文件中定義了storage為1 GiB表示PVC需要的容量;

Access Modes表示需要的volume存儲類型,ReadWriteOnce表示只能在一個node節點上進行讀寫操作,其他的Access Modes詳見:https://kubernetes.io/docs/concepts/storage/persistent-volumes/#access-modes;

storageClassName為空,表示的是storageClass的名稱,我們下面會說到。

然后獲取一下PVC的狀態:

$ kubectl get pvc
NAME                   STATUS   VOLUME              CAPACITY   ACCESS MODES   STORAGECLASS   AGE
mongodb-pvc            Available    mongodb-pv          1Gi        RWO,ROX                       2m25s

此時可以看到,我們的PVC處於可用狀態。

然后再定義一個PV:

apiVersion: v1
kind: PersistentVolume
metadata:
  name: mongodb-pv
spec:
  capacity:
    storage: 1Gi
  accessModes:
  - ReadWriteOnce
  - ReadOnlyMany
  persistentVolumeReclaimPolicy: Retain
  gcePersistentDisk:
    pdName: mongodb
    fsType: ext4

這個 PV 對象中會詳細定義存儲的類型是GCE,以及大小是1 GiB,這里沒有聲明storageClassName,是因為storageClassName默認就為空。

然后我們看一下PV和PVC的狀態:

$ kubectl get pv
NAME                CAPACITY   ACCESS MODES   RECLAIM POLICY   STATUS      CLAIM                          STORAGECLASS   REASON   AGE
mongodb-pv          1Gi        RWO,ROX        Retain           Bound       default/mongodb-pvc                                    77m

$ kubectl get pvc
NAME                   STATUS   VOLUME              CAPACITY   ACCESS MODES   STORAGECLASS   AGE
mongodb-pvc            Bound    mongodb-pv          1Gi        RWO,ROX                       7m7s

可以看到PV和PVC已經相互綁定了。

PVC和PV相當於“接口”和“實現”,所以我們需要將PVC和PV綁定起來才可以使用,而PVC和PV綁定的時候需要滿足:

  1. PV 和 PVC 的 spec 字段要匹配,比如PV 的存儲(storage)大小,就必須滿足 PVC 的要求。
  2. PV 和 PVC 的 storageClassName 字段必須一樣才能進行綁定。storageClassName表示的是StorageClass的name屬性。

如果我們想要在Pod中使用這個PVC,那么我們可以這么做:

apiVersion: v1
kind: Pod
metadata:
  name: mongodb
spec:
  containers:
  - image: mongo
    name: mongodb
    volumeMounts:
    - name: mongodb-data
      mountPath: /data/db
    ports:
    - containerPort: 27017
      protocol: TCP
  volumes:
  - name: mongodb-data
    persistentVolumeClaim:
      claimName: mongodb-pvc

在Pod中只需要聲明PVC的名字,等Pod創建后kubelet 就會把這個 PVC 所對應的 PV,也就是一個 GCE類型的 Volume,掛載在這個 Pod 容器內的目錄上。

PersistentVolumeController會不斷地查看當前每一個 PVC,是不是已經處於 Bound(已綁定)狀態。如果不是,那它就會遍歷所有的、可用的 PV,並嘗試將其與這個“單身”的 PVC 進行綁定。這個PersistentVolumeController的源碼我們下面會進行分析。那么問題來了,k8s為什么要將一個存儲卷分成兩部分呢?

因為實際上,我們項目當中,研發人員和集群的管理人員是分開的,研發人員只管使用,但是並不關心底層到底用的是什么存儲技術,所以研發人員只要聲明一個PVC,表示我需要多大的一個存儲,以及讀寫類型就可以了。

pv_and_pvc

StorageClass的Dynamic Provisioning

在上面我們說的PV和PVC綁定的過程稱為Static Provisioning,需要手動的創建PV,我們在研發中可能有這樣的情況,就是管理員沒有及時給我們創建對應的PV,難道一直等着嗎?所以這個時候就需要用到StorageClass了,StorageClass提供了Dynamic Provisioning機制,可以根據模板創建PV。

StorageClass 對象會定義如下兩個部分內容:

  1. PV 的屬性。比如,存儲類型、Volume 的大小等等。
  2. 創建這種 PV 需要用到的存儲插件。比如,Ceph 等等。

這樣k8s就能夠根據用戶提交的 PVC,找到一個對應的 StorageClass ,然后調用該 StorageClass 聲明的存儲插件,創建出需要的 PV。

例如聲明如下StorageClass:

apiVersion: storage.k8s.io/v1
kind: StorageClass
metadata:
  name: block-service
provisioner: kubernetes.io/gce-pd
parameters:
  type: pd-ssd

這里定義了名叫 block-service 的 StorageClass,provisioner 字段的值是:kubernetes.io/gce-pd,這是k8s內置的存儲插件,type字段也是跟着provisioner定義的,官方默認支持 Dynamic Provisioning 的內置存儲插件:https://kubernetes.io/docs/concepts/storage/storage-classes/。

然后就可以在PVC中聲明storageClassName為block-service,當創建好PVC 對象之后,k8s就會調用相應的存儲插件API創建一個PV對象。

如下:

apiVersion: v1
kind: PersistentVolumeClaim
metadata:
  name: claim1
spec:
  accessModes:
    - ReadWriteOnce
  storageClassName: block-service
  resources:
    requests:
      storage: 30Gi

這種自動創建PV的機制就是Dynamic Provisioning,Kubernetes 就能夠根據用戶提交的 PVC,找到一個對應的 StorageClass ,然后會調用StorageClass 聲明的存儲插件,創建出需要的 PV。

需要注意的是,如果沒有聲明StorageClassName在PVC中,PVC 的 storageClassName 的值就是"",這也意味着它只能夠跟 storageClassName 也是""的 PV 進行綁定。

PV和PVC的生命周期

PV和PVC之間的相互作用遵循這個生命周期:

Provisioning --->Binding --->Using --->Reclaiming

Provisioning

k8s提供了兩種PV生成方式: statically or dynamically

statically:由管理員創建PV,它們攜帶可供集群用戶使用的真實存儲的詳細信息。 它們存在於Kubernetes API中,可用於消費。

dynamically:當管理員創建的靜態PV都不匹配用戶的PersistentVolumeClaim時,集群可能會嘗試為PVC動態配置卷。 此配置基於StorageClasses,PVC必須請求一個StorageClasses,並且管理員必須已創建並配置該類才能進行動態配置。

Binding

由用戶創建好PersistentVolumeClaim 后,PersistentVolumeController會不斷地查看當前每一個 PVC,是不是已經處於 Bound(已綁定)狀態。如果不是,那它就會遍歷所有的、可用的 PV,並嘗試將其與這個“單身”的 PVC 進行綁定。

Using

Pods聲明並使用PVC作為volume后,集群會找到該PVC,如果該PVC已經綁定了PV,那么會將該volume掛載到Pod中。

Reclaiming

當用戶已經不再使用該volume,可以將該PVC刪除,以便讓資源得以回收。相應的在PVC刪除后,PV的回收策略可以是Retained, Recycled, or Deleted,這個策略可以在字段spec.persistentVolumeReclaimPolicy中設置。

  • Retain:這個策略允許手動回收資源,當PVC被刪除后,PV仍然可以存在,管理員可以手動的執行刪除PV,並且和PV綁定的存儲資源也不會被刪除,如果想要刪除相應的存儲資源的數據,需要手動刪除對應存儲資源的數據。
  • Delete:這個策略會在PVC被刪除之后,連帶將PV以及PV管理的存儲資源也刪除。
  • Recycle:相當於在volume中執行rm -rf /thevolume/*命令,以便讓volume可以重復利用。

一般的情況下,我們遵循這個刪除流程:

  1. 刪除使用這個 PV 的 Pod;
  2. 從宿主機移除本地磁盤(比如,umount 它);
  3. 刪除 PVC;
  4. 刪除 PV。

源碼分析

PV和PVC的源碼處理邏輯都在pv_controller_base.go和pv_controller.go這兩個文件中。我們直接看核心代碼是怎樣的。

首先來看看PersistentVolumeController的Run方法,這是入口:

func (ctrl *PersistentVolumeController) Run(stopCh <-chan struct{}) {
	... 
	go wait.Until(ctrl.resync, ctrl.resyncPeriod, stopCh)
	go wait.Until(ctrl.volumeWorker, time.Second, stopCh)
	go wait.Until(ctrl.claimWorker, time.Second, stopCh)
	...
}

這個代碼主要就是起了三個Goroutine,分別運行不同的方法。resync方法十分簡單,主要作用是找出pv和pvc列表然后放入到隊列volumeQueue和claimQueue中,給volumeWorker和claimWorker進行消費。所以下面我們主要看volumeWorker以及claimWorker

volumeWorker

volumeWorker會不斷循環消費volumeQueue隊列里面的數據,然后獲取到相應的PV執行updateVolume操作。

func (ctrl *PersistentVolumeController) updateVolume(volume *v1.PersistentVolume) {
	// Store the new volume version in the cache and do not process it if this
	// is an old version.
	//更新緩存
	new, err := ctrl.storeVolumeUpdate(volume)
	if err != nil {
		klog.Errorf("%v", err)
	}
	if !new {
		return
	}
	//核心方法,根據當前 PV 對象的規格對 PV 和 PVC 進行綁定或者解綁
	err = ctrl.syncVolume(volume)
	if err != nil {
		if errors.IsConflict(err) {
			// Version conflict error happens quite often and the controller
			// recovers from it easily.
			klog.V(3).Infof("could not sync volume %q: %+v", volume.Name, err)
		} else {
			klog.Errorf("could not sync volume %q: %+v", volume.Name, err)
		}
	}
}

updateVolume方法會調用syncVolume方法,執行核心流程。

我們繼續:

func (ctrl *PersistentVolumeController) syncVolume(volume *v1.PersistentVolume) error {
	klog.V(4).Infof("synchronizing PersistentVolume[%s]: %s", volume.Name, getVolumeStatusForLogging(volume)) 
	...
	//如果spec.claimRef未設置,則是未使用過的pv,則調用updateVolumePhase函數更新狀態設置 phase 為 available
	if volume.Spec.ClaimRef == nil { 
		klog.V(4).Infof("synchronizing PersistentVolume[%s]: volume is unused", volume.Name)
		if _, err := ctrl.updateVolumePhase(volume, v1.VolumeAvailable, ""); err != nil { 
			return err
		}
		return nil
	} else /* pv.Spec.ClaimRef != nil */ { 
		//正在被bound中,更新狀態available
		if volume.Spec.ClaimRef.UID == "" { 
			klog.V(4).Infof("synchronizing PersistentVolume[%s]: volume is pre-bound to claim %s", volume.Name, claimrefToClaimKey(volume.Spec.ClaimRef))
			if _, err := ctrl.updateVolumePhase(volume, v1.VolumeAvailable, ""); err != nil { 
				return err
			}
			return nil
		}
		klog.V(4).Infof("synchronizing PersistentVolume[%s]: volume is bound to claim %s", volume.Name, claimrefToClaimKey(volume.Spec.ClaimRef))
		// Get the PVC by _name_
		var claim *v1.PersistentVolumeClaim
		//根據 pv 的 claimRef 獲得 pvc
		claimName := claimrefToClaimKey(volume.Spec.ClaimRef)
		obj, found, err := ctrl.claims.GetByKey(claimName)
		if err != nil {
			return err
		}
		//如果在隊列未發現,可能是volume被刪除了,或者失敗了,重新同步pvc
		if !found && metav1.HasAnnotation(volume.ObjectMeta, pvutil.AnnBoundByController) { 
			if volume.Status.Phase != v1.VolumeReleased && volume.Status.Phase != v1.VolumeFailed {
				obj, err = ctrl.claimLister.PersistentVolumeClaims(volume.Spec.ClaimRef.Namespace).Get(volume.Spec.ClaimRef.Name)
				if err != nil && !apierrors.IsNotFound(err) {
					return err
				}
				found = !apierrors.IsNotFound(err)
				if !found {
					obj, err = ctrl.kubeClient.CoreV1().PersistentVolumeClaims(volume.Spec.ClaimRef.Namespace).Get(context.TODO(), volume.Spec.ClaimRef.Name, metav1.GetOptions{})
					if err != nil && !apierrors.IsNotFound(err) {
						return err
					}
					found = !apierrors.IsNotFound(err)
				}
			}
		}
		if !found {
			klog.V(4).Infof("synchronizing PersistentVolume[%s]: claim %s not found", volume.Name, claimrefToClaimKey(volume.Spec.ClaimRef)) 
		} else {
			var ok bool
			claim, ok = obj.(*v1.PersistentVolumeClaim)
			if !ok {
				return fmt.Errorf("Cannot convert object from volume cache to volume %q!?: %#v", claim.Spec.VolumeName, obj)
			}
			klog.V(4).Infof("synchronizing PersistentVolume[%s]: claim %s found: %s", volume.Name, claimrefToClaimKey(volume.Spec.ClaimRef), getClaimStatusForLogging(claim))
		}
		if claim != nil && claim.UID != volume.Spec.ClaimRef.UID { 
			klog.V(4).Infof("synchronizing PersistentVolume[%s]: claim %s has different UID, the old one must have been deleted", volume.Name, claimrefToClaimKey(volume.Spec.ClaimRef))
			// Treat the volume as bound to a missing claim.
			claim = nil
		}
		//claim可能被刪除了,或者pv被刪除了
		if claim == nil { 
			if volume.Status.Phase != v1.VolumeReleased && volume.Status.Phase != v1.VolumeFailed {
				// Also, log this only once:
				klog.V(2).Infof("volume %q is released and reclaim policy %q will be executed", volume.Name, volume.Spec.PersistentVolumeReclaimPolicy)
				if volume, err = ctrl.updateVolumePhase(volume, v1.VolumeReleased, ""); err != nil { 
					return err
				}
			}
			//根據persistentVolumeReclaimPolicy配置做相應的處理,Retain 保留/ Delete 刪除/ Recycle 回收
			if err = ctrl.reclaimVolume(volume); err != nil { 
				return err
			}
			if volume.Spec.PersistentVolumeReclaimPolicy == v1.PersistentVolumeReclaimRetain {
				// volume is being retained, it references a claim that does not exist now.
				klog.V(4).Infof("PersistentVolume[%s] references a claim %q (%s) that is not found", volume.Name, claimrefToClaimKey(volume.Spec.ClaimRef), volume.Spec.ClaimRef.UID)
			}
			return nil
		} else if claim.Spec.VolumeName == "" {
			if pvutil.CheckVolumeModeMismatches(&claim.Spec, &volume.Spec) { 
				volumeMsg := fmt.Sprintf("Cannot bind PersistentVolume to requested PersistentVolumeClaim %q due to incompatible volumeMode.", claim.Name)
				ctrl.eventRecorder.Event(volume, v1.EventTypeWarning, events.VolumeMismatch, volumeMsg)
				claimMsg := fmt.Sprintf("Cannot bind PersistentVolume %q to requested PersistentVolumeClaim due to incompatible volumeMode.", volume.Name)
				ctrl.eventRecorder.Event(claim, v1.EventTypeWarning, events.VolumeMismatch, claimMsg)
				// Skipping syncClaim
				return nil
			}

			if metav1.HasAnnotation(volume.ObjectMeta, pvutil.AnnBoundByController) { 
				klog.V(4).Infof("synchronizing PersistentVolume[%s]: volume not bound yet, waiting for syncClaim to fix it", volume.Name)
			} else { 
				klog.V(4).Infof("synchronizing PersistentVolume[%s]: volume was bound and got unbound (by user?), waiting for syncClaim to fix it", volume.Name)
			} 
			ctrl.claimQueue.Add(claimToClaimKey(claim))
			return nil
		//	已經綁定更新狀態status phase為Bound
		} else if claim.Spec.VolumeName == volume.Name {
			// Volume is bound to a claim properly, update status if necessary
			klog.V(4).Infof("synchronizing PersistentVolume[%s]: all is bound", volume.Name)
			if _, err = ctrl.updateVolumePhase(volume, v1.VolumeBound, ""); err != nil {
				// Nothing was saved; we will fall back into the same
				// condition in the next call to this method
				return err
			}
			return nil
		//	PV綁定到PVC上,但是PVC被綁定到其他PV上,重置
		} else {
			// Volume is bound to a claim, but the claim is bound elsewhere
			if metav1.HasAnnotation(volume.ObjectMeta, pvutil.AnnDynamicallyProvisioned) && volume.Spec.PersistentVolumeReclaimPolicy == v1.PersistentVolumeReclaimDelete {
				 
				if volume.Status.Phase != v1.VolumeReleased && volume.Status.Phase != v1.VolumeFailed { 
					klog.V(2).Infof("dynamically volume %q is released and it will be deleted", volume.Name)
					if volume, err = ctrl.updateVolumePhase(volume, v1.VolumeReleased, ""); err != nil { 
						return err
					}
				}
				if err = ctrl.reclaimVolume(volume); err != nil { 
					return err
				}
				return nil
			} else { 
				if metav1.HasAnnotation(volume.ObjectMeta, pvutil.AnnBoundByController) { 
					klog.V(4).Infof("synchronizing PersistentVolume[%s]: volume is bound by controller to a claim that is bound to another volume, unbinding", volume.Name)
					if err = ctrl.unbindVolume(volume); err != nil {
						return err
					}
					return nil
				} else { 
					klog.V(4).Infof("synchronizing PersistentVolume[%s]: volume is bound by user to a claim that is bound to another volume, waiting for the claim to get unbound", volume.Name) 
					if err = ctrl.unbindVolume(volume); err != nil {
						return err
					}
					return nil
				}
			}
		}
	}
}

這個方法有點長,我們一步步分析:

這個方法首先會校驗一個ClaimRef有沒有被設置,因為如果一個PV被綁定了,那么它的ClaimRef屬性是會被賦值的,我們可以使用kubectl edit pv mongodb-pv 進入到實例中查看當前的PV屬性,會發現:

  claimRef:
    apiVersion: v1
    kind: PersistentVolumeClaim
    name: mongodb-pvc
    namespace: default
    resourceVersion: "824043"
    uid: 5cf34ad0-2181-4d99-9875-0d4559e58f42

所以如果這個屬性為空,那么需要更新PV的狀態為Available。

如果ClaimRef不為空,接下來會校驗UID屬性,UID為空說明PV綁定了PVC,但是PVC卻沒有綁定PV,所以需要重新設置PV的狀態為Available;

然后獲取PV對應的PVC,如果在PVC集合里沒有找到對應的PVC,那么為了防止本地緩存還未刷新,所以再通過apiserver再去找一下,然后給found變量打上標記;

如果找到了對應的PVC,那么需要比較一下UID是否相等,如果不相等,那么說明不是被綁定的那個PVC,可以認為PVC是被刪除了,那么需要更新釋放PV,將PV的狀態改為Released;

然后會調用reclaimVolume方法,這個方法里面會根據persistentVolumeReclaimPolicy配置做相應的處理:

PersistentVolumeController#reclaimVolume

func (ctrl *PersistentVolumeController) reclaimVolume(volume *v1.PersistentVolume) error {
	...
	switch volume.Spec.PersistentVolumeReclaimPolicy {
	//這個策略允許手動回收資源,當PVC被刪除后,PV仍然可以存在,管理員可以手動的執行刪除PV
	case v1.PersistentVolumeReclaimRetain:
		klog.V(4).Infof("reclaimVolume[%s]: policy is Retain, nothing to do", volume.Name)
	//回收PV,如果沒有pod在使用PV,那么將該PV的狀態設置為Available
	case v1.PersistentVolumeReclaimRecycle:
		...
		ctrl.scheduleOperation(opName, func() error {
			ctrl.recycleVolumeOperation(volume)
			return nil
		})
	//這個策略會在PVC被刪除之后,連帶將PV以及PV管理的存儲資源也刪除
	case v1.PersistentVolumeReclaimDelete:
		...
		ctrl.scheduleOperation(opName, func() error {
			_, err := ctrl.deleteVolumeOperation(volume)
			if err != nil { 
				metrics.RecordMetric(volume.Name, &ctrl.operationTimestamps, err)
			}
			return err
		})

	default:
		...
	}
	return nil
}

這個方法里面是用了一個switch case來處理PersistentVolumeReclaimPolicy策略,如果是Retain策略,那么需要手動執行刪除,這里只記錄了一個log;如果是Recycle則調用recycleVolumeOperation執行解綁操作;如果是Delete則調用deleteVolumeOperation方法將對應的PV刪除。

下面我們來挑deleteVolumeOperation看一下這個方法的具體實現:

func (ctrl *PersistentVolumeController) deleteVolumeOperation(volume *v1.PersistentVolume) (string, error) {
	klog.V(4).Infof("deleteVolumeOperation [%s] started", volume.Name)

	//這里先讀取最新的PV實例
	newVolume, err := ctrl.kubeClient.CoreV1().PersistentVolumes().Get(context.TODO(), volume.Name, metav1.GetOptions{})
	if err != nil {
		klog.V(3).Infof("error reading persistent volume %q: %v", volume.Name, err)
		return "", nil
	}
	//如果已經被刪除了,直接返回
	if newVolume.GetDeletionTimestamp() != nil {
		klog.V(3).Infof("Volume %q is already being deleted", volume.Name)
		return "", nil
	}
	//看一下是否還能找得到對應的PVC
	needsReclaim, err := ctrl.isVolumeReleased(newVolume)
	if err != nil {
		klog.V(3).Infof("error reading claim for volume %q: %v", volume.Name, err)
		return "", nil
	}
	//如果還有PVC與之關聯,那么就不能刪除這個PV
	if !needsReclaim {
		klog.V(3).Infof("volume %q no longer needs deletion, skipping", volume.Name)
		return "", nil
	}
	//調用相應的plugin刪除PV
	pluginName, deleted, err := ctrl.doDeleteVolume(volume) 
	...
	return pluginName, nil
}

可見在執行刪除的時候先會進行一系列的校驗,會去確認這個PV是否已手動刪除、PV所對應的PVC是否還存在然后才調用對應的插件執行刪除。

我們繼續回到PersistentVolumeController的syncVolume方法中。對claim進行校驗之后會繼續檢查VolumeName是否為空,這種情況是表明正在綁定中;

如果PVC的VolumeName等於PV的name,那么說明已經綁定,那么更新一下狀態為Bound;否則表示PV綁定到PVC上,但是PVC被綁定到其他PV上,檢查一下是否是dynamically provisioned自動生成的,如果是的話就釋放這個PV;如果是手動創建的PV,那么調用unbindVolume進行解綁。

到這里我們volumeWorker已經看完了,接下來看一下claimWorker

claimWorker

和volumeWorker一樣,claimWorker也在一個循環里不斷的獲取PVC,然后調用updateClaim方法進入到syncClaim進行具體的操作:

PersistentVolumeController#syncClaim

func (ctrl *PersistentVolumeController) syncClaim(claim *v1.PersistentVolumeClaim) error {
	klog.V(4).Infof("synchronizing PersistentVolumeClaim[%s]: %s", claimToClaimKey(claim), getClaimStatusForLogging(claim))
 
	newClaim, err := ctrl.updateClaimMigrationAnnotations(claim)
	if err != nil { 
		return err
	}
	claim = newClaim
	//根據當前對象中的注解決定調用邏輯
	if !metav1.HasAnnotation(claim.ObjectMeta, pvutil.AnnBindCompleted) {
		//處理未綁定的pvc
		return ctrl.syncUnboundClaim(claim)
	} else {
		//處理已經綁定的pvc
		return ctrl.syncBoundClaim(claim)
	}
}

這個方法會從緩存里面重新獲取一些PVC,然后根據PVC的注解決定調用邏輯。

下面我先從syncUnboundClaim開始,方法比較長,分成兩部分:

func (ctrl *PersistentVolumeController) syncUnboundClaim(claim *v1.PersistentVolumeClaim) error {
		//說明pvc處於pending狀態,沒有完成綁定操作
	if claim.Spec.VolumeName == "" {
		// User did not care which PV they get.
		// 是否是延遲綁定
		delayBinding, err := pvutil.IsDelayBindingMode(claim, ctrl.classLister)
		if err != nil {
			return err
		}

		// [Unit test set 1]
		//根據聲明的PVC設置的字段找到對應的PV
		volume, err := ctrl.volumes.findBestMatchForClaim(claim, delayBinding)
		if err != nil {
			klog.V(2).Infof("synchronizing unbound PersistentVolumeClaim[%s]: Error finding PV for claim: %v", claimToClaimKey(claim), err)
			return fmt.Errorf("Error finding PV for claim %q: %v", claimToClaimKey(claim), err)
		}
		//如果沒有可用volume情況
		if volume == nil {
			klog.V(4).Infof("synchronizing unbound PersistentVolumeClaim[%s]: no volume found", claimToClaimKey(claim)) 
			switch {
			case delayBinding && !pvutil.IsDelayBindingProvisioning(claim):
				if err = ctrl.emitEventForUnboundDelayBindingClaim(claim); err != nil {
					return err
				}
			//	找對應的storageclass
			case v1helper.GetPersistentVolumeClaimClass(claim) != "":
				//根據對應的插件創建PV
				if err = ctrl.provisionClaim(claim); err != nil {
					return err
				}
				return nil
			default:
				ctrl.eventRecorder.Event(claim, v1.EventTypeNormal, events.FailedBinding, "no persistent volumes available for this claim and no storage class is set")
			}
 
			// 等待下次循環再查找匹配的PV進行綁定
			if _, err = ctrl.updateClaimStatus(claim, v1.ClaimPending, nil); err != nil {
				return err
			}
			return nil
		//	找到volume,進行綁定操作
		} else /* pv != nil */ { 
			claimKey := claimToClaimKey(claim)
			klog.V(4).Infof("synchronizing unbound PersistentVolumeClaim[%s]: volume %q found: %s", claimKey, volume.Name, getVolumeStatusForLogging(volume))
			//執行綁定操作
			if err = ctrl.bind(volume, claim); err != nil { 
				metrics.RecordMetric(claimKey, &ctrl.operationTimestamps, err)
				return err
			} 
			metrics.RecordMetric(claimKey, &ctrl.operationTimestamps, nil)
			return nil
		}
	}
	...
}

這個方法首先會校驗VolumeName是否為空,如果為空,那么檢查一下是否設置了延遲綁定,想要知道延遲綁定是什么的,可以看這篇:https://izsk.me/2020/05/12/Kubernetes-why-local-PV-need-bound-delay/;

然后去PV集合里面查看是否能找到符合要求的PV,如果沒有可用的PV,那么看一下是否是dynamically provisioned,如果是的話異步創建PV后設置PVC狀態為Binding,然后等待下次循環再查找匹配的PV進行綁定;

如果找到相匹配的PV,那么調用bind方法執行綁定,bind方法就不貼出來了,里面會更新ClaimRef字段、status phase、VolumeName等。

接下來看看syncUnboundClaim下半部分代碼:

func (ctrl *PersistentVolumeController) syncUnboundClaim(claim *v1.PersistentVolumeClaim) error {
	...
	} else /* pvc.Spec.VolumeName != nil */ { 
		klog.V(4).Infof("synchronizing unbound PersistentVolumeClaim[%s]: volume %q requested", claimToClaimKey(claim), claim.Spec.VolumeName)
		//若VolumeName不為空,那么找到相應的PV
		obj, found, err := ctrl.volumes.store.GetByKey(claim.Spec.VolumeName)
		if err != nil {
			return err
		}
		//說明對應的PV已經不存在了,更新狀態為Pending
		if !found { 
			klog.V(4).Infof("synchronizing unbound PersistentVolumeClaim[%s]: volume %q requested and not found, will try again next time", claimToClaimKey(claim), claim.Spec.VolumeName)
			if _, err = ctrl.updateClaimStatus(claim, v1.ClaimPending, nil); err != nil {
				return err
			}
			return nil
		} else {
			volume, ok := obj.(*v1.PersistentVolume)
			if !ok {
				return fmt.Errorf("Cannot convert object from volume cache to volume %q!?: %+v", claim.Spec.VolumeName, obj)
			}
			klog.V(4).Infof("synchronizing unbound PersistentVolumeClaim[%s]: volume %q requested and found: %s", claimToClaimKey(claim), claim.Spec.VolumeName, getVolumeStatusForLogging(volume))
			//PV的ClaimRef字段為空,那么調用bind執行綁定操作
			if volume.Spec.ClaimRef == nil { 
				klog.V(4).Infof("synchronizing unbound PersistentVolumeClaim[%s]: volume is unbound, binding", claimToClaimKey(claim))
				if err = checkVolumeSatisfyClaim(volume, claim); err != nil {
					klog.V(4).Infof("Can't bind the claim to volume %q: %v", volume.Name, err) 
					msg := fmt.Sprintf("Cannot bind to requested volume %q: %s", volume.Name, err)
					ctrl.eventRecorder.Event(claim, v1.EventTypeWarning, events.VolumeMismatch, msg) 
					if _, err = ctrl.updateClaimStatus(claim, v1.ClaimPending, nil); err != nil {
						return err
					}
				} else if err = ctrl.bind(volume, claim); err != nil { 
					return err
				} 
				return nil
			//	這里主要校驗volume是否已綁定了別的PVC,如果沒有的話,執行綁定
			} else if pvutil.IsVolumeBoundToClaim(volume, claim) { 
				klog.V(4).Infof("synchronizing unbound PersistentVolumeClaim[%s]: volume already bound, finishing the binding", claimToClaimKey(claim))
 
				if err = ctrl.bind(volume, claim); err != nil {
					return err
				} 
				return nil
			} else {
				//這里是PV綁定了其他PVC,等待下次循環再重試
				...
			}
		}
	}
}

這里說明VolumeName不為空,那么自然需要取出對應的PV,如果對應的PV已經不存在了,那么等待下次調用再執行綁定;

如果找到對應的PV,那么如果ClaimRef字段為空,那么調用bind執行綁定操作;

如果ClaimRef不為空,那么調用IsVolumeBoundToClaim校驗一下PV是否已綁定了別的PVC,如果沒有的話,執行綁定

IsVolumeBoundToClaim

func IsVolumeBoundToClaim(volume *v1.PersistentVolume, claim *v1.PersistentVolumeClaim) bool {
	if volume.Spec.ClaimRef == nil {
		return false
	}
	if claim.Name != volume.Spec.ClaimRef.Name || claim.Namespace != volume.Spec.ClaimRef.Namespace {
		return false
	}
	if volume.Spec.ClaimRef.UID != "" && claim.UID != volume.Spec.ClaimRef.UID {
		return false
	}
	return true
}

我們可以看到這個方法主要是校驗相應字段是否相等,如果不相等則返回false,說明PV綁定了其他PVC,等待下次循環再重試。

下面我們看一下syncBoundClaim進行了什么操作:

func (ctrl *PersistentVolumeController) syncBoundClaim(claim *v1.PersistentVolumeClaim) error { 
	if claim.Spec.VolumeName == "" { 
		//這里說明以前被綁定過,但現在已經找不到對應的PV了,說明數據丟失,在變更狀態的同時,需要發出一個警告事件
		if _, err := ctrl.updateClaimStatusWithEvent(claim, v1.ClaimLost, nil, v1.EventTypeWarning, "ClaimLost", "Bound claim has lost reference to PersistentVolume. Data on the volume is lost!"); err != nil {
			return err
		}
		return nil
	}
	obj, found, err := ctrl.volumes.store.GetByKey(claim.Spec.VolumeName)
	if err != nil {
		return err
	}
	//綁定到不存在的pv情況
	if !found { 
		//這里說明以前被綁定過,但現在已經找不到對應的PV了,說明數據丟失,在變更狀態的同時,需要發出一個警告事件
		if _, err = ctrl.updateClaimStatusWithEvent(claim, v1.ClaimLost, nil, v1.EventTypeWarning, "ClaimLost", "Bound claim has lost its PersistentVolume. Data on the volume is lost!"); err != nil {
			return err
		}
		return nil
	//	存在pv情況
	} else {
		volume, ok := obj.(*v1.PersistentVolume)
		if !ok {
			return fmt.Errorf("Cannot convert object from volume cache to volume %q!?: %#v", claim.Spec.VolumeName, obj)
		}

		klog.V(4).Infof("synchronizing bound PersistentVolumeClaim[%s]: volume %q found: %s", claimToClaimKey(claim), claim.Spec.VolumeName, getVolumeStatusForLogging(volume))
		//更新綁定關系,這里說明PVC是綁定的,但是PV處於未綁定
		if volume.Spec.ClaimRef == nil { 
			klog.V(4).Infof("synchronizing bound PersistentVolumeClaim[%s]: volume is unbound, fixing", claimToClaimKey(claim))
			if err = ctrl.bind(volume, claim); err != nil {
				// Objects not saved, next syncPV or syncClaim will try again
				return err
			}
			return nil
		//	更新綁定關系
		} else if volume.Spec.ClaimRef.UID == claim.UID { 
			klog.V(4).Infof("synchronizing bound PersistentVolumeClaim[%s]: claim is already correctly bound", claimToClaimKey(claim))
			if err = ctrl.bind(volume, claim); err != nil { 
				return err
			}
			return nil
		} else { 
			//這里說明兩個PVC綁定到同一個PV上了
			if _, err = ctrl.updateClaimStatusWithEvent(claim, v1.ClaimLost, nil, v1.EventTypeWarning, "ClaimMisbound", "Two claims are bound to the same volume, this one is bound incorrectly"); err != nil {
				return err
			}
			return nil
		}
	}
}

這個方法主要是處理PVC已經綁定的各種異常情況,例如檢查VolumeName字段是否為空,檢查是否能找到對應的PV,檢查對應的PV是否已經綁定當前的PVC,檢查是否有多個PVC綁定到同一個PV上等等。

總結

這篇一開始通過一個例子講解了PV和PVC的使用,然后講解了動態綁定的過程,最后講解了PV和PVC一些基本概念。接下來我們通過源碼了解到了PV和PVC對應的處理流程,已經互相綁定的細節,不過這篇文章有些遺憾的地方是沒有講解volume 的attach和detach對應的AD controller是如何進行的,后面有機會補上。

References

https://draveness.me/kubernetes-volume/

https://kubernetes.io/docs/concepts/storage/persistent-volumes/

https://here2say.com/45/

https://izsk.me/2020/05/12/Kubernetes-why-local-PV-need-bound-delay/

《k8s in Action》

《深入理解k8s》


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM