PV、PVC、StorageClass講解
為了方便開發人員更加容易的使用儲存才出現的概念。通常我們在一個POD中定義使用儲存是這樣的方式,我們以hostpath型別來說:
apiVersion: v1
kind: Pod
metadata:
name: mypod
spec:
containers:
- image: nginx
name: mynginx
volumeMounts:
- mountPath: /usr/share/nginx/html
name: html
volumes:
- name: html # 名稱
hostPath: # 儲存型別
path: /data # 物理節點上的真實路徑
type: Directory # 如果該路徑不存在講如何處理,Directory是要求目錄必須存在
其實透過上面可以看出來,無論你使用什麼型別的儲存你都需要手動定義,指明儲存型別以及相關配置。這裡的hostpath型別還是比較簡單的,如果是其他型別的比如分散式儲存,那麼這對開發人員來說將會是一種挑戰,因為畢竟真正的儲存是由儲存管理員來設定的他們會更加了解,那麼有沒有一種方式讓我們使用儲存更加容易,對上層使用人員遮蔽底層細節呢?答案是肯定的,這就是PV、PVC的概念。不過需要注意的是我們在叢集中通常不使用hostPath、emptyDir這種型別,除非你只是測試使用。
什麼是PV
PV全稱叫做Persistent Volume,持久化儲存卷。它是用來描述或者說用來定義一個儲存卷的,這個通常都是有運維或者資料儲存工程師來定義。比如下面我們定義一個NFS型別的PV:
apiVersion: v1
kind: PersistentVolume
metadata: # PV建立不要加名稱空間,因為PV屬於叢集級別的
name: nfs-pv001 # PV名稱
labels: # 這些labels可以不定義
name: nfs-pv001
storetype: nfs
spec: # 這裡的spec和volumes裡面的一樣
storageClassName: normal
accessModes: # 設定訪問模型
- ReadWriteMany
- ReadWriteOnce
- ReadOnlyMany
capacity: # 設定儲存空間大小
storage: 500Mi
persistentVolumeReclaimPolicy: Retain # 回收策略
nfs:
path: /work/volumes/v1
server: stroagesrv01.contoso.com
accessModes:支援三種型別
-
ReadWriteMany 多路讀寫,卷能被叢集多個節點掛載並讀寫
-
ReadWriteOnce 單路讀寫,卷只能被單一叢集節點掛載讀寫
-
ReadOnlyMany 多路只讀,卷能被多個叢集節點掛載且只能讀
這裡的訪問模型總共有三種,但是不同的儲存型別支援的訪問模型不同,具體支援什麼需要查詢官網。比如我們這裡使用nfs,它支援全部三種。但是ISCI就不支援ReadWriteMany;HostPath就不支援ReadOnlyMany和ReadWriteMany。
persistentVolumeReclaimPolicy:也有三種策略,這個策略是當與之關聯的PVC被刪除以後,這個PV中的資料如何被處理
-
Retain 當刪除與之繫結的PVC時候,這個PV被標記為released(PVC與PV解綁但還沒有執行回收策略)且之前的資料依然儲存在該PV上,但是該PV不可用,需要手動來處理這些資料並刪除該PV。
-
Delete 當刪除與之繫結的PVC時候
-
Recycle 這個在1.14版本中以及被廢棄,取而代之的是推薦使用動態儲存供給策略,它的功能是當刪除與該PV關聯的PVC時,自動刪除該PV中的所有資料
注意:PV必須先與POD建立,而且只能是網路儲存不能屬於任何Node,雖然它支援HostPath型別但由於你不知道POD會被排程到哪個Node上,所以你要定義HostPath型別的PV就要保證所有節點都要有HostPath中指定的路徑。
PVC
PVC是用來描述希望使用什麼樣的或者說是滿足什麼條件的儲存,它的全稱是Persistent Volume Claim,也就是持久化儲存宣告。開發人員使用這個來描述該容器需要一個什麼儲存。比如下面使用NFS的PVC:
apiVersion: v1
kind: PersistentVolumeClaim
metadata:
name: nfs-pvc001
namespace: default
labels: # 這些labels可以不定義
name: nfs-pvc001
storetype: nfs
capacity: 500Mi
spec:
storageClassName: normal
accessModes: # PVC也需要定義訪問模式,不過它的模式一定是和現有PV相同或者是它的子集,否則匹配不到PV
- ReadWriteMany
resources: # 定義資源要求PV滿足這個PVC的要求才會被匹配到
requests:
storage: 500Mi # 定義要求有多大空間
這個PVC就會和上面的PV進行繫結,為什麼呢?它有一些原則:
-
PV和PVC中的spec關鍵欄位要匹配,比如儲存(storage)大小。
-
PV和PVC中的storageClassName欄位必須一致,這個後面再說。
上面的labels中的標籤只是增加一些描述,對於PVC和PV的繫結沒有關係
應用了上面的PV和PVC,可以看到自動繫結了。
在POD中如何使用PVC呢
apiVersion: apps/v1
kind: Deployment
metadata:
name: tomcat-deploy
spec:
replicas: 1
selector:
matchLabels:
appname: myapp
template:
metadata:
name: myapp
labels:
appname: myapp
spec:
containers:
- name: myapp
image: tomcat:8.5.38-jre8
ports:
- name: http
containerPort: 8080
protocol: TCP
volumeMounts:
- name: tomcatedata
mountPath : "/data"
volumes:
- name: tomcatedata
persistentVolumeClaim:
claimName: nfs-pvc001
這裡透過volumes來宣告使用哪個PVC,可以看到和自己定義持久化卷類似,但是這裡更加簡單了,直接使用PVC的名字即可。在容器中使用/data目錄就會把資料寫入到NFS伺服器上的目錄中。
當我們刪除那個PVC的時候,該PV變成Released狀態,由於我們的策略是Retain,所以如果想讓這個PV變為可用我們就需要手動清理資料並刪除這個PV。這裡你可能會覺得矛盾,你讓這個PV變為可用,為什麼還要刪除這個PV呢?其實所謂可用就是刪除這個PV然後建立一個同名的。
可以看出來PVC就相當於是容器和PV之間的一個介面,使用人員只需要和PVC打交道即可。另外你可能也會想到如果當前環境中沒有合適的PV和我的PVC繫結,那麼我建立的POD不就失敗了麼?的確是這樣的,不過如果發現這個問題,那麼就趕快建立一個合適的PV,那麼這時候持久化儲存迴圈控制器會不斷的檢查PVC和PV,當發現有合適的可以繫結之後它會自動給你繫結上然後被掛起的POD就會自動啟動,而不需要你重建POD。
什麼是持久化儲存
我們知道所謂容器掛載卷就是將宿主機的目錄掛載到容器中的某個目錄。而持久化則意味著這個目錄裡面的內容不會因為容器被刪除而清除,也不會和當前宿主機有什麼直接關係,而是一個外部的。這樣當POD重建以後或者在其他主機節點上啟動後依然可以訪問這些內容。不過之前說過hostPath和emptyDir則推薦使用,因為前者和當前宿主機有必然聯絡而後者就是一個隨POD刪除而被刪除的臨時目錄。
宿主機是如何掛載遠端目錄的
掛載過程會有不同,這取決於遠端儲存的型別,它是塊裝置儲存還是檔案裝置儲存。但是不管怎麼樣POD有這樣一個目錄/var/lib/kubelet/pods/<Pod 的 ID>/volumes/kubernetes.io~<Volume 型別 >/<Volume 名字 >
這個目錄是POD被排程到該節點之後,由kubelet為POD建立的。因為它一定會被建立,因為系統中的預設secret就會被掛載到這裡。之後就要根據儲存裝置型別的不同做不同處理。
檔案儲存裝置
以nfs這種檔案裝置儲存來說。我們依然啟動之前的容器繼續使用之前的PVC。
由於這個POD執行在node01節點,我們登陸node01節點,檢視這個目錄
/var/lib/kubelet/pods/<Pod 的 ID>/volumes/kubernetes.io~<Volume 型別 >/<Volume 名字 >
當你建立POD的時候它由於它被排程到node01節點,所以會建立這個目錄,而且根據YAML中的定義就也會在這個目錄中建立你在volumesMount中定義的目錄,如下圖:
透過命令檢視在本地宿主機的掛載情況
由於建立了必要的目錄,那麼kubelet就直接使用mount命令把nfs目錄掛載到這個目錄上volumes/kubernetes.io~<type>/<Volume 名字>
,注意這時候僅僅是把這個遠端儲存掛載到宿主機目錄上,要想讓容器使用還需要做呼叫相關介面來把這個宿主機上的目錄掛載到容器上。所以當準備好之後啟動容器的時候就是利用CRI裡的mounts引數把這個宿主機的目錄掛載到容器中指定的目錄上,就相當於執行docker run -v
。
不過需要注意的是由於nfs檔案儲存不是一個塊裝置,所以宿主機系統需要扮演的就是nfs客戶端角色,kubelet就是呼叫這個客戶端工具來完成掛載的。
塊儲存裝置
塊儲存裝置你可以理解為一個磁碟。這個的處理要稍微複雜一點,就好像你為Linux伺服器新增一塊磁碟一樣,你得先安裝然後分割槽格式化之後掛載到某個目錄使用。
/var/lib/kubelet/pods/<Pod 的 ID>/volumes/kubernetes.io~<Volume 型別 >/<Volume 名字 >
這個目錄依然會建立。當POD被排程到該節點上會做如下操作
-
首先要安裝一個塊裝置儲存到宿主機(不是物理安裝,而是透過API來安裝),如何安裝取決於不同塊儲存裝置的API,很多雲廠商有這種塊儲存裝置比如Google的GCE。
-
格式化磁碟,
-
把格式化好的磁碟裝置掛載到宿主機上的目錄
-
啟動容器掛載宿主機上的目錄到容器中
相對於檔案裝置儲存來說塊裝置要稍微複雜一點,不過上面這些過程都是自動的有kubelet來完成。
小結
負責把PVC繫結到PV的是一個持久化儲存卷控制迴圈,這個控制器也是kube-manager-controller的一部分執行在master上。而真正把目錄掛載到容器上的操作是在POD所在主機上發生的,所以透過kubelet來完成。而且建立PV以及PVC的繫結是在POD被排程到某一節點之後進行的,完成這些操作,POD就可以執行了。下面梳理一下掛載一個PV的過程:
-
使用者提交一個包含PVC的POD
-
排程器把根據各種排程演算法把該POD分配到某個節點,比如node01
-
Node01上的kubelet等待Volume Manager準備儲存裝置
-
PV控制器呼叫儲存外掛建立PV並與PVC進行繫結
-
Attach/Detach Controller或Volume Manager透過儲存外掛實現裝置的attach。(這一步是針對塊裝置儲存)
-
Volume Manager等待儲存裝置變為可用後,掛載該裝置到
/var/lib/kubelet/pods/<Pod 的 ID>/volumes/kubernetes.io~<Volume 型別 >/<Volume 名字 >
目錄上 -
Kubelet被告知卷已經準備好,開始啟動POD,透過對映方式掛載到容器中
StorageClass
PV是運維人員來建立的,開發操作PVC,可是大規模叢集中可能會有很多PV,如果這些PV都需要運維手動來處理這也是一件很繁瑣的事情,所以就有了動態供給概念,也就是Dynamic Provisioning。而我們上面的建立的PV都是靜態供給方式,也就是Static Provisioning。而動態供給的關鍵就是StorageClass,它的作用就是建立PV模板。
建立StorageClass裡面需要定義PV屬性比如儲存型別、大小等;另外建立這種PV需要用到儲存外掛。最終效果是,使用者提交PVC,裡面指定儲存型別,如果符合我們定義的StorageClass,則會為其自動建立PV並進行繫結。
我們這裡演示一下NFS的動態PV建立
kubernetes本身支援的動態PV建立不包括nfs,所以需要使用額外外掛實現。nfs-client
我這裡就按照網站的例子來建立,裡面的內容毫無修改,當然你需要自己準備NFS伺服器。由於用於提供動態建立PV的程式是執行在POD中,所以你需要保證你的Kubernetes節點到NFS的網路通暢,我這裡就在我的Kubernetes叢集的某個節點上建立的NFS服務。下面是PVC檔案
apiVersion: v1
kind: PersistentVolumeClaim
metadata:
name: mytomcat-pvc
spec:
storageClassName: managed-nfs-storage
accessModes:
- ReadWriteMany
resources:
requests:
storage: 500Mi
當你應用這個PVC的時候,由於例子中的storageClassName也是managed-nfs-storage(當然這個名字你可以修改)就會去自動建立PV。
下圖是在Node02這個節點上看到的
基於這種形式,我們只需要根據我們有的儲存系統來定義StorageClass,透過名稱來標識不同種類的儲存,比如SSD、block-device這種名稱,而不需要定義具體大小。那麼使用人員就可以根據需要透過StorageClass的名字來使用,從而實現動態建立PV的過程。
這裡有個要求就是你的儲存系統需要提供某種介面來讓controller可以呼叫並傳遞進去PVC的引數去建立PV,很多雲端儲存都支援。可是也有不支援的,比如NFS就不支援所以我們需要一個單獨的外掛來完成這個工作。也就是例子中使用的quay.io/external_storage/nfs-client-provisioner
映象,但是建立PV也需要相關許可權,也就是例子中rabc.yaml部分。在定義StorageClass中有一個叫做provisioner: fuseim.pri/ifs
這個就是外掛的名稱,這個名稱其實也就是官方例子中deployment中設定的名字,這個名字你可以修改。
當然我們說過有些本身就支援,比如下面的kubernetes官網中的一個AWS的例子:
kind: StorageClass
apiVersion: storage.k8s.io/v1
metadata:
name: slow
provisioner: kubernetes.io/aws-ebs
parameters:
type: io1
iopsPerGB: "10"
fsType: ext4
kubernetes.io/aws-ebs
就是kubernetes內建的儲存外掛名稱,如果你使用aws就用這個名稱就好。因為kubernetes就會去呼叫AWS的API來建立儲存然後在建立PV。
這裡你可能會有個疑問,為什麼開篇的例子裡面也用了storageClassName: normal
,可是我們並沒有定義任何StorageClass。其實雖然我們使用了,但是系統上並沒有一個叫做normal的儲存類,這時候還是靜態繫結,只是繫結的時候它會考慮你的PV和PVC中的儲存類名稱是否一致。當然如果是靜態繫結你可以不寫storageClassName
,因為如果開起一個的叫做DefaultStorageClass
plugin外掛就會預設有這樣一個儲存類,它會自動新增到你的任何沒有明確宣告storageClassName
的PV和PVC中。
本地持久化儲存
本地持久化儲存(Local Persistent Volume)就是把資料儲存在POD執行的宿主機上,我們知道宿主機有hostPath和emptyDir,由於這兩種的特定不適用於本地持久化儲存。那麼本地持久化儲存必須能保證POD被排程到具有本地持久化儲存的節點上。
為什麼需要這種型別的儲存呢?有時候你的應用對磁碟IO有很高的要求,網路儲存效能肯定不如本地的高,尤其是本地使用了SSD這種磁碟。
但這裡有個問題,通常我們先建立PV,然後建立PVC,這時候如果兩者匹配那麼系統會自動進行繫結,哪怕是動態PV建立,也是先排程POD到任意一個節點,然後根據PVC來進行建立PV然後進行繫結最後掛載到POD中,可是本地持久化儲存有一個問題就是這種PV必須要先準備好,而且不一定叢集所有節點都有這種PV,如果POD隨意排程肯定不行,如何保證POD一定會被排程到有PV的節點上呢?這時候就需要在PV中宣告節點親和,且POD被排程的時候還要考慮卷的分佈情況。
定義PV
apiVersion: v1
kind: PersistentVolume
metadata:
name: example-pv
spec:
capacity:
storage: 5Gi
volumeMode: Filesystem
accessModes:
- ReadWriteOnce
persistentVolumeReclaimPolicy: Delete
storageClassName: local-storage
local: # local型別
path: /data/vol1 # 節點上的具體路徑
nodeAffinity: # 這裡就設定了節點親和
required:
nodeSelectorTerms:
- matchExpressions:
- key: kubernetes.io/hostname
operator: In
values:
- node01 # 這裡我們使用node01節點,該節點有/data/vol1路徑
如果你在node02上也有/data/vol1這個目錄,上面這個PV也一定不會在node02上,因為下面的nodeAffinity設定了主機名就等於node01。
另外這種本地PV通常推薦使用的是宿主機上單獨的硬碟裝置,而不是和作業系統共有一塊硬碟,雖然可以這樣用。
定義儲存類
kind: StorageClass
apiVersion: storage.k8s.io/v1
metadata:
name: local-storage
provisioner: kubernetes.io/no-provisioner
volumeBindingMode: WaitForFirstConsumer
這裡的volumeBindingMode: WaitForFirstConsumer
很關鍵,意思就是延遲繫結,當有符合PVC要求的PV不立即繫結。因為POD使用PVC,而繫結之後,POD被排程到其他節點,顯然其他節點很有可能沒有那個PV所以POD就掛起了,另外就算該節點有合適的PV,而POD被設定成不能執行在該節點,這時候就沒法了,延遲繫結的好處是,POD的排程要參考卷的分佈。當開始排程POD的時候看看它要求的LPV在哪裡,然後就排程到該節點,然後進行PVC的繫結,最後在掛載到POD中,這樣就保證了POD所在的節點就一定是LPV所在的節點。所以讓PVC延遲繫結,就是等到使用這個PVC的POD出現在排程器上之後(真正被排程之前),然後根據綜合評估再來繫結這個PVC。
定義PVC
kind: PersistentVolumeClaim
apiVersion: v1
metadata:
name: local-claim
spec:
accessModes:
- ReadWriteOnce
resources:
requests:
storage: 5Gi
storageClassName: local-storage
可以看到這個PVC是pending狀態,這也就是延遲繫結,因為此時還沒有POD。
定義POD
apiVersion: apps/v1
kind: Deployment
metadata:
name: tomcat-deploy
spec:
replicas: 1
selector:
matchLabels:
appname: myapp
template:
metadata:
name: myapp
labels:
appname: myapp
spec:
containers:
- name: myapp
image: tomcat:8.5.38-jre8
ports:
- name: http
containerPort: 8080
protocol: TCP
volumeMounts:
- name: tomcatedata
mountPath : "/data"
volumes:
- name: tomcatedata
persistentVolumeClaim:
claimName: local-claim
這個POD被排程到node01上,因為我們的PV就在node01上,這時候你刪除這個POD,然後在重建該POD,那麼依然會被排程到node01上。
總結:本地卷也就是LPV不支援動態供給的方式,延遲繫結,就是為了綜合考慮所有因素再進行POD排程。其根本原因是動態供給是先排程POD到節點,然後動態建立PV以及繫結PVC最後執行POD;而LPV是先建立與某一節點關聯的PV,然後在排程的時候綜合考慮各種因素而且要包括PV在哪個節點,然後再進行排程,到達該節點後在進行PVC的繫結。也就說動態供給不考慮節點,LPV必須考慮節點。所以這兩種機制有衝突導致無法在動態供給策略下使用LPV。換句話說動態供給是PV跟著POD走,而LPV是POD跟著PV走。