【Kubernetes】K8s筆記(十四):PersistentVolume 使用網絡共享存儲(NFS)
要想讓存儲卷真正能被 Pod 任意掛載,我們需要變更存儲的方式,不能限定在本地磁盤,而是要改成網絡存儲,這樣 Pod 無論在哪里運行,只要知道 IP 地址或者域名,就可以通過網絡通信訪問存儲設備。
網絡存儲是一個非常熱門的應用領域,有很多知名的產品,比如 AWS、Azure、Ceph,Kubernetes 還專門定義了 CSI(Container Storage Interface)規范,不過這些存儲類型的安裝、使用都比較復雜,在實驗環境里部署難度比較高。
所以我們以 NFS (Network File System)為例學習如何在 Kubernetes 里使用網絡存儲,以及靜態存儲卷和動態存儲卷的概念。
0. 安裝 NFS 服務器及客戶端
NFS 采用的是經典的 Client/Server 架構,需要選定一臺主機作為 Server,安裝 NFS 服務端;其他要使用存儲的主機作為 Client,安裝 NFS 客戶端工具。
我這里就再安裝一臺虛擬機作為 NFS Server:
虛擬機地址 | 功能 |
---|---|
172.16.63.128 | Kubernetes Control-Plane |
172.16.63.129 | Kubernetes Worker Node |
172.16.63.131 | NFS Server |
要安裝 NFS Server 只需要執行下面的命令:
$ sudo apt -y install nfs-kernel-server
安裝好之后,需要給 NFS 指定一個存儲位置,也就是網絡共享目錄。一般來說,應該建立一個專門的 /data
目錄,在這里我使用 /data/nfs
:
$ sudo mkdir -p /data/nfs
接下配置 NFS 訪問共享目錄,修改 /etc/exports
,指定目錄名、允許訪問的網段,還有權限等參數:
/data/nfs 172.16.63.1/24(rw,sync,no_subtree_check,no_root_squash,insecure)
改好之后,需要用 exportfs -ra 通知 NFS,讓配置生效,再用 exportfs -v 驗證效果:
$ sudo exportfs -ra
$ sudo exportfs -v
/data/nfs 172.16.63.1/24(sync,wdelay,hide,no_subtree_check,sec=sys,rw,insecure,no_root_squash,no_all_squash)
最后使用 systemctl
啟動 NFS 服務:
$ sudo systemctl start nfs-server
$ sudo systemctl enable nfs-server
$ sudo systemctl status nfs-server
● nfs-server.service - NFS server and services
Loaded: loaded (/lib/systemd/system/nfs-server.service; enabled; vendor preset: enabled)
Drop-In: /run/systemd/generator/nfs-server.service.d
└─order-with-mounts.conf
Active: active (exited) since Fri 2022-10-28 08:39:24 UTC; 8min ago
Main PID: 2677 (code=exited, status=0/SUCCESS)
CPU: 8ms
然后使用下面的命令檢查 NFS 的網絡掛載情況:
$ showmount -e 127.0.0.1
Export list for 127.0.0.1:
/data/nfs 172.16.63.1/24
為了讓 Kubernetes 集群能夠訪問 NFS 存儲服務,我們還需要在每個節點上都安裝 NFS 客戶端:
$ sudo apt -y install nfs-common
同樣,在節點上可以用 showmount 檢查 NFS 能否正常掛載,注意 IP 地址要寫成 NFS 服務器的地址:
$ showmount -e 172.16.63.131
Export list for 172.16.63.131:
/data/nfs 172.16.63.1/24
手動測試掛載 NFS
首先在 Worker 節點上創建一個文件夾作為掛載點:$ sudo mkdir -p /tmp/nfs-test
用命令
mount
把 NFS 服務器的共享目錄掛載到剛才創建的本地目錄上:$ sudo -i # echo "hello, nfs!" > /tmp/nfs-test/hello # cat /tmp/nfs-test/hello hello, nfs!
回到 NFS 服務器,檢查共享目錄,應該會看到也出現了一個同樣的文件。
1. 在 Kubernetes 中使用 NFS 存儲卷
現在我們已經為 Kubernetes 配置好了 NFS 存儲系統,就可以使用它來創建新的 PV 存儲對象了。
手工分配一個存儲卷,指定 storageClassName
為 nfs
,accessMode
設置為 ReadWriteMany
(因為 NFS 支持多個節點同時訪問一個共享目錄)。
因為這個存儲卷是 NFS 系統,所以我們還需要在 YAML 里添加 nfs 字段,指定 NFS 服務器的 IP 地址和共享目錄名。
下面我們在 NFS 服務器的 共享目錄中建立一個文件夾 1gib-pv
表示一個 1GiB 的 PersistentVolume,然后使用 YAML 文件描述這個 PV:
# nfs-1gib-pv.yaml
apiVersion: v1
kind: PersistentVolume
metadata:
name: nfs-1gib-pv
spec:
storageClassName: nfs
accessModes:
- ReadWriteMany
capacity:
storage: 1Gi
nfs:
path: /data/nfs/1gib-pv
server: 172.16.63.131
然后我們創建這個 PV 對象,然后查看狀態:
$ kubectl apply -f nfs-1gib-pv.yaml
persistentvolume/nfs-1gib-pv created
$ kubectl get pv -o wide
NAME CAPACITY ACCESS MODES RECLAIM POLICY STATUS CLAIM STORAGECLASS REASON AGE VOLUMEMODE
nfs-1gib-pv 1Gi RWX Retain Available nfs 22s Filesystem
注意:
spec.nfs
里的 IP 地址一定要正確,路徑一定要存在(事先創建好),否則 Kubernetes 按照 PV 的描述會無法掛載 NFS 共享目錄,PV 就會處于Pending
狀態無法使用。
有了 PV,我們就可以定義申請存儲的 PVC 對象了,它的內容和 PV 差不多,但不涉及 NFS 存儲的細節,只需要用 resources.request
來表示希望要有多大的容量,這里寫成 1GB,和 PV 的容量相同:
# 1gib-pvc.yaml
apiVersion: v1
kind: PersistentVolumeClaim
metadata:
name: nfs-static-pvc
spec:
storageClassName: nfs
accessModes:
- ReadWriteMany
resources:
requests:
storage: 1Gi
創建 PVC 對象之后,Kubernetes 就會根據 PVC 的描述,找到最合適的 PV:
$ kubectl apply -f 1gib-pvc.yaml
persistentvolumeclaim/nfs-static-pvc created
$ kubectl get pv -o wide
NAME CAPACITY ACCESS MODES RECLAIM POLICY STATUS CLAIM STORAGECLASS REASON AGE VOLUMEMODE
nfs-1gib-pv 1Gi RWX Retain Bound default/nfs-static-pvc nfs 8m1s Filesystem
$ kubectl get pvc -o wide
NAME STATUS VOLUME CAPACITY ACCESS MODES STORAGECLASS AGE VOLUMEMODE
nfs-static-pvc Bound nfs-1gib-pv 1Gi RWX nfs 17s Filesystem
最后創建一個 Pod,把 PVC 掛載成它的一個 volume。在這一步我們只需要在 persistentVolumeClaim
中指定 PVC 的名稱就可以了:
# nfs-static-pod.yaml
apiVersion: v1
kind: Pod
metadata:
name: nfs-static-pod
spec:
volumes:
- name: nfs-pvc-vol
persistentVolumeClaim:
claimName: nfs-static-pvc
containers:
- name: nfs-pvc-test
image: nginx:alpine
ports:
- containerPort: 80
volumeMounts:
- name: nfs-pvc-vol
mountPath: /tmp
創建完畢 Pod 后我們使用 describe
命令查看 Volumes:
$ kubectl apply -f nfs-static-pod.yaml
pod/nfs-static-pod created
$ kubectl describe pod nfs-static-pod
Name: nfs-static-pod
Namespace: default
Priority: 0
Service Account: default
Node: worker1/172.16.63.129
...
Volumes:
nfs-pvc-vol:
Type: PersistentVolumeClaim (a reference to a PersistentVolumeClaim in the same namespace)
ClaimName: nfs-static-pvc
ReadOnly: false
...
Pod、PVC、PV 和 NFS 存儲的關系可以用下圖來形象地表示:
因為我們在 PV/PVC 里指定了 storageClassName
是 nfs
,節點上也安裝了 NFS 客戶端,所以 Kubernetes 就會自動執行 NFS 掛載動作,把 NFS 的共享目錄 /tmp/nfs/1g-pv
掛載到 Pod 里的 /tmp
,完全不需要我們去手動管理。
現在測試一下掛載的正確性,首先我們使用命令進入 Pod:
$ kubectl exec -it pods/nfs-static-pod -- sh
進入 Pod 后我們在掛載目錄建立一個文件:
/ # cd tmp
/tmp # echo Hello! This is a file created on a pod. > hello.text
/tmp #
然后在 NFS 服務器查看該文件:
$ ls
hello.text
$ cat hello.text
Hello! This is a file created on a pod.
發現 Pod 里創建的文件確實寫入了共享目錄。
而且因為 NFS 是一個網絡服務,不會受 Pod 調度位置的影響,所以只要網絡通暢,這個 PV 對象就會一直可用,數據也就實現了真正的持久化存儲。
2. 動態存儲卷 Provisioner
現在網絡存儲系統確實能夠讓集群里的 Pod 任意訪問,數據在 Pod 銷毀后仍然存在,新創建的 Pod 可以再次掛載,然后讀取之前寫入的數據。但是,PV 之類的對象還是需要運維人員手工管理,而且 PV 的大小也很難提前知曉、精確控制,容易出現空間不足或者空間浪費等情況。
在一個大集群里,每天可能會有幾百幾千個應用需要 PV 存儲,如果仍然用人力來管理分配存儲,管理員很可能會忙得焦頭爛額,導致分配存儲的工作大量積壓。
為了實現 PV 創建自動化和卷分配自動化,Kubernetes 提出“動態存儲卷”的概念:它可以用 StorageClass 綁定一個 Provisioner 對象,而這個 Provisioner 就是一個能夠自動管理存儲、創建 PV 的應用,代替了原來系統管理員的手工勞動。
目前,Kubernetes 里每類存儲設備都有相應的 Provisioner 對象,對于 NFS 來說,它的 Provisioner 就是 NFS subdir external provisioner
NFS Provisioner 也是以 Pod 的形式運行在 Kubernetes 里的,在 GitHub 的 deploy 目錄里是部署它所需的 YAML 文件,一共有三個,分別是 rbac.yaml
class.yaml
deployment.yaml
。
這里我將部署文件放在 nfs/provisioner
目錄下。
要想在集群內運行 Provisioner,我們還要對其中兩個文件進行修改:
第一個要修改的是 rbac.yaml
,它使用的是默認的 default 名字空間,應該把它改成其他的名字空間,避免與普通應用混在一起,可以用“查找替換”的方式把它統一改成 kube-system
。
然后修改 deployment.yaml
,首先要把名字空間改成和 rbac.yaml
一樣,比如是 kube-system
,然后重點要修改 volumes
和 env
里的 IP 地址和共享目錄名,必須和集群里的 NFS 服務器配置一樣。
# nfs/provisoner/deployment.yaml
apiVersion: apps/v1
kind: Deployment
metadata:
name: nfs-client-provisioner
labels:
app: nfs-client-provisioner
# replace with namespace where provisioner is deployed
namespace: kube-system
spec:
replicas: 1
strategy:
type: Recreate
selector:
matchLabels:
app: nfs-client-provisioner
template:
metadata:
labels:
app: nfs-client-provisioner
spec:
serviceAccountName: nfs-client-provisioner
containers:
- name: nfs-client-provisioner
image: docker.io/chronolaw/nfs-subdir-external-provisioner:v4.0.2 # 改一下鏡像地址
volumeMounts:
- name: nfs-client-root
mountPath: /persistentvolumes
env:
- name: PROVISIONER_NAME
value: k8s-sigs.io/nfs-subdir-external-provisioner
- name: NFS_SERVER
value: 172.16.63.131
- name: NFS_PATH
value: /data/nfs
volumes:
- name: nfs-client-root
nfs:
server: 172.16.63.131
path: /data/nfs
還有一件事就是 gcr.io
上的鏡像拉取困難,羅劍鋒老師把它的鏡像轉存到了 Docker Hub 上。我們只需要更改一下鏡像地址即可 image: docker.io/chronolaw/nfs-subdir-external-provisioner:v4.0.2
。
把這兩個 YAML 修改好之后,我們就可以在 Kubernetes 里創建 NFS Provisioner 了。
$ kubectl apply -f rbac.yaml -f class.yaml -f deployment.yaml
serviceaccount/nfs-client-provisioner created
clusterrole.rbac.authorization.k8s.io/nfs-client-provisioner-runner created
clusterrolebinding.rbac.authorization.k8s.io/run-nfs-client-provisioner created
role.rbac.authorization.k8s.io/leader-locking-nfs-client-provisioner created
rolebinding.rbac.authorization.k8s.io/leader-locking-nfs-client-provisioner created
storageclass.storage.k8s.io/nfs-client created
deployment.apps/nfs-client-provisioner created
使用命令 kubectl get
,再加上名字空間限定 -n kube-system
,就可以看到 NFS Provisioner 在 Kubernetes 里運行起來了。
$ kubectl get deploy -n kube-system -o wide
NAME READY UP-TO-DATE AVAILABLE AGE CONTAINERS IMAGES SELECTOR
coredns 2/2 2 2 13d coredns registry.aliyuncs.com/google_containers/coredns:v1.9.3 k8s-app=kube-dns
nfs-client-provisioner 1/1 1 1 61s nfs-client-provisioner docker.io/chronolaw/nfs-subdir-external-provisioner:v4.0.2 app=nfs-client-provisioner
$ kubectl get pods -n kube-system -l app=nfs-client-provisioner
NAME READY STATUS RESTARTS AGE
nfs-client-provisioner-7f58779d49-k78m2 1/1 Running 0 2m22s
3. 使用 NFS 動態存儲卷
因為有了 Provisioner,我們就不再需要手工定義 PV 對象了,只需要在 PVC 里指定 StorageClass 對象,它再關聯到 Provisioner。
我們來看一下 NFS 默認的 StorageClass 定義:
# nfs/provisioner/class.yaml
apiVersion: storage.k8s.io/v1
kind: StorageClass
metadata:
name: nfs-client
provisioner: k8s-sigs.io/nfs-subdir-external-provisioner # or choose another name, must match deployment's env PROVISIONER_NAME'
parameters:
archiveOnDelete: "false"
YAML 里的關鍵字段是 provisioner
,它指定了應該使用哪個 Provisioner。另一個字段 parameters
是調節 Provisioner 運行的參數,需要參考文檔來確定具體值,在這里的 archiveOnDelete: "false"
就是自動回收存儲空間。
理解了 StorageClass 的 YAML 之后,你也可以不使用默認的 StorageClass,而是根據自己的需求,任意定制具有不同存儲特性的 StorageClass,比如添加字段 onDelete: "retain"
暫時保留分配的存儲,之后再手動刪除。
現在我們定義一個 PVC,向系統申請 10MB 的存儲空間,使用的 StorageClass 是默認的 nfs-client
:
# nfs/test/nfs-provisioner-pvc.yaml
apiVersion: v1
kind: PersistentVolumeClaim
metadata:
name: nfs-dyn-10mib-pvc
spec:
storageClassName: nfs-client
accessModes:
- ReadWriteMany
resources:
requests:
storage: 10Mi
寫好了 PVC,我們還是在 Pod 里用 volumes
和 volumeMounts
掛載,然后 Kubernetes 就會自動找到 NFS Provisioner,在 NFS 的共享目錄上創建出合適的 PV 對象:
# nfs/test/nfs-provisioner-pod.yaml
apiVersion: v1
kind: Pod
metadata:
name: nfs-dyn-pod
spec:
volumes:
- name: nfs-dyn-10mib-vol
persistentVolumeClaim:
claimName: nfs-dyn-10mib-pvc
containers:
- name: nfs-dyn-test
image: nginx:alpine
ports:
- containerPort: 80
volumeMounts:
- name: nfs-dyn-10mib-vol
mountPath: /tmp
創建 PVC 和 Pod,然后查看集群狀態:
$ kubectl apply -f nfs-provisioner-pvc.yaml -f nfs-provisioner-pod.yaml
persistentvolumeclaim/nfs-dyn-10mib-pvc created
pod/nfs-dyn-pod created
$ kubectl get pv -o wide
NAME CAPACITY ACCESS MODES RECLAIM POLICY STATUS CLAIM STORAGECLASS REASON AGE VOLUMEMODE
pvc-4a7bc325-ca6e-46ca-9f96-8d0217647019 10Mi RWX Delete Bound default/nfs-dyn-10mib-pvc nfs-client 35s Filesystem
$ kubectl get pvc -o wide
NAME STATUS VOLUME CAPACITY ACCESS MODES STORAGECLASS AGE VOLUMEMODE
nfs-dyn-10mib-pvc Bound pvc-4a7bc325-ca6e-46ca-9f96-8d0217647019 10Mi RWX nfs-client 23s Filesystem
雖然我們沒有直接定義 PV 對象,但由于有 NFS Provisioner,它就自動創建一個 PV,大小剛好是在 PVC 里申請的 10MiB。
如果這個時候再去 NFS 服務器上查看共享目錄,也會發現多出了一個目錄,名字與這個自動創建的 PV 一樣,但加上了名字空間和 PVC 的前綴:
nfs-server:/data/nfs$ ls
1gib-pv default-nfs-dyn-10mib-pvc-pvc-4a7bc325-ca6e-46ca-9f96-8d0217647019 hello