簡介: 衆所周知,K8s 的持久化存儲(Persistent Storage)保證了應用數據獨立於應用生命週期而存在,但其內部實現卻少有人說起。K8s 內部的存儲流程究竟是怎樣的?PV、PVC、StorageClass、Kubelet、CSI 插件等之間的調用關係又如何,這些謎底將在本文中一一揭曉。node
做者 | 孫志恆(惠志) 阿里巴巴開發工程師nginx
導讀:衆所周知,K8s 的持久化存儲(Persistent Storage)保證了應用數據獨立於應用生命週期而存在,但其內部實現卻少有人說起。K8s 內部的存儲流程究竟是怎樣的?PV、PVC、StorageClass、Kubelet、CSI 插件等之間的調用關係又如何,這些謎底將在本文中一一揭曉。
K8s 持久化存儲基礎
在進行 K8s 存儲流程講解以前,先回顧一下 K8s 中持久化存儲的基礎概念。api
1. 名詞解釋
- in-tree:代碼邏輯在 K8s 官方倉庫中;
- out-of-tree:代碼邏輯在 K8s 官方倉庫以外,實現與 K8s 代碼的解耦;
- PV:PersistentVolume,集羣級別的資源,由 集羣管理員 or External Provisioner 建立。PV 的生命週期獨立於使用 PV 的 Pod,PV 的 .Spec 中保存了存儲設備的詳細信息;
- PVC:PersistentVolumeClaim,命名空間(namespace)級別的資源,由 用戶 or StatefulSet 控制器(根據VolumeClaimTemplate) 建立。PVC 相似於 Pod,Pod 消耗 Node 資源,PVC 消耗 PV 資源。Pod 能夠請求特定級別的資源(CPU 和內存),而 PVC 能夠請求特定存儲卷的大小及訪問模式(Access Mode);
- StorageClass:StorageClass 是集羣級別的資源,由集羣管理員建立。SC 爲管理員提供了一種動態提供存儲卷的「類」模板,SC 中的 .Spec 中詳細定義了存儲卷 PV 的不一樣服務質量級別、備份策略等等;
- CSI:Container Storage Interface,目的是定義行業標準的「容器存儲接口」,使存儲供應商(SP)基於 CSI 標準開發的插件能夠在不一樣容器編排(CO)系統中工做,CO 系統包括 Kubernetes、Mesos、Swarm 等。
2. 組件介紹
- PV Controller:負責 PV/PVC 綁定及週期管理,根據需求進行數據卷的 Provision/Delete 操做;
- AD Controller:負責數據卷的 Attach/Detach 操做,將設備掛接到目標節點;
- Kubelet:Kubelet 是在每一個 Node 節點上運行的主要 「節點代理」,功能是 Pod 生命週期管理、容器健康檢查、容器監控等;
- Volume Manager:Kubelet 中的組件,負責管理數據卷的 Mount/Umount 操做(也負責數據卷的 Attach/Detach 操做,需配置 kubelet 相關參數開啓該特性)、卷設備的格式化等等;
- Volume Plugins:存儲插件,由存儲供應商開發,目的在於擴展各類存儲類型的卷管理能力,實現第三方存儲的各類操做能力,便是上面藍色操做的實現。Volume Plugins 有 in-tree 和 out-of-tree 兩種;
- External Provioner:External Provioner 是一種 sidecar 容器,做用是調用 Volume Plugins 中的 CreateVolume 和 DeleteVolume 函數來執行 Provision/Delete 操做。由於 K8s 的 PV 控制器沒法直接調用 Volume Plugins 的相關函數,故由 External Provioner 經過 gRPC 來調用;
- External Attacher:External Attacher 是一種 sidecar 容器,做用是調用 Volume Plugins 中的 ControllerPublishVolume 和 ControllerUnpublishVolume 函數來執行 Attach/Detach 操做。由於 K8s 的 AD 控制器沒法直接調用 Volume Plugins 的相關函數,故由 External Attacher 經過 gRPC 來調用。
3. 持久卷使用
Kubernetes 爲了使應用程序及其開發人員可以正常請求存儲資源,避免處理存儲設施細節,引入了 PV 和 PVC。建立 PV 有兩種方式:ide
- 一種是集羣管理員經過手動方式靜態建立應用所須要的 PV;
- 另外一種是用戶手動建立 PVC 並由 Provisioner 組件動態建立對應的 PV。
下面咱們以 NFS 共享存儲爲例來看兩者區別。函數
靜態建立存儲卷
靜態建立存儲卷流程以下圖所示:ui
第一步:集羣管理員建立 NFS PV,NFS 屬於 K8s 原生支持的 in-tree 存儲類型。yaml 文件以下:spa
apiVersion: v1kind: PersistentVolumemetadata:
name: nfs-pvspec:
capacity:
storage: 10Gi
accessModes:
- ReadWriteOnce
persistentVolumeReclaimPolicy: Retain
nfs:
server: 192.168.4.1
path: /nfs_storage 插件
第二步:用戶建立 PVC,yaml 文件以下:3d
apiVersion: v1kind: PersistentVolumeClaimmetadata:
name: nfs-pvcspec:
accessModes:
- ReadWriteOnce
resources:
requests:
storage: 10Gi 代理
經過 kubectl get pv 命令可看到 PV 和 PVC 已綁定:
[root@huizhi ~]# kubectl get pvcNAME STATUS VOLUME CAPACITY ACCESS MODES STORAGECLASS AGE
nfs-pvc Bound nfs-pv-no-affinity 10Gi RWO 4s
第三步:用戶建立應用,並使用第二步建立的 PVC。
apiVersion: v1kind: Podmetadata:
name: test-nfsspec:
containers:
- image: nginx:alpine
imagePullPolicy: IfNotPresent
name: nginx
volumeMounts:
- mountPath: /data
name: nfs-volume
volumes:
- name: nfs-volume
persistentVolumeClaim:
claimName: nfs-pvc
此時 NFS 的遠端存儲就掛載了到 Pod 中 nginx 容器的 /data 目錄下。
動態建立存儲卷
動態建立存儲卷,要求集羣中部署有 nfs-client-provisioner 以及對應的 storageclass。
動態建立存儲卷相比靜態建立存儲卷,少了集羣管理員的干預,流程以下圖所示:
集羣管理員只須要保證環境中有 NFS 相關的 storageclass 便可:
kind: StorageClassapiVersion: storage.k8s.io/v1metadata:
name: nfs-scprovisioner: example.com/nfsmountOptions:
- vers=4.1
第一步:用戶建立 PVC,此處 PVC 的 storageClassName 指定爲上面 NFS 的 storageclass 名稱:
kind: PersistentVolumeClaimapiVersion: v1metadata:
name: nfs
annotations:
volume.beta.kubernetes.io/storage-class: "example-nfs"spec:
accessModes:
- ReadWriteMany
resources:
requests:
storage: 10Mi
storageClassName: nfs-sc
第二步:集羣中的 nfs-client-provisioner 會動態建立相應 PV。此時可看到環境中 PV 已建立,並與 PVC 已綁定。
[root@huizhi ~]# kubectl get pvNAME CAPACITY ACCESSMODES RECLAIMPOLICY STATUS CLAIM REASON AGE
pvc-dce84888-7a9d-11e6-b1ee-5254001e0c1b 10Mi RWX Delete Bound default/nfs 4s
第三步:用戶建立應用,並使用第二步建立的 PVC,同靜態建立存儲卷的第三步。
K8s 持久化存儲流程
1. 流程概覽
此處借鑑 @郡寶 在雲原生存儲課程中的流程圖
流程以下:
- 用戶建立了一個包含 PVC 的 Pod,該 PVC 要求使用動態存儲卷;
- Scheduler 根據 Pod 配置、節點狀態、PV 配置等信息,把 Pod 調度到一個合適的 Worker 節點上;
- PV 控制器 watch 到該 Pod 使用的 PVC 處於 Pending 狀態,因而調用 Volume Plugin(in-tree)建立存儲卷,並建立 PV 對象(out-of-tree 由 External Provisioner 來處理);
- AD 控制器發現 Pod 和 PVC 處於待掛接狀態,因而調用 Volume Plugin 掛接存儲設備到目標 Worker 節點上
- 在 Worker 節點上,Kubelet 中的 Volume Manager 等待存儲設備掛接完成,並經過 Volume Plugin 將設備掛載到全局目錄:**/var/lib/kubelet/pods/[pod uid]/volumes/kubernetes.io~iscsi/[PV
name]**(以 iscsi 爲例);
- Kubelet 經過 Docker 啓動 Pod 的 Containers,用 bind mount 方式將已掛載到本地全局目錄的卷映射到容器中。
更詳細的流程以下:
2. 流程詳解
不一樣 K8s 版本,持久化存儲流程略有區別。本文基於 Kubernetes 1.14.8 版本。
從上述流程圖中可看到,存儲卷從建立到提供應用使用共分爲三個階段:Provision/Delete、Attach/Detach、Mount/Unmount。
provisioning volumes
PV 控制器中有兩個 Worker:
- ClaimWorker:處理 PVC 的 add / update / delete 相關事件以及 PVC 的狀態遷移;
- VolumeWorker:負責 PV 的狀態遷移。
PV 狀態遷移(UpdatePVStatus):
- PV 初始狀態爲 Available,當 PV 與 PVC 綁定後,狀態變爲 Bound;
- 與 PV 綁定的 PVC 刪除後,狀態變爲 Released;
- 當 PV 回收策略爲 Recycled 或手動刪除 PV 的 .Spec.ClaimRef 後,PV 狀態變爲 Available;
- 當 PV 回收策略未知或 Recycle 失敗或存儲卷刪除失敗,PV 狀態變爲 Failed;
- 手動刪除 PV 的 .Spec.ClaimRef,PV 狀態變爲 Available。
PVC 狀態遷移(UpdatePVCStatus):
- 當集羣中不存在知足 PVC 條件的 PV 時,PVC 狀態爲 Pending。在 PV 與 PVC 綁定後,PVC 狀態由 Pending 變爲 Bound;
- 與 PVC 綁定的 PV 在環境中被刪除,PVC 狀態變爲 Lost;
- 再次與一個同名 PV 綁定後,PVC 狀態變爲 Bound。
Provisioning 流程以下(此處模擬用戶建立一個新 PVC):
靜態存儲卷流程(FindBestMatch):PV 控制器首先在環境中篩選一個狀態爲 Available 的 PV 與新 PVC匹配。
- DelayBinding:PV 控制器判斷該 PVC 是否須要延遲綁定:1. 查看 PVC 的 annotation 中是否包含volume.kubernetes.io/selected-node,若存在則表示該 PVC 已經被調度器指定好了節點(屬於 ProvisionVolume),故不須要延遲綁定;2. 若 PVC 的 annotation 中不存在 volume.kubernetes.io/selected-node,同時沒有 StorageClass,默認表示不須要延遲綁定;如有 StorageClass,查看其 VolumeBindingMode 字段,若爲 WaitForFirstConsumer 則須要延遲綁定,若爲 Immediate 則不須要延遲綁定;
- FindBestMatchPVForClaim:PV 控制器嘗試找一個知足 PVC 要求的環境中現有的 PV。PV 控制器會將全部的 PV 進行一次篩選,並會從知足條件的 PV 中選擇一個最佳匹配的PV。篩選規則:1. VolumeMode 是否匹配;2. PV 是否已綁定到 PVC 上;3. PV 的 .Status.Phase 是否爲 Available;4. LabelSelector 檢查,PV 與 PVC 的 label 要保持一致;5. PV 與 PVC 的 StorageClass 是否一致;6. 每次迭代更新最小知足 PVC requested size 的 PV,並做爲最終結果返回;
- Bind:PV 控制器對選中的 PV、PVC 進行綁定:1. 更新 PV 的 .Spec.ClaimRef 信息爲當前 PVC;2. 更新 PV 的 .Status.Phase 爲 Bound;3. 新增 PV 的 annotation : pv.kubernetes.io/bound-by-controller: "yes";4. 更新 PVC 的 .Spec.VolumeName 爲 PV 名稱;5. 更新 PVC 的 .Status.Phase 爲 Bound;6. 新增 PVC 的 annotation:pv.kubernetes.io/bound-by-controller: "yes" 和 pv.kubernetes.io/bind-completed: "yes";
動態存儲卷流程(ProvisionVolume):若環境中沒有合適的 PV,則進入動態 Provisioning 場景:
- Before Provisioning:1. PV 控制器首先判斷 PVC 使用的 StorageClass 是 in-tree 仍是 out-of-tree:經過查看 StorageClass 的 Provisioner 字段是否包含 "kubernetes.io/" 前綴來判斷;2. PV 控制器更新 PVC 的 annotation:claim.Annotations["volume.beta.kubernetes.io/storage-provisioner"] = storageClass.Provisioner;
- in-tree Provisioning(internal provisioning):1. in-tree 的 Provioner 會實現 ProvisionableVolumePlugin 接口的 NewProvisioner 方法,用來返回一個新的 Provisioner;2. PV 控制器調用 Provisioner 的 Provision 函數,該函數會返回一個 PV 對象;3. PV 控制器建立上一步返回的 PV 對象,將其與 PVC 綁定,Spec.ClaimRef 設置爲 PVC,.Status.Phase 設置爲 Bound,.Spec.StorageClassName 設置爲與 PVC 相同的 StorageClassName;同時新增 annotation:"pv.kubernetes.io/bound-by-controller"="yes" 和 "pv.kubernetes.io/provisioned-by"=plugin.GetPluginName();
- out-of-tree Provisioning(external provisioning):1. External Provisioner 檢查 PVC 中的 claim.Spec.VolumeName 是否爲空,不爲空則直接跳過該 PVC;2. External Provisioner 檢查 PVC 中的 claim.Annotations["volume.beta.kubernetes.io/storage-provisioner"] 是否等於本身的 Provisioner Name(External Provisioner 在啓動時會傳入--provisioner 參數來肯定本身的 Provisioner Name);3. 若 PVC 的 VolumeMode=Block,檢查 External Provisioner 是否支持塊設備;4. External Provisioner 調用 Provision 函數:經過 gRPC 調用 CSI 存儲插件的 CreateVolume 接口;5. External Provisioner 建立一個 PV 來表明該 volume,同時將該 PV 與以前的 PVC 作綁定。
deleting volumes
Deleting 流程爲 Provisioning 的反操做:
用戶刪除 PVC,刪除 PV 控制器改變 PV.Status.Phase 爲 Released。
當 PV.Status.Phase == Released 時,PV 控制器首先檢查 Spec.PersistentVolumeReclaimPolicy 的值,爲 Retain 時直接跳過,爲 Delete 時:
- in-tree Deleting:1. in-tree 的 Provioner 會實現 DeletableVolumePlugin 接口的 NewDeleter 方法,用來返回一個新的 Deleter;2. 控制器調用 Deleter 的 Delete 函數,刪除對應 volume;3. 在 volume 刪除後,PV 控制器會刪除 PV 對象;
- out-of-tree Deleting:1. External Provisioner 調用 Delete 函數,經過 gRPC 調用 CSI 插件的 DeleteVolume 接口;2. 在 volume 刪除後,External Provisioner 會刪除 PV 對象
Attaching Volumes
Kubelet 組件和 AD 控制器均可以作 attach/detach 操做,若 Kubelet 的啓動參數中指定了--enable-controller-attach-detach,則由 Kubelet 來作;不然默認由 AD 控制起來作。下面以 AD 控制器爲例來說解 attach/detach 操做。
AD 控制器中有兩個核心變量:
- DesiredStateOfWorld(DSW):集羣中預期的數據卷掛接狀態,包含了 nodes->volumes->pods 的信息;
- ActualStateOfWorld(ASW):集羣中實際的數據卷掛接狀態,包含了 volumes->nodes 的信息。
Attaching 流程以下:
AD 控制器根據集羣中的資源信息,初始化 DSW 和 ASW。
AD 控制器內部有三個組件週期性更新 DSW 和 ASW:
- Reconciler。經過一個 GoRoutine 週期性運行,確保 volume 掛接/摘除完畢。此期間不斷更新 ASW:
in-tree attaching:1. in-tree 的 Attacher 會實現 AttachableVolumePlugin 接口的 NewAttacher 方法,用來返回一個新的 Attacher;2. AD 控制器調用 Attacher 的 Attach 函數進行設備掛接;3. 更新 ASW。
out-of-tree attaching:1. 調用 in-tree 的 CSIAttacher 建立一個 VolumeAttachement(VA)對象,該對象包含了 Attacher 信息、節點名稱、待掛接 PV 信息;2. External Attacher 會 watch 集羣中的 VolumeAttachement 資源,發現有須要掛接的數據卷時,調用 Attach 函數,經過 gRPC 調用 CSI 插件的 ControllerPublishVolume 接口。
- DesiredStateOfWorldPopulator。經過一個 GoRoutine 週期性運行,主要功能是更新 DSW:
findAndRemoveDeletedPods - 遍歷全部 DSW 中的 Pods,若其已從集羣中刪除則從 DSW 中移除;
findAndAddActivePods - 遍歷全部 PodLister 中的 Pods,若 DSW 中不存在該 Pod 則添加至 DSW。
- PVC Worker。watch PVC 的 add/update 事件,處理 PVC 相關的 Pod,並實時更新 DSW。
Detaching Volumes
Detaching 流程以下:
- 當 Pod 被刪除,AD 控制器會 watch 到該事件。首先 AD 控制器檢查 Pod 所在的 Node 資源是否包含"volumes.kubernetes.io/keep-terminated-pod-volumes"標籤,若包含則不作操做;不包含則從 DSW 中去掉該 volume;
- AD 控制器經過 Reconciler 使 ActualStateOfWorld 狀態向 DesiredStateOfWorld 狀態靠近,當發現 ASW 中有 DSW 中不存在的 volume 時,會作 Detach 操做:
in-tree detaching:1. AD 控制器會實現 AttachableVolumePlugin 接口的 NewDetacher 方法,用來返回一個新的 Detacher;2. 控制器調用 Detacher 的 Detach 函數,detach 對應 volume;3. AD 控制器更新 ASW。
out-of-tree detaching:1. AD 控制器調用 in-tree 的 CSIAttacher 刪除相關 VolumeAttachement 對象;2. External Attacher 會 watch 集羣中的 VolumeAttachement(VA)資源,發現有須要摘除的數據卷時,調用 Detach 函數,經過 gRPC 調用 CSI 插件的 ControllerUnpublishVolume 接口;3. AD 控制器更新 ASW。
Volume Manager 中一樣也有兩個核心變量:
- DesiredStateOfWorld(DSW):集羣中預期的數據卷掛載狀態,包含了 volumes->pods 的信息;
- ActualStateOfWorld(ASW):集羣中實際的數據卷掛載狀態,包含了 volumes->pods 的信息。
Mounting/UnMounting 流程以下:
全局目錄(global mount path)存在的目的:塊設備在 Linux 上只能掛載一次,而在 K8s 場景中,一個 PV 可能被掛載到同一個 Node 上的多個 Pod 實例中。若塊設備格式化後先掛載至 Node 上的一個臨時全局目錄,而後再使用 Linux 中的 bind mount 技術把這個全局目錄掛載進 Pod 中對應的目錄上,就能夠知足要求。上述流程圖中,全局目錄即 /var/lib/kubelet/pods/[pod uid]/volumes/kubernetes.io~iscsi/[PV
name]
VolumeManager 根據集羣中的資源信息,初始化 DSW 和 ASW。
VolumeManager 內部有兩個組件週期性更新 DSW 和 ASW:
- DesiredStateOfWorldPopulator:經過一個 GoRoutine 週期性運行,主要功能是更新 DSW;
- Reconciler:經過一個 GoRoutine 週期性運行,確保 volume 掛載/卸載完畢。此期間不斷更新 ASW:
unmountVolumes:確保 Pod 刪除後 volumes 被 unmount。遍歷一遍全部 ASW 中的 Pod,若其不在 DSW 中(表示 Pod 被刪除),此處以 VolumeMode=FileSystem 舉例,則執行以下操做:
- Remove all bind-mounts:調用 Unmounter 的 TearDown 接口(若爲 out-of-tree 則調用 CSI 插件的 NodeUnpublishVolume 接口);
- Unmount volume:調用 DeviceUnmounter 的 UnmountDevice 函數(若爲 out-of-tree 則調用 CSI 插件的 NodeUnstageVolume 接口);
- 更新 ASW。
mountAttachVolumes:確保 Pod 要使用的 volumes 掛載成功。遍歷一遍全部 DSW 中的 Pod,若其不在 ASW 中(表示目錄待掛載映射到 Pod 上),此處以 VolumeMode=FileSystem 舉例,執行以下操做:
- 等待 volume 掛接到節點上(由 External Attacher or Kubelet 自己掛接);
- 掛載 volume 到全局目錄:調用 DeviceMounter 的 MountDevice 函數(若爲 out-of-tree 則調用 CSI 插件的 NodeStageVolume 接口);
- 更新 ASW:該 volume 已掛載到全局目錄;
- bind-mount volume 到 Pod 上:調用 Mounter 的 SetUp 接口(若爲 out-of-tree 則調用 CSI 插件的 NodePublishVolume 接口);
- 更新 ASW。
unmountDetachDevices:確保須要 unmount 的 volumes 被 unmount。遍歷一遍全部 ASW 中的 UnmountedVolumes,若其不在 DSW 中(表示 volume 已無需使用),執行以下操做:
- Unmount volume:調用 DeviceUnmounter 的 UnmountDevice 函數(若爲 out-of-tree 則調用 CSI 插件的NodeUnstageVolume接口);
- 更新 ASW。
總結
本文先對 K8s 持久化存儲基礎概念及使用方法進行了介紹,並對 K8s 內部存儲流程進行了深度解析。在 K8s 上,使用任何一種存儲都離不開上面的流程(有些場景不會用到 attach/detach),環境上的存儲問題也必定是其中某個環節出現了故障。
容器存儲的坑比較多,專有云環境下尤爲如此。不過挑戰越多,機遇也越多!目前國內專有云市場在存儲領域也是羣雄逐鹿,咱們敏捷 PaaS 容器團隊歡迎大俠的加入,一塊兒共創將來!