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走。