PV和PVC
Kubernetes Volume提供了非常好的數據持久化方案,不過對於大型Kubernetes集羣來說管理上還有不方便之處。Volume方案需要創建Pod或者Deployment的管理員對共享存儲的參數和機制比較清楚,甚至對一些存儲的訪問是需要身份驗證的,這導致集羣用戶(存儲使用者)和系統管理員(存儲管理員)的職責耦合在一起了。但對於大型的生產環境,爲了管理的效率和安全,集羣用戶(存儲使用者)和系統管理員(存儲管理員)是分置的。
Kubernetes引入了兩個新的API資源PersistentVolume和PersistentVolumeClaim來解決這個問題。
PersistentVolume(PV)是集羣中由系統管理員配置的一段網絡存儲。它是集羣中的資源,就像node是集羣資源一樣。PV也是像是Volumes一樣的存儲插件,但其生命週期獨立於使用PV的任何單個Pod。PV配置存儲實現的詳細信息,包括NFS,iSCSI或特定於雲提供程序的存儲系統。PV屬於集羣級別資源,不屬於任何的Namespace。
PersistentVolumeClaim(PVC)是使用存儲的請求,屬於Namespace中的資源。PVC類似於Pod,Pod消耗node資源,PVC消耗PV資源。Pod可以請求特定級別的計算資源(CPU和內存),PVC可以請求特定大小和訪問模式的存儲資源。
PV由系統管理員創建和維護,系統管理員會根據後端存儲系統的特點以及訪問需求(訪問模式和容量)創建不同的PV,系統管理員不用關心哪些Pod會使用這些PV。PVC由集羣用戶創建和維護,PVC指明需求的存儲資源的訪問模式和容量大小,Kubernetes會根據PVC的需求自動查找並提供滿足條件的PV,開發人員提出PVC請求時不用關心真正的存儲資源在哪兒,如何訪問等底層信息。
下面採用NFS共享存儲舉一個例子進行說明。NFS服務器的搭建請參見NFS v4的安裝和使用-CentOS 7。並創建一個新的共享目錄。
mkdir /data/pvpvc -p
chmod 777 /data/pvpvc
vim /etc/exports
/data/pvpvc 192.168.1.0/24(sync,rw,no_root_squash)
exportfs -r
showmount -e nfs
Export list for nfs:
/data/pvpvc 192.168.1.0/24
在Kubernetes的所有worker節點安裝NFS客戶端,並測試是否能夠連接NFS服務器。
yum install -y nfs-utils
showmount -e 192.168.1.80
Export list for 192.168.1.80:
/data/pvpvc 192.168.1.0/24
創建PV和PVC,PVC和PV之間沒有依靠ID、名稱或者label匹配,而是靠容量和訪問模式,PVC的容量和訪問模式需要是某個PV的子集才能自動匹配上。注意:PVC和PV是一對一的,也即一個PV被一個PVC自動匹配後,不會再被其它PVC匹配了,即使PVC需求能夠完全滿足。
vi my-nfs-pv.yaml
apiVersion: v1
kind: PersistentVolume
metadata:
name: my-nfs-pv
spec:
#容量參數
capacity:
storage: 2Gi
#訪問模式
accessModes:
- ReadWriteMany
- ReadWriteOnce
- ReadOnlyMany
#後端共享存儲訪問參數
nfs:
path: /data/pvpvc
server: 192.168.1.80
vi my-nfs-pvc.yaml
kind: PersistentVolumeClaim
apiVersion: v1
metadata:
name: my-nfs-pvc
spec:
#訪問模式需求
accessModes:
- ReadWriteMany
#資源(容量)請求
resources:
requests:
storage: 2Gi
kubectl apply -f my-nfs-pv.yaml
kubectl get pv
NAME CAPACITY ACCESS MODES RECLAIM POLICY STATUS CLAIM STORAGECLASS REASON AGE
my-nfs-pv 2Gi RWX Retain Available 14s
kubectl apply -f my-nfs-pvc.yaml
#發現PVC已經和PV自動匹配上
kubectl get pv
NAME CAPACITY ACCESS MODES RECLAIM POLICY STATUS CLAIM STORAGECLASS REASON AGE
my-nfs-pv 2Gi RWX Retain Bound default/my-nfs-pvc 49s
kubectl get pvc
NAME STATUS VOLUME CAPACITY ACCESS MODES STORAGECLASS AGE
my-nfs-pvc Bound my-nfs-pv 2Gi RWX 23s
使用pvc創建Pod。Pod通過volumes來聲明使用哪個PVC,直接使用PVC的名字即可,可見在創建Pod時無需再關心共享存儲的細節了。Pod會向存儲卷中寫入數據。
vi my-pvpvc-pod.yaml
---
apiVersion: v1
kind: Pod
metadata:
name: my-pvpvc-pod
spec:
volumes:
- name: my-nfs-pvc-vol
persistentVolumeClaim:
claimName: my-nfs-pvc
containers:
- name: busybox
image: busybox:latest
volumeMounts:
- mountPath: /data
name: my-nfs-pvc-vol
command:
- "/bin/sh"
- "-c"
- "echo 'hello world' >> /data/hello.txt; sleep 3600"
在NFS服務器中查看,確實有文件生成。
cat /data/pvpvc/hello.txt
hello world
PV訪問模式
- ReadWriteMany:多路讀寫,卷能被集羣多個節點掛載並讀寫
- ReadWriteOnce:單路讀寫,卷只能被單個集羣節點掛載讀寫
- ReadOnlyMany:多路只讀,卷能被多個集羣節點掛載且只能讀
PV和PVC之間的相互作用遵循以下生命週期:Provisioning --> Binding --> Using --> Reclaiming
- 供應Provisioning:通過集羣外的存儲系統或者雲平臺來提供存儲持久化支持。
- 靜態供應Static:系統管理員創建多個PV,它們攜帶可供集羣用戶使用的真實存儲的詳細信息。 它們存在於Kubernetes API中,可用於消費。
- 動態供應Dynamic:當管理員創建的靜態PV都不匹配用戶的PersistentVolumeClaim時,集羣可能會嘗試爲PVC動態配置卷,此配置基於StorageClasses。StorageClasses後面會展開講。
- 綁定Binding:集羣用戶創建PVC並指定需要的資源和訪問模式。在找到可用PV之前,PVC會保持未綁定狀態。PVC與PV的綁定是一對一的映射。
- 使用Using:用戶可在Pod中像volume一樣使用PVC。
- 回收Reclaiming:集羣用戶可以刪除PVC來回收存儲資源,回收策略告訴Kubernetes集羣在刪除PVC後如何處理該存儲卷:保留Retain,回收Recycle和刪除Delete。注意:外部存儲支持哪幾種回收策略需要視外部存儲系統特性決定。
- 保留Retain:允許手動處理回收資源。刪除PVC後PV將變成”Released“狀態,但該PV是無法應用於其它PVC,因爲其上還有前一個PVC的數據。系統管理員可以先刪除PV;PV刪除後共享存儲仍然存在,系統管理員需要手動處理共享存儲中的數據;此時系統管理員可以重新爲該共享存儲創建PVC,也可以直接刪除該共享存儲。
- 刪除Delete:刪除操作會同時從Kubernetes中刪除PersistentVolume對象以及外部存儲系統中的關聯的共享存儲。
- 回收Recycle:該策略已被廢棄,被StorageClasses替代。
接着上面的例子,可以看出my-nfs-pv沒有配置回收策略,其缺省爲Retain,狀態爲Bound。我們依次刪除Pod和PVC。
kubectl delete -f my-pvpvc-pod.yaml
kubectl delete -f my-nfs-pvc.yaml
再次查看PVC和PV,PV狀態爲Released。而且PV的CLAIM字段仍然還保留着default/my-nfs-pvc,頑固的表明其和PVC的一對一的綁定關係。NFS服務器中的文件仍然還在。
kubectl get pvc
No resources found in default namespace.
kubectl get pv
NAME CAPACITY ACCESS MODES RECLAIM POLICY STATUS CLAIM STORAGECLASS REASON AGE
my-nfs-pv 2Gi RWX Retain Released default/my-nfs-pvc 59m
cat /data/pvpvc/hello.txt
hello world
此時在創建一個新的PVC,其需求和上一個PVC完全相同,能夠匹配PV。
vi my-nfs-pvc-2.yaml
kind: PersistentVolumeClaim
apiVersion: v1
metadata:
name: my-nfs-pvc-2
spec:
accessModes:
- ReadWriteMany
resources:
requests:
storage: 2Gi
kubectl apply -f my-nfs-pvc-2.yaml
可以發現my-nfs-pvc-2並沒有綁定此PV,它在等待和自己匹配的PV的新創建。原有的PV並不會被重新分配出去。
kubectl get pvc
NAME STATUS VOLUME CAPACITY ACCESS MODES STORAGECLASS AGE
my-nfs-pvc-2 Pending 9s
kubectl get pv
NAME CAPACITY ACCESS MODES RECLAIM POLICY STATUS CLAIM STORAGECLASS REASON AGE
my-nfs-pv 2Gi RWX Retain Released default/my-nfs-pvc 66m
此時刪除PV,NFS服務器中的文件仍然還在。
kubectl delete -f my-nfs-pv.yaml
cat /data/pvpvc/hello.txt
hello world
不修改my-nfs-pv.yaml,直接重新創建PV。可以發現新的PVC和新的PV已經綁定起來了,但此PVC和PV已經非彼PVC和PV了。
kubectl apply -f my-nfs-pv.yaml
kubectl get pvc
NAME STATUS VOLUME CAPACITY ACCESS MODES STORAGECLASS AGE
my-nfs-pvc-2 Bound my-nfs-pv 2Gi RWX 10m
kubectl get pv
NAME CAPACITY ACCESS MODES RECLAIM POLICY STATUS CLAIM STORAGECLASS REASON AGE
my-nfs-pv 2Gi RWX Retain Bound default/my-nfs-pvc-2 32s
重新創建Pod,注意使用新的my-nfs-pvc-2。可以發現NFS服務器的文件被重複使用。
vi my-pvpvc-pod-2.yaml
---
apiVersion: v1
kind: Pod
metadata:
name: my-pvpvc-pod
spec:
volumes:
- name: my-nfs-pvc-vol
persistentVolumeClaim:
claimName: my-nfs-pvc-2
containers:
- name: busybox
image: busybox:latest
volumeMounts:
- mountPath: /data
name: my-nfs-pvc-vol
command:
- "/bin/sh"
- "-c"
- "echo 'hello world' >> /data/hello.txt; sleep 3600"
kubectl apply -f my-pvpvc-pod-2.yaml
cat /data/pvpvc/hello.txt
hello world
hello world
當一個Pod被調度到一個node節點,kubelet會爲Pod創建Pod對應的目錄/var/lib/kubelet/pods/。my-pvpvc-pod被調度到k8s-node1節點,所以會在k8s-node1節點創建Pod目錄/var/lib/kubelet/pods/5757df8b-fd9c-43f8-a660-60177de1e9ea。
kubectl get pod -o wide
NAME READY STATUS RESTARTS AGE IP NODE NOMINATED NODE READINESS GATES
my-pvpvc-pod 1/1 Running 23 23h 10.244.1.48 k8s-node1 <none> <none>
kubectl get pod my-pvpvc-pod -o yaml | grep uid
uid: 5757df8b-fd9c-43f8-a660-60177de1e9ea
cd /var/lib/kubelet/pods/5757df8b-fd9c-43f8-a660-60177de1e9ea
ls
containers etc-hosts plugins volumes
當創建Pod時,同時也會創建存儲卷目錄/volumes/kubernetes.io~<Volume類型>/<Volume名字>,並將共享存儲mount到該目錄。可以看出k8s-node1節點Mount了兩個目錄,其中一個爲Secret,一個爲NFS。hello.txt文件就是NFS服務器的hello.txt文件。
kubelet將遠程共享存儲Mount到Pod宿主機上。容器想使用該存儲卷還需要將該宿主機上的目錄掛載到容器中。
mount | grep 5757df8b-fd9c-43f8-a660-60177de1e9ea
tmpfs on /var/lib/kubelet/pods/5757df8b-fd9c-43f8-a660-60177de1e9ea/volumes/kubernetes.io~secret/default-token-wtqgl type tmpfs (rw,relatime)
192.168.1.80:/data/pvpvc on /var/lib/kubelet/pods/5757df8b-fd9c-43f8-a660-60177de1e9ea/volumes/kubernetes.io~nfs/my-nfs-pv type nfs4 (rw,relatime,vers=4.1,rsize=131072,wsize=131072,namlen=255,hard,proto=tcp,timeo=600,retrans=2,sec=sys,clientaddr=192.168.1.56,local_lock=none,addr=192.168.1.80)
cd /volumes/kubernetes.io~nfs/my-nfs-pv
cat hello.txt
hello world
hello world
StorageClass
在靜態供應模式下,這需要系統管理員需要提前準備好各種不同資源(容量)和訪問模式的存儲資源。但在大規模集羣中會需要衆多PV,如果這些PV都需要系統管理員手動逐個創建是很繁瑣的。所以Kubernetes又提供了動態供應模式,動態供應的關鍵是StorageClass,StorageClass的作用就像是創建PV模板,當PVC發出存儲資源(容量和訪問模式)請求時,Kubernetes的供應者Provisioner根據StorageClass來動態創建PV。StorageClass並不會事先定義PV提供的容量和訪問模式,而是根據PVC的容量和訪問模式需求自動創建的對應的PV。系統管理員可以針對不同的後端共享存儲類型封裝不同的StorageClasses供PVC使用。
下面演示一下如何利用StorageClasses來動態創建PV。在NFS服務器創建一個新的共享目錄。
mkdir /data/sc -p
chmod 777 /data/sc
vim /etc/exports
/data/sc 192.168.1.0/24(sync,rw,no_root_squash)
exportfs -r
showmount -e nfs
Export list for nfs:
/data/sc 192.168.1.0/24
在Kubernetes的所有worker節點測試是否能夠連接NFS服務器。
showmount -e 192.168.1.80
Export list for 192.168.1.80:
/data/sc 192.168.1.0/24
/data/pvpvc 192.168.1.0/24
Kubernetes官方提供內置的StorageClass的供應者Provisioner,但其中是不支持NFS的,不過有支持NFS的外置的Provisioner,通過helm安裝nfs-cleint-provisioner。
helm install nfs-provisioner stable/nfs-client-provisioner --set nfs.server=192.168.1.80 --set nfs.path=/data/sc
#注意StorageClass的名稱nfs-client
kubectl get storageclass
NAME PROVISIONER AGE
nfs-client cluster.local/nfs-provisioner-nfs-client-provisioner 36m
kubectl get all
NAME READY STATUS RESTARTS AGE
pod/nfs-provisioner-nfs-client-provisioner-bbd974457-g9p9m 1/1 Running 0 86s
NAME TYPE CLUSTER-IP EXTERNAL-IP PORT(S) AGE
service/kubernetes ClusterIP 10.1.0.1 <none> 443/TCP 27d
NAME READY UP-TO-DATE AVAILABLE AGE
deployment.apps/nfs-provisioner-nfs-client-provisioner 1/1 1 1 86s
NAME DESIRED CURRENT READY AGE
replicaset.apps/nfs-provisioner-nfs-client-provisioner-bbd974457 1 1 1 86s
創建PVC,其中storageClassName關聯的就是StorageClass的名稱nfs-client,在創建PVC時會關聯StorageClass然後自動創建PV,並綁定上去。注意PV的容量爲100Mi,訪問模式爲ReadWriteOnce,回收策略是Delete。
vi my-sc-pvc-rwo-100m.yaml
---
apiVersion: v1
kind: PersistentVolumeClaim
metadata:
name: my-sc-pvc-rwo-100m
spec:
storageClassName: nfs-client
accessModes:
- ReadWriteOnce
resources:
requests:
storage: 100Mi
kubectl apply -f my-sc-pvc-rwo-100m.yaml
kubectl get pvc
NAME STATUS VOLUME CAPACITY ACCESS MODES STORAGECLASS AGE
my-sc-pvc-rwo-100m Bound pvc-9640b177-0c66-42b0-9ede-cffa59a82673 100Mi RWO nfs-client 11s
kubectl get pv
NAME CAPACITY ACCESS MODES RECLAIM POLICY STATUS CLAIM STORAGECLASS REASON AGE
pvc-9640b177-0c66-42b0-9ede-cffa59a82673 100Mi RWO Delete Bound default/my-sc-pvc-rwo-100m nfs-client 35s
創建Pod,可以看出Pod存儲卷配置和靜態供給方式沒有任何區別。
vi my-sc-pod.yaml
apiVersion: v1
kind: Pod
metadata:
name: my-sc-pod
spec:
volumes:
- name: my-sc-pvc-vol
persistentVolumeClaim:
claimName: my-sc-pvc-rwo-100m
containers:
- name: busybox
image: busybox:latest
volumeMounts:
- mountPath: /data
name: my-sc-pvc-vol
command:
- "/bin/sh"
- "-c"
- "echo 'hello world' >> /data/hello.txt; sleep 3600"
kubectl apply -f my-sc-pod.yaml
NFS服務器有文件創建。
cd /data/sc/
ls
default-my-sc-pvc-rwo-100m-pvc-9640b177-0c66-42b0-9ede-cffa59a82673
cd default-my-sc-pvc-rwo-100m-pvc-9640b177-0c66-42b0-9ede-cffa59a82673/
ls
hello.txt
cat hello.txt
hello world
再創建一個新的PVC,其中容量和訪問模式不同。可以發現自動創建了對應需求的PV。
vi my-sc-pvc-rwx-200m.yaml
---
apiVersion: v1
kind: PersistentVolumeClaim
metadata:
name: my-sc-pvc-rwx-200m
spec:
storageClassName: nfs-client
accessModes:
- ReadWriteMany
resources:
requests:
storage: 200Mi
kubectl apply -f my-sc-pvc-rwx-200m.yaml
kubectl get pvc
NAME STATUS VOLUME CAPACITY ACCESS MODES STORAGECLASS AGE
my-sc-pvc-rwx-200m Bound pvc-f987075d-aa2c-4d7b-a19e-607e2416af72 200Mi RWX nfs-client 13s
my-sc-pvc-rwo-100m Bound pvc-9640b177-0c66-42b0-9ede-cffa59a82673 100Mi RWO nfs-client 20m
kubectl get pv
NAME CAPACITY ACCESS MODES RECLAIM POLICY STATUS CLAIM STORAGECLASS REASON AGE
pvc-9640b177-0c66-42b0-9ede-cffa59a82673 100Mi RWO Delete Bound default/my-sc-pvc-rwo-100m nfs-client 20m
pvc-f987075d-aa2c-4d7b-a19e-607e2416af72 200Mi RWX Delete Bound default/my-sc-pvc-rwx-200m nfs-client 17s
刪除Pod和PVC,再到NFS服務器查看,看到共享存儲目錄已經改爲歸檔archived-default-my-sc-pvc-pvc-430a5b2f-36df-4a41-9432-0005aa5a460c,並且其中文件仍然存在。如果要重新使用需要手動操作了。
kubectl delete -f my-sc-pod.yaml
kubectl delete -f my-sc-pvc-rwo-100m.yaml
cd /data/sc
ls
default-my-sc-pvc-rwx-200m-pvc-f987075d-aa2c-4d7b-a19e-607e2416af72 archived-default-my-sc-pvc-rwo-100m-pvc-9640b177-0c66-42b0-9ede-cffa59a82673
cd archived-default-my-sc-pvc-rwo-100m-pvc-9640b177-0c66-42b0-9ede-cffa59a82673/
ls
hello.txt
LocalVolume
LocalVolume本地數據卷允許用戶通過標準PVC接口以簡單且可移植的方式訪問node節點的本地存儲。PV的定義中需要包含描述節點親和性的信息,Kubernetes系統則使用該信息將容器調度到正確的node節點。本地數據卷(Local Volume)代表一個本地存儲設備,比如磁盤、分區或者目錄等。主要的應用場景包括分佈式存儲和數據庫等需要高性能和高可靠性的環境裏。
LocalVolume和hostpath的區別:
- 二者都基於node節點本地存儲資源實現了容器內數據的持久化功能,都爲某些特殊場景下提供了更爲適用的存儲解決方案;
- 二者都爲k8s存儲管理提供了PV、PVC和StorageClass的方法實現;
- LocalVolume實現的StorageClass不具備完整功能,目前只支持卷的延遲綁定;
- hostPath是單節點的本地存儲卷方案,不提供任何基於node節點親和性的pod調度管理支持;
- Localvolume適用於小規模的、多節點的k8s開發或測試環境,尤其是在不具備一套安全、可靠且性能有保證的存儲集羣服務時;
下面演示一下LocalVolume的使用。先創建一個StorageClass,供應者爲kubernetes.io/no-provisioner,WaitForFirstConsumer表示PV不要立即綁定PVC,而是直到有Pod需要用PVC的時候才綁定。調度器會在調度時綜合考慮選擇合適的Local PV,這樣就不會導致跟Pod資源設置selectors,affinity and anti-affinity策略等產生衝突。很明顯:如果PVC先跟Local PV綁定了,由於Local PV是跟node綁定的,這樣selectors,affinity等等就基本沒用了,所以更好的做法是先根據調度策略選擇node,然後再綁定Local PV。
mkdir -p /data/lv
vi lv-sc.yaml
---
apiVersion: storage.k8s.io/v1
kind: StorageClass
metadata:
name: lv-sc
provisioner: kubernetes.io/no-provisioner
volumeBindingMode: WaitForFirstConsumer
vi lv-pv.yaml
---
apiVersion: v1
kind: PersistentVolume
metadata:
name: lv-pv
spec:
capacity:
storage: 100Mi
accessModes:
- ReadWriteOnce
persistentVolumeReclaimPolicy: Retain
storageClassName: lv-sc
local:
path: /data/lv
nodeAffinity:
required:
nodeSelectorTerms:
- matchExpressions:
- key: kubernetes.io/hostname
operator: In
values:
- k8s-node1
kubectl apply -f lv-sc.yaml
kubectl apply -f lv-pv.yaml
kubectl get sc
NAME PROVISIONER AGE
lv-sc kubernetes.io/no-provisioner 45s
kubectl get pv
NAME CAPACITY ACCESS MODES RECLAIM POLICY STATUS CLAIM STORAGECLASS REASON AGE
lv-pv 100Mi RWO Retain Available lv-sc 17s
接下來創建PVC,可以看出PV和PVC沒有綁定。
vi lv-pvc.yaml
---
apiVersion: v1
kind: PersistentVolumeClaim
metadata:
name: lv-pvc
spec:
accessModes:
- ReadWriteOnce
resources:
requests:
storage: 100Mi
storageClassName: lv-sc
kubectl apply -f lv-pvc.yaml
kubectl get pv
NAME CAPACITY ACCESS MODES RECLAIM POLICY STATUS CLAIM STORAGECLASS REASON AGE
lv-pv 100Mi RWO Retain Available lv-sc 8m30s
kubectl get pvc
NAME STATUS VOLUME CAPACITY ACCESS MODES STORAGECLASS AGE
lv-pvc Pending lv-sc 9s
創建Pod,Pod中沒有指定調度node,但確實被調度到了k8s-node1,是根據PV的node親和性策略調度的,並且PV和PVC綁定了。
vi lv-pod.yaml
---
apiVersion: v1
kind: Pod
metadata:
name: lv-pod
spec:
volumes:
- name: lv-vol
persistentVolumeClaim:
claimName: lv-pvc
containers:
- name: busybox
image: busybox:latest
volumeMounts:
- name: lv-vol
mountPath: /data
command:
- "/bin/sh"
- "-c"
- "while true; do echo 'hello world' >> /data/hello.txt; sleep 10; done"
kubectl apply -f lv-pod.yaml
kubectl get pod -o wide
NAME READY STATUS RESTARTS AGE IP NODE NOMINATED NODE READINESS GATES
lv-pod 1/1 Running 0 16s 10.244.1.55 k8s-node1 <none> <none>
kubectl get pv
NAME CAPACITY ACCESS MODES RECLAIM POLICY STATUS CLAIM STORAGECLASS REASON AGE
lv-pv 100Mi RWO Retain Bound default/lv-pvc lv-sc 41s
kubectl get pvc
NAME STATUS VOLUME CAPACITY ACCESS MODES STORAGECLASS AGE
lv-pvc Bound lv-pv 100Mi RWO lv-sc 37s
可以在k8s-node1節點查看。
cat /data/lv/hello.txt
hello world
hello world
hello world
hello world