Kubernetes 存儲原理解析
前面的章節(jié)中我們介紹了在 Kubernetes 中的持久化存儲的使用,了解了 PV、PVC 以及 StorageClass 的使用方法,從本地存儲到 Ceph 共享存儲都有學(xué)習(xí),到這里我們其實(shí)已經(jīng)可以完成應(yīng)用各種場景的數(shù)據(jù)持久化了,但是難免在實(shí)際的使用過程中會遇到各種各樣的問題,要解決這些問題最好的方式就是來了解下 Kubernetes 中存儲的實(shí)現(xiàn)原理。
Kubernetes 默認(rèn)情況下就提供了主流的存儲卷接入方案,我們可以執(zhí)行命令 kubectl explain pod.spec.volumes 查看到支持的各種存儲卷,另外也提供了插件機(jī)制,允許其他類型的存儲服務(wù)接入到 Kubernetes 系統(tǒng)中來,在 Kubernetes 中就對應(yīng) In-Tree 和 Out-Of-Tree 兩種方式, In-Tree 就是在 Kubernetes 源碼內(nèi)部實(shí)現(xiàn)的,和 Kubernetes 一起發(fā)布、管理的,但是更新迭代慢、靈活性比較差, Out-Of-Tree 是獨(dú)立于 Kubernetes 的,目前主要有 CSI 和 FlexVolume 兩種機(jī)制,開發(fā)者可以根據(jù)自己的存儲類型實(shí)現(xiàn)不同的存儲插件接入到 Kubernetes 中去,其中 CSI 是現(xiàn)在也是以后主流的方式,所以當(dāng)然我們的重點(diǎn)也會是 CSI 的使用介紹。
NFS
我們這里為了演示方便,先使用相對簡單的 NFS 這種存儲資源,接下來我們在節(jié)點(diǎn) 10.151.30.11 上來安裝 NFS 服務(wù),數(shù)據(jù)目錄: /data/k8s/
關(guān)閉防火墻
- $ systemctl stop firewalld.service
- $ systemctl disable firewalld.service
安裝配置 nfs
- $ yum -y install nfs-utils rpcbind
共享目錄設(shè)置權(quán)限:
- $ mkdir -p /data/k8s/
- $ chmod 755 /data/k8s/
配置 nfs,nfs 的默認(rèn)配置文件在 /etc/exports 文件下,在該文件中添加下面的配置信息:
- $ vi /etc/exports
- /data/k8s *(rw,sync,no_root_squash)
配置說明:
- /data/k8s:是共享的數(shù)據(jù)目錄
- *:表示任何人都有權(quán)限連接,當(dāng)然也可以是一個(gè)網(wǎng)段,一個(gè) IP,也可以是域名
- rw:讀寫的權(quán)限
- sync:表示文件同時(shí)寫入硬盤和內(nèi)存
- no_root_squash:當(dāng)?shù)卿?NFS 主機(jī)使用共享目錄的使用者是 root 時(shí),其權(quán)限將被轉(zhuǎn)換成為匿名使用者,通常它的 UID 與 GID,都會變成 nobody 身份
當(dāng)然 nfs 的配置還有很多,感興趣的同學(xué)可以在網(wǎng)上去查找一下。
啟動(dòng)服務(wù) nfs 需要向 rpc 注冊,rpc 一旦重啟了,注冊的文件都會丟失,向他注冊的服務(wù)都需要重啟 注意啟動(dòng)順序,先啟動(dòng) rpcbind
- $ systemctl start rpcbind.service
- $ systemctl enable rpcbind
- $ systemctl status rpcbind
- ● rpcbind.service - RPC bind service
- Loaded: loaded (/usr/lib/systemd/system/rpcbind.service; disabled; vendor preset: enabled)
- Active: active (running) since Tue 2018-07-10 20:57:29 CST; 1min 54s ago
- Process: 17696 ExecStart=/sbin/rpcbind -w $RPCBIND_ARGS (code=exited, status=0/SUCCESS)
- Main PID: 17697 (rpcbind)
- Tasks: 1
- Memory: 1.1M
- CGroup: /system.slice/rpcbind.service
- └─17697 /sbin/rpcbind -w
- Jul 10 20:57:29 master systemd[1]: Starting RPC bind service...
- Jul 10 20:57:29 master systemd[1]: Started RPC bind service.
看到上面的 Started 證明啟動(dòng)成功了。
然后啟動(dòng) nfs 服務(wù):
- $ systemctl start nfs.service
- $ systemctl enable nfs
- $ systemctl status nfs
- ● nfs-server.service - NFS server and services
- Loaded: loaded (/usr/lib/systemd/system/nfs-server.service; enabled; vendor preset: disabled)
- Drop-In: /run/systemd/generator/nfs-server.service.d
- └─order-with-mounts.conf
- Active: active (exited) since Tue 2018-07-10 21:35:37 CST; 14s ago
- Main PID: 32067 (code=exited, status=0/SUCCESS)
- CGroup: /system.slice/nfs-server.service
- Jul 10 21:35:37 master systemd[1]: Starting NFS server and services...
- Jul 10 21:35:37 master systemd[1]: Started NFS server and services.
同樣看到 Started 則證明 NFS Server 啟動(dòng)成功了。
另外我們還可以通過下面的命令確認(rèn)下:
- $ rpcinfo -p|grep nfs
- 100003 3 tcp 2049 nfs
- 100003 4 tcp 2049 nfs
- 100227 3 tcp 2049 nfs_acl
- 100003 3 udp 2049 nfs
- 100003 4 udp 2049 nfs
- 100227 3 udp 2049 nfs_acl
查看具體目錄掛載權(quán)限:
- $ cat /var/lib/nfs/etab
- /data/k8s *(rw,sync,wdelay,hide,nocrossmnt,secure,no_root_squash,no_all_squash,no_subtree_check,secure_locks,acl,no_pnfs,anonuid=65534,anongid=65534,sec=sys,secure,no_root_squash,no_all_squash)
到這里我們就把 nfs server 給安裝成功了,然后就是前往節(jié)點(diǎn)安裝 nfs 的客戶端來驗(yàn)證,安裝 nfs 當(dāng)前也需要先關(guān)閉防火墻:
- $ systemctl stop firewalld.service
- $ systemctl disable firewalld.service
然后安裝 nfs
- $ yum -y install nfs-utils rpcbind
安裝完成后,和上面的方法一樣,先啟動(dòng) rpc、然后啟動(dòng) nfs:
- $ systemctl start rpcbind.service
- $ systemctl enable rpcbind.service
- $ systemctl start nfs.service
- $ systemctl enable nfs.service
掛載數(shù)據(jù)目錄 客戶端啟動(dòng)完成后,我們在客戶端來掛載下 nfs 測試下,首先檢查下 nfs 是否有共享目錄:
- $ showmount -e 10.151.30.11
- Export list for 10.151.30.11:
- /data/k8s *
然后我們在客戶端上新建目錄:
- $ mkdir -p /root/course/kubeadm/data
將 nfs 共享目錄掛載到上面的目錄:
- $ mount -t nfs 10.151.30.11:/data/k8s /root/course/kubeadm/data
掛載成功后,在客戶端上面的目錄中新建一個(gè)文件,然后我們觀察下 nfs 服務(wù)端的共享目錄下面是否也會出現(xiàn)該文件:
- $ touch /root/course/kubeadm/data/test.txt
然后在 nfs 服務(wù)端查看:
- $ ls -ls /data/k8s/
- total 4
- 4 -rw-r--r--. 1 root root 4 Jul 10 21:50 test.txt
如果上面出現(xiàn)了 test.txt 的文件,那么證明我們的 nfs 掛載成功了。
存儲架構(gòu)
前面我們了解到了 PV、PVC、StorgeClass 的使用,但是他們是如何和我們的 Pod 關(guān)聯(lián)起來使用的呢?這就需要從 Volume 的處理流程和原理說起了。
如下所示,我們創(chuàng)建了一個(gè) nfs 類型的 PV 資源對象:(volume.yaml)
- apiVersion: v1
- kind: PersistentVolume
- metadata:
- name: nfs-pv
- spec:
- storageClassName: manual
- capacity:
- storage: 1Gi
- accessModes:
- - ReadWriteOnce
- persistentVolumeReclaimPolicy: Retain
- nfs:
- path: /data/k8s # 指定nfs的掛載點(diǎn)
- server: 10.151.30.11 # 指定nfs服務(wù)地址
- ---
- apiVersion: v1
- kind: PersistentVolumeClaim
- metadata:
- name: nfs-pvc
- spec:
- storageClassName: manual
- accessModes:
- - ReadWriteOnce
- resources:
- requests:
- storage: 1Gi
我們知道用戶真正使用的是 PVC,而要使用 PVC 的前提就是必須要先和某個(gè)符合條件的 PV 進(jìn)行一一綁定,比如存儲容器、訪問模式,以及 PV 和 PVC 的 storageClassName 字段必須一樣,這樣才能夠進(jìn)行綁定,當(dāng) PVC 和 PV 綁定成功后就可以直接使用這個(gè) PVC 對象了:(pod.yaml)
- apiVersion: v1
- kind: Pod
- metadata:
- name: test-volumes
- spec:
- volumes:
- - name: nfs
- persistentVolumeClaim:
- claimName: nfs-pvc
- containers:
- - name: web
- image: nginx
- ports:
- - name: web
- containerPort: 80
- volumeMounts:
- - name: nfs
- subPath: test-volumes
- mountPath: "/usr/share/nginx/html"
直接創(chuàng)建上面的資源對象即可:
- $ kubectl apply -f volume.yaml
- $ kubectl apply -f pod.yaml
我們只是在 volumes 中指定了我們上面創(chuàng)建的 PVC 對象,當(dāng)這個(gè) Pod 被創(chuàng)建之后, kubelet 就會把這個(gè) PVC 對應(yīng)的這個(gè) NFS 類型的 Volume(PV)掛載到這個(gè) Pod 容器中的目錄中去。前面我們也提到了這樣的話對于普通用戶來說完全就不用關(guān)心后面的具體存儲在 NFS 還是 Ceph 或者其他了,只需要直接使用 PVC 就可以了,因?yàn)檎嬲拇鎯κ切枰芏嘞嚓P(guān)的專業(yè)知識的,這樣就完全職責(zé)分離解耦了。
普通用戶直接使用 PVC 沒有問題,但是也會出現(xiàn)一個(gè)問題,那就是當(dāng)普通用戶創(chuàng)建一個(gè) PVC 對象的時(shí)候,這個(gè)時(shí)候系統(tǒng)里面并沒有合適的 PV 來和它進(jìn)行綁定,因?yàn)?PV 大多數(shù)情況下是管理員給我們創(chuàng)建的,這個(gè)時(shí)候啟動(dòng) Pod 肯定就會失敗了,如果現(xiàn)在管理員如果去創(chuàng)建一個(gè)對應(yīng)的 PV 的話,PVC 和 PV 當(dāng)然就可以綁定了,然后 Pod 也會自動(dòng)的啟動(dòng)成功,這是因?yàn)樵?Kubernetes 中有一個(gè)專門處理持久化存儲的控制器 Volume Controller,這個(gè)控制器下面有很多個(gè)控制循環(huán),其中一個(gè)就是用于 PV 和 PVC 綁定的 PersistentVolumeController。
PersistentVolumeController 會不斷地循環(huán)去查看每一個(gè) PVC,是不是已經(jīng)處于 Bound(已綁定)狀態(tài)。如果不是,那它就會遍歷所有的、可用的 PV,并嘗試將其與未綁定的 PVC 進(jìn)行綁定,這樣,Kubernetes 就可以保證用戶提交的每一個(gè) PVC,只要有合適的 PV 出現(xiàn),它就能夠很快進(jìn)入綁定狀態(tài)。而所謂將一個(gè) PV 與 PVC 進(jìn)行 “綁定” ,其實(shí)就是將這個(gè) PV 對象的名字,填在了 PVC 對象的 spec.volumeName 字段上。
PV 和 PVC 綁定上了,那么又是如何將容器里面的數(shù)據(jù)進(jìn)行持久化的呢,前面我們學(xué)習(xí)過 Docker 的 Volume 掛載,其實(shí)就是 將一個(gè)宿主機(jī)上的目錄和一個(gè)容器里的目錄綁定掛載在了一起 ,具有持久化功能當(dāng)然就是指的宿主機(jī)上面的這個(gè)目錄了,當(dāng)容器被刪除或者在其他節(jié)點(diǎn)上重建出來以后,這個(gè)目錄里面的內(nèi)容依然存在,所以一般情況下實(shí)現(xiàn)持久化是需要一個(gè)遠(yuǎn)程存儲的,比如 NFS、Ceph 或者云廠商提供的磁盤等等。所以接下來需要做的就是持久化宿主機(jī)目錄這個(gè)過程。
當(dāng) Pod 被調(diào)度到一個(gè)節(jié)點(diǎn)上后,節(jié)點(diǎn)上的 kubelet 組件就會為這個(gè) Pod 創(chuàng)建它的 Volume 目錄,默認(rèn)情況下 kubelet 為 Volume 創(chuàng)建的目錄在 kubelet 工作目錄下面:
- /var/lib/kubelet/pods/<Pod的ID>/volumes/kubernetes.io~<Volume類型>/<Volume名字>
比如上面我們創(chuàng)建的 Pod 對應(yīng)的 Volume 目錄完整路徑為:
- /var/lib/kubelet/pods/d4fcdb11-baf7-43d9-8d7d-3ede24118e08/volumes/kubernetes.io~nfs/nfs-pv
“
要獲取 Pod 的唯一標(biāo)識 uid,可通過命令 kubectl get pod pod名 -o jsonpath={.metadata.uid} 獲取。
”
然后就需要根據(jù)我們的 Volume 類型來決定需要做什么操作了,比如上節(jié)課我們用的 Ceph RBD,那么 kubelet 就需要先將 Ceph 提供的 RBD 掛載到 Pod 所在的宿主機(jī)上面,這個(gè)階段在 Kubernetes 中被稱為 Attach 階段。Attach 階段完成后,為了能夠使用這個(gè)塊設(shè)備,kubelet 還要進(jìn)行第二個(gè)操作,即:格式化這個(gè)塊設(shè)備,然后將它掛載到宿主機(jī)指定的掛載點(diǎn)上。這個(gè)掛載點(diǎn),也就是上面我們提到的 Volume 的宿主機(jī)的目錄。將塊設(shè)備格式化并掛載到 Volume 宿主機(jī)目錄的操作,在 Kubernetes 中被稱為 Mount 階段。上節(jié)課我們使用 Ceph RBD 持久化的 Wordpress 的 MySQL 數(shù)據(jù),我們可以查看對應(yīng)的 Volume 信息:
- $ kubectl get pods -o wide -l app=wordpress
- NAME READY STATUS RESTARTS AGE IP NODE NOMINATED NODE READINESS GATES
- wordpress-5b886cf59b-dv2zt 1/1 Running 0 20d 10.244.1.158 ydzs-node1 <none> <none>
- wordpress-mysql-b9ddd6d4c-pjhbt 1/1 Running 0 20d 10.244.4.70 ydzs-node4 <none> <none>
我們可以看到 MySQL 運(yùn)行在 node4 節(jié)點(diǎn)上,然后可以在該節(jié)點(diǎn)上查看 Volume 信息,Pod 對應(yīng)的 uid 可以通過如下命令獲?。?/p>
- $ kubectl get pod wordpress-mysql-b9ddd6d4c-pjhbt -o jsonpath={.metadata.uid}
- 3f84af87-9f58-4c69-9e38-5ef234498133
- $ ls /var/lib/kubelet/pods/3f84af87-9f58-4c69-9e38-5ef234498133/volumes/kubernetes.io~csi/pvc-c8861c23-c03d-47aa-96f6-73c4d4093109/
- mount vol_data.json
然后通過如下命令可以查看 Volume 的持久化信息:
- $ findmnt /var/lib/kubelet/pods/3f84af87-9f58-4c69-9e38-5ef234498133/volumes/kubernetes.io~csi/pvc-c8861c23-c03d-47aa-96f6-73c4d4093109/mount
- TARGET SOURCE FSTYPE OPTIONS
- /var/lib/kubelet/pods/3f84af87-9f58-4c69-9e38-5ef234498133/volumes/kubernetes.io~csi/pvc-c8861c23-c03d-47aa-96f6-73c4d4093109/mount /dev/rbd0 ext4 rw,relatime,
可以看到這里的 Volume 是掛載到 /dev/rbd0 這個(gè)設(shè)備上面的,通過 df 命令也是可以看到的:
- $ df -h |grep dev
- devtmpfs 3.9G 0 3.9G 0% /dev
- tmpfs 3.9G 0 3.9G 0% /dev/shm
- /dev/vda3 18G 4.7G 13G 27% /
- /dev/vda1 497M 158M 340M 32% /boot
- /dev/vdb1 197G 24G 164G 13% /data
- /dev/rbd0 20G 160M 20G 1% /var/lib/kubelet/pods/3f84af87-9f58-4c69-9e38-5ef234498133/volumes/kubernetes.io~csi/pvc-c8861c23-c03d-47aa-96f6-73c4d4093109/mount
這里我們就經(jīng)過了 Attach 和 Mount 兩個(gè)階段完成了 Volume 的持久化。但是對于上面我們使用的 NFS 就更加簡單了, 因?yàn)?NFS 存儲并沒有一個(gè)設(shè)備需要掛載到宿主機(jī)上面,所以這個(gè)時(shí)候 kubelet 就會直接進(jìn)入第二個(gè) Mount 階段,相當(dāng)于直接在宿主機(jī)上面執(zhí)行如下的命令:
- $ mount -t nfs 10.151.30.11:/data/k8s /var/lib/kubelet/pods/d4fcdb11-baf7-43d9-8d7d-3ede24118e08/volumes/kubernetes.io~nfs/nfs-pv
同樣可以在測試的 Pod 所在節(jié)點(diǎn)查看 Volume 的掛載信息:
- $ findmnt /var/lib/kubelet/pods/d4fcdb11-baf7-43d9-8d7d-3ede24118e08/volumes/kubernetes.io~nfs/nfs-pv
- TARGET SOURCE FSTYPE OPTIONS
- /var/lib/kubelet/pods/d4fcdb11-baf7-43d9-8d7d-3ede24118e08/volumes/kubernetes.io~nfs/nfs-pv
- 10.151.30.11:/data/k8s nfs4 rw,relatime,
我們可以看到這個(gè) Volume 被掛載到了 NFS(10.151.30.11:/data/k8s)下面,以后我們在這個(gè)目錄里寫入的所有文件,都會被保存在遠(yuǎn)程 NFS 服務(wù)器上。
這樣在經(jīng)過了上面的兩個(gè)階段過后,我們就得到了一個(gè)持久化的宿主機(jī)上面的 Volume 目錄了,接下來 kubelet 只需要把這個(gè) Volume 目錄掛載到容器中對應(yīng)的目錄即可,這樣就可以為 Pod 里的容器掛載這個(gè)持久化的 Volume 了,這一步其實(shí)也就相當(dāng)于執(zhí)行了如下所示的命令:
- $ docker run -v /var/lib/kubelet/pods/<Pod的ID>/volumes/kubernetes.io~<Volume類型>/<Volume名字>:/<容器內(nèi)的目標(biāo)目錄> 我的鏡像 ...
整個(gè)存儲的架構(gòu)可以用下圖來說明:

- PV Controller:負(fù)責(zé) PV/PVC 的綁定,并根據(jù)需求進(jìn)行數(shù)據(jù)卷的 Provision/Delete 操作
- AD Controller:負(fù)責(zé)存儲設(shè)備的 Attach/Detach 操作,將設(shè)備掛載到目標(biāo)節(jié)點(diǎn)
- Volume Manager:管理卷的 Mount/Unmount 操作、卷設(shè)備的格式化等操作
- Volume Plugin:擴(kuò)展各種存儲類型的卷管理能力,實(shí)現(xiàn)第三方存儲的各種操作能力和 Kubernetes 存儲系統(tǒng)結(jié)合
我們上面使用的 NFS 就屬于 In-Tree 這種方式,而上節(jié)課使用的 Ceph RBD 就是 Out-Of-Tree 的方式,而且是使用的是 CSI 插件。下面我們再來了解下 FlexVolume 和 CSI 兩種插件方式。
FlexVolume
FlexVolume 提供了一種擴(kuò)展 Kubernetes 存儲插件的方式,用戶可以自定義自己的存儲插件。要使用 FlexVolume 需要在每個(gè)節(jié)點(diǎn)上安裝存儲插件二進(jìn)制文件,該二進(jìn)制需要實(shí)現(xiàn) FlexVolume 的相關(guān)接口,默認(rèn)存儲插件的存放路徑為 /usr/libexec/kubernetes/kubelet-plugins/volume/exec/
其中 vendor~driver 的名字需要和 Pod 中 flexVolume.driver 的字段名字匹配,例如:
- /usr/libexec/kubernetes/kubelet-plugins/volume/exec/foo~cifs/cifs
對應(yīng)的 Pod 中的 flexVolume.driver 屬性為: foo/cifs 。
在我們實(shí)現(xiàn)自定義存儲插件的時(shí)候,需要實(shí)現(xiàn) FlexVolume 的部分接口,因?yàn)橐磳?shí)際需求,并不一定所有接口都需要實(shí)現(xiàn)。比如對于類似于 NFS 這樣的存儲就沒必要實(shí)現(xiàn) attach/detach 這些接口了,因?yàn)椴恍枰恍枰獙?shí)現(xiàn) init/mount/umount 3個(gè)接口即可。
- init:
init - kubelet/kube-controller-manager 初始化存儲插件時(shí)調(diào)用,插件需要返回是否需要要 attach 和 detach 操作 - attach:
attach - 將存儲卷掛載到 Node 節(jié)點(diǎn)上 - detach:
detach - 將存儲卷從 Node 上卸載 - waitforattach:
waitforattach - 等待 attach 操作成功(超時(shí)時(shí)間為 10 分鐘) - isattached:
isattached - 檢查存儲卷是否已經(jīng)掛載 - mountdevice:
mountdevice - 將設(shè)備掛載到指定目錄中以便后續(xù) bind mount 使用 - unmountdevice:
unmountdevice - 將設(shè)備取消掛載 - mount:
mount - 將存儲卷掛載到指定目錄中 - unmount:
unmount - 將存儲卷取消掛載
實(shí)現(xiàn)上面的這些接口需要返回如下所示的 JSON 格式的數(shù)據(jù):
- {
- "status": "<Success/Failure/Not supported>",
- "message": "<Reason for success/failure>",
- "device": "<Path to the device attached. This field is valid only for attach & waitforattach call-outs>"
- "volumeName": "<Cluster wide unique name of the volume. Valid only for getvolumename call-out>"
- "attached": <True/False (Return true if volume is attached on the node. Valid only for isattached call-out)>
- "capabilities": <Only included as part of the Init response>
- {
- "attach": <True/False (Return true if the driver implements attach and detach)>
- }
- }
比如我們來實(shí)現(xiàn)一個(gè) NFS 的 FlexVolume 插件,最簡單的方式就是寫一個(gè)腳本,然后實(shí)現(xiàn) init、mount、unmount 3個(gè)命令即可,然后按照上面的 JSON 格式返回?cái)?shù)據(jù),最后把這個(gè)腳本放在節(jié)點(diǎn)的 FlexVolume 插件目錄下面即可。
下面就是官方給出的一個(gè) NFS 的 FlexVolume 插件示例,可以從 https://github.com/kubernetes/examples/blob/master/staging/volumes/flexvolume/nfs 獲取腳本:
- #!/bin/bash
- # 注意:
- # - 在使用插件之前需要先安裝 jq。
- usage() {
- err "Invalid usage. Usage: "
- err "\t$0 init"
- err "\t$0 mount <mount dir> <json params>"
- err "\t$0 unmount <mount dir>"
- exit 1
- }
- err() {
- echo -ne $* 1>&2
- }
- log() {
- echo -ne $* >&1
- }
- ismounted() {
- MOUNT=`findmnt -n ${MNTPATH} 2>/dev/null | cut -d' ' -f1`
- if [ "${MOUNT}" == "${MNTPATH}" ]; then
- echo "1"
- else
- echo "0"
- fi
- }
- domount() {
- MNTPATH=$1
- NFS_SERVER=$(echo $2 | jq -r '.server')
- SHARE=$(echo $2 | jq -r '.share')
- if [ $(ismounted) -eq 1 ] ; then
- log '{"status": "Success"}'
- exit 0
- fi
- mkdir -p ${MNTPATH} &> /dev/null
- mount -t nfs ${NFS_SERVER}:/${SHARE} ${MNTPATH} &> /dev/null
- if [ $? -ne 0 ]; then
- err "{ \"status\": \"Failure\", \"message\": \"Failed to mount ${NFS_SERVER}:${SHARE} at ${MNTPATH}\"}"
- exit 1
- fi
- log '{"status": "Success"}'
- exit 0
- }
- unmount() {
- MNTPATH=$1
- if [ $(ismounted) -eq 0 ] ; then
- log '{"status": "Success"}'
- exit 0
- fi
- umount ${MNTPATH} &> /dev/null
- if [ $? -ne 0 ]; then
- err "{ \"status\": \"Failed\", \"message\": \"Failed to unmount volume at ${MNTPATH}\"}"
- exit 1
- fi
- log '{"status": "Success"}'
- exit 0
- }
- op=$1
- if ! command -v jq >/dev/null 2>&1; then
- err "{ \"status\": \"Failure\", \"message\": \"'jq' binary not found. Please install jq package before using this driver\"}"
- exit 1
- fi
- if [ "$op" = "init" ]; then
- log '{"status": "Success", "capabilities": {"attach": false}}'
- exit 0
- fi
- if [ $# -lt 2 ]; then
- usage
- fi
- shift
- case "$op" in
- mount)
- domount $*
- ;;
- unmount)
- unmount $*
- ;;
- *)
- log '{"status": "Not supported"}'
- exit 0
- esac
- exit 1
將上面腳本命名成 nfs,放置到 node1 節(jié)點(diǎn)對應(yīng)的插件下面: /usr/libexec/kubernetes/kubelet-plugins/volume/exec/ydzs~nfs/nfs ,并設(shè)置權(quán)限為 700:
- $ chmod 700 /usr/libexec/kubernetes/kubelet-plugins/volume/exec/ydzs~nfs/nfs
- # 安裝 jq 工具
- $ yum -y install https://dl.fedoraproject.org/pub/epel/epel-release-latest-7.noarch.rpm
- $ yum install jq -y
這個(gè)時(shí)候我們部署一個(gè)應(yīng)用到 node1 節(jié)點(diǎn)上,并用 flexVolume 來持久化容器中的數(shù)據(jù)(當(dāng)然也可以通過定義 flexvolume 類型的 PV、PVC 來使用),如下所示:(test-flexvolume.yaml)
- apiVersion: v1
- kind: Pod
- metadata:
- name: test-flexvolume
- spec:
- nodeSelector:
- kubernetes.io/hostname: ydzs-node1
- volumes:
- - name: test
- flexVolume:
- driver: "ydzs/nfs" # 定義插件類型,根據(jù)這個(gè)參數(shù)在對應(yīng)的目錄下面找到插件的可執(zhí)行文件
- fsType: "nfs" # 定義存儲卷文件系統(tǒng)類型
- options: # 定義所有與存儲相關(guān)的一些具體參數(shù)
- server: "10.151.30.11"
- share: "data/k8s"
- containers:
- - name: web
- image: nginx
- ports:
- - containerPort: 80
- volumeMounts:
- - name: test
- subPath: testflexvolume
- mountPath: /usr/share/nginx/html
其中 flexVolume.driver 就是插件目錄 ydzs~nfs 對應(yīng)的 ydzs/nfs 名稱, flexVolume.options 中根據(jù)上面的 nfs 腳本可以得知里面配置的是 NFS 的 Server 地址和掛載目錄路徑,直接創(chuàng)建上面的資源對象:
- $ kubectl apply -f test-flexvolume.yaml
- $ kubectl get pods
- NAME READY STATUS RESTARTS AGE
- test-flexvolume 1/1 Running 0 13h
- ......
- $ kubectl exec -it test-flexvolume mount |grep test
- 10.151.30.11:/data/k8s/testflexvolume on /usr/share/nginx/html type nfs4 (rw,relatime,vers=4.1,rsize=524288,wsize=524288,namlen=255,hard,proto=tcp,timeo=600,retrans=2,sec=sys,clientaddr=10.151.30.22,local_lock=none,addr=10.151.30.11)
- $ mount |grep test
- 10.151.30.11:/data/k8s on /var/lib/kubelet/pods/a376832a-7638-4faf-b1a0-404956e8e60a/volumes/ydzs~nfs/test type nfs4 (rw,relatime,vers=4.1,rsize=524288,wsize=524288,namlen=255,hard,proto=tcp,timeo=600,retrans=2,sec=sys,clientaddr=10.151.30.22,local_lock=none,addr=10.151.30.11)
- 10.151.30.11:/data/k8s/testflexvolume on /var/lib/kubelet/pods/a376832a-7638-4faf-b1a0-404956e8e60a/volume-subpaths/test/web/0 type nfs4 (rw,relatime,vers=4.1,rsize=524288,wsize=524288,namlen=255,hard,proto=tcp,timeo=600,retrans=2,sec=sys,clientaddr=10.151.30.22,local_lock=none,addr=10.151.30.11)
同樣我們可以查看到 Pod 的本地持久化目錄是被 mount 到了 NFS 上面,證明上面我們的 FlexVolume 插件是正常的。
“
當(dāng)我們要去真正的 mount NFS 的時(shí)候,就是通過 kubelet 調(diào)用 VolumePlugin,然后直接執(zhí)行命令 /usr/libexec/kubernetes/kubelet-plugins/volume/exec/ydzs~nfs/nfs mount
”
CSI
既然已經(jīng)有了 FlexVolume 插件了,為什么還需要 CSI 插件呢?上面我們使用 FlexVolume 插件的時(shí)候可以看出 FlexVolume 插件實(shí)際上相當(dāng)于就是一個(gè)普通的 shell 命令,類似于平時(shí)我們在 Linux 下面執(zhí)行的 ls 命令一樣,只是返回的信息是 JSON 格式的數(shù)據(jù),并不是我們通常認(rèn)為的一個(gè)常駐內(nèi)存的進(jìn)程,而 CSI 是一個(gè)更加完善、編碼更加方便友好的一種存儲插件擴(kuò)展方式。
CSI 是由來自 Kubernetes、Mesos、 Cloud Foundry 等社區(qū)成員聯(lián)合制定的一個(gè)行業(yè)標(biāo)準(zhǔn)接口規(guī)范,旨在將任意存儲系統(tǒng)暴露給容器化應(yīng)用程序。CSI 規(guī)范定義了存儲提供商實(shí)現(xiàn) CSI 兼容插件的最小操作集合和部署建議,CSI 規(guī)范的主要焦點(diǎn)是聲明插件必須實(shí)現(xiàn)的接口。
在 Kubernetes 上整合 CSI 插件的整體架構(gòu)如下圖所示:

Kubernetes CSI 存儲體系主要由兩部分組成:
- Kubernetes 外部組件:包含 Driver registrar、External provisioner、External attacher 三部分,這三個(gè)組件是從 Kubernetes 原本的 in-tree 存儲體系中剝離出來的存儲管理功能,實(shí)際上是 Kubernetes 中的一種外部 controller ,它們 watch kubernetes 的 API 資源對象,根據(jù) watch 到的狀態(tài)來調(diào)用下面提到的第二部分的 CSI 插件來實(shí)現(xiàn)存儲的管理和操作。這部分是 Kubernetes 團(tuán)隊(duì)維護(hù)的,插件開發(fā)者完全不必關(guān)心其實(shí)現(xiàn)細(xì)節(jié)。
- Driver registra:用于將插件注冊到 kubelet 的 sidecar 容器,并將驅(qū)動(dòng)程序自定義的 NodeId 添加到節(jié)點(diǎn)的 Annotations 上,通過與 CSI 上面的 Identity 服務(wù)進(jìn)行通信調(diào)用 CSI 的 GetNodeId 方法來完成該操作。
- External provisioner:用于 watch Kubernetes 的 PVC 對象并調(diào)用 CSI 的 CreateVolume 和 DeleteVolume 操作。
- External attacher:用于 Attach/Detach 階段,通過 watch Kubernetes 的 VolumeAttachment 對象并調(diào)用 CSI 的 ControllerPublish 和 ControllerUnpublish 操作來完成對應(yīng)的 Volume 的 Attach/Detach。而 Volume 的 Mount/Unmount 階段并不屬于外部組件,當(dāng)真正需要執(zhí)行 Mount 操作的時(shí)候,kubelet 會去直接調(diào)用下面的 CSI Node 服務(wù)來完成 Volume 的 Mount/UnMount 操作。
- CSI 存儲插件: 這部分正是開發(fā)者需要實(shí)現(xiàn)的 CSI 插件部分,都是通過 gRPC 實(shí)現(xiàn)的服務(wù),一般會用一個(gè)二進(jìn)制文件對外提供服務(wù),主要包含三部分:CSI Identity、CSI Controller、CSI Node。
- CSI Identity — 主要用于負(fù)責(zé)對外暴露這個(gè)插件本身的信息,確保插件的健康狀態(tài)。service Identity {
// 返回插件的名稱和版本
rpc GetPluginInfo(GetPluginInfoRequest)returns (GetPluginInfoResponse) {}// 返回這個(gè)插件的包含的功能,比如非塊存儲類型的 CSI 插件不需要實(shí)現(xiàn) Attach 功能,GetPluginCapabilities 就可以在返回中標(biāo)注這個(gè) CSI 插件不包含 Attach 功能
rpc GetPluginCapabilities(GetPluginCapabilitiesRequest)returns (GetPluginCapabilitiesResponse) {}// 插件插件是否正在運(yùn)行
rpc Probe (ProbeRequest)returns (ProbeResponse) {}}
- CSI Controller - 主要實(shí)現(xiàn) Volume 管理流程當(dāng)中的 Provision 和 Attach 階段,Provision 階段是指創(chuàng)建和刪除 Volume 的流程,而 Attach 階段是指把存儲卷附著在某個(gè)節(jié)點(diǎn)或脫離某個(gè)節(jié)點(diǎn)的流程,另外只有塊存儲類型的 CSI 插件才需要 Attach 功能。
- service Controller {
- // 創(chuàng)建存儲卷,包括云端存儲介質(zhì)以及PV對象
- rpc CreateVolume (CreateVolumeRequest)
- returns (CreateVolumeResponse) {}
- // 刪除存儲卷
- rpc DeleteVolume (DeleteVolumeRequest)
- returns (DeleteVolumeResponse) {}
- // 掛載存儲卷,將存儲介質(zhì)掛載到目標(biāo)節(jié)點(diǎn)
- rpc ControllerPublishVolume (ControllerPublishVolumeRequest)
- returns (ControllerPublishVolumeResponse) {}
- // 卸載存儲卷
- rpc ControllerUnpublishVolume (ControllerUnpublishVolumeRequest)
- returns (ControllerUnpublishVolumeResponse) {}
- // 例如:是否可以同時(shí)用于多個(gè)節(jié)點(diǎn)的讀/寫
- rpc ValidateVolumeCapabilities (ValidateVolumeCapabilitiesRequest)
- returns (ValidateVolumeCapabilitiesResponse) {}
- // 返回所有可用的 volumes
- rpc ListVolumes (ListVolumesRequest)
- returns (ListVolumesResponse) {}
- // 可用存儲池的總?cè)萘?nbsp;
- rpc GetCapacity (GetCapacityRequest)
- returns (GetCapacityResponse) {}
- // 例如. 插件可能未實(shí)現(xiàn) GetCapacity、Snapshotting
- rpc ControllerGetCapabilities (ControllerGetCapabilitiesRequest)
- returns (ControllerGetCapabilitiesResponse) {}
- // 創(chuàng)建快照
- rpc CreateSnapshot (CreateSnapshotRequest)
- returns (CreateSnapshotResponse) {}
- // 刪除指定的快照
- rpc DeleteSnapshot (DeleteSnapshotRequest)
- returns (DeleteSnapshotResponse) {}
- // 獲取所有的快照
- rpc ListSnapshots (ListSnapshotsRequest)
- returns (ListSnapshotsResponse) {}
- }
- CSI Node — 負(fù)責(zé)控制 Kubernetes 節(jié)點(diǎn)上的 Volume 操作。其中 Volume 的掛載被分成了 NodeStageVolume 和 NodePublishVolume 兩個(gè)階段。NodeStageVolume 接口主要是針對塊存儲類型的 CSI 插件而提供的,塊設(shè)備在 "Attach" 階段被附著在 Node 上后,需要掛載至 Pod 對應(yīng)目錄上,但因?yàn)閴K設(shè)備在 linux 上只能 mount 一次,而在 kubernetes volume 的使用場景中,一個(gè) volume 可能被掛載進(jìn)同一個(gè) Node 上的多個(gè) Pod 實(shí)例中,所以這里提供了 NodeStageVolume 這個(gè)接口,使用這個(gè)接口把塊設(shè)備格式化后先掛載至 Node 上的一個(gè)臨時(shí)全局目錄,然后再調(diào)用 NodePublishVolume 使用 linux 中的 bind mount 技術(shù)把這個(gè)全局目錄掛載進(jìn) Pod 中對應(yīng)的目錄上。
- service Node {
- // 在節(jié)點(diǎn)上初始化存儲卷(格式化),并執(zhí)行掛載到Global目錄
- rpc NodeStageVolume (NodeStageVolumeRequest)
- returns (NodeStageVolumeResponse) {}
- // umount 存儲卷在節(jié)點(diǎn)上的 Global 目錄
- rpc NodeUnstageVolume (NodeUnstageVolumeRequest)
- returns (NodeUnstageVolumeResponse) {}
- // 在節(jié)點(diǎn)上將存儲卷的 Global 目錄掛載到 Pod 的實(shí)際掛載目錄
- rpc NodePublishVolume (NodePublishVolumeRequest)
- returns (NodePublishVolumeResponse) {}
- // unmount 存儲卷在節(jié)點(diǎn)上的 Pod 掛載目錄
- rpc NodeUnpublishVolume (NodeUnpublishVolumeRequest)
- returns (NodeUnpublishVolumeResponse) {}
- // 獲取節(jié)點(diǎn)上Volume掛載文件系統(tǒng)統(tǒng)計(jì)信息(總空間、可用空間等)
- rpc NodeGetVolumeStats (NodeGetVolumeStatsRequest)
- returns (NodeGetVolumeStatsResponse) {}
- // 獲取節(jié)點(diǎn)的唯一 ID
- rpc NodeGetId (NodeGetIdRequest)
- returns (NodeGetIdResponse) {
- option deprecated = true;
- }
- // 返回節(jié)點(diǎn)插件的能力
- rpc NodeGetCapabilities (NodeGetCapabilitiesRequest)
- returns (NodeGetCapabilitiesResponse) {}
- // 獲取節(jié)點(diǎn)的一些信息
- rpc NodeGetInfo (NodeGetInfoRequest)
- returns (NodeGetInfoResponse) {}
- }
只需要實(shí)現(xiàn)上面的接口就可以實(shí)現(xiàn)一個(gè) CSI 插件了。雖然 Kubernetes 并未規(guī)定 CSI 插件的打包安裝,但是提供了以下建議來簡化我們在 Kubernetes 上容器化 CSI Volume 驅(qū)動(dòng)程序的部署方案,具體的方案介紹可以查看 CSI 規(guī)范介紹文檔 https://github.com/kubernetes/community

container storage interface deploy
按照上圖的推薦方案,CSI Controller 部分以 StatefulSet 或者 Deployment 方式部署,CSI Node 部分以 DaemonSet 方式部署。因?yàn)檫@兩部分實(shí)現(xiàn)在同一個(gè) CSI 插件程序中,因此只需要把這個(gè) CSI 插件與 External Components 以容器方式部署在同一個(gè) Pod中,把這個(gè) CSI 插件與 Driver registrar 以容器方式部署在 DaemonSet 的 Pod 中,即可完成 CSI 的部署。
前面我們使用的 Rook 部署的 Ceph 集群就是實(shí)現(xiàn)了 CSI 插件的:
- $ kubectl get pods -n rook-ceph |grep plugin
- csi-cephfsplugin-2s9d5 3/3 Running 0 21d
- csi-cephfsplugin-fgp4v 3/3 Running 0 17d
- csi-cephfsplugin-fv5nx 3/3 Running 0 21d
- csi-cephfsplugin-mn8q4 3/3 Running 0 17d
- csi-cephfsplugin-nf6h8 3/3 Running 0 21d
- csi-cephfsplugin-provisioner-56c8b7ddf4-68h6d 4/4 Running 0 21d
- csi-cephfsplugin-provisioner-56c8b7ddf4-rq4t6 4/4 Running 0 21d
- csi-cephfsplugin-xwnl4 3/3 Running 0 21d
- csi-rbdplugin-7r88w 3/3 Running 0 21d
- csi-rbdplugin-95g5j 3/3 Running 0 21d
- csi-rbdplugin-bnzpr 3/3 Running 0 21d
- csi-rbdplugin-dvftb 3/3 Running 0 21d
- csi-rbdplugin-jzmj2 3/3 Running 0 17d
- csi-rbdplugin-provisioner-6ff4dd4b94-bvtss 5/5 Running 0 21d
- csi-rbdplugin-provisioner-6ff4dd4b94-lfn68 5/5 Running 0 21d
- csi-rbdplugin-trxb4 3/3 Running 0 17d
這里其實(shí)是實(shí)現(xiàn)了 RBD 和 CephFS 兩種 CSI,用 DaemonSet 在每個(gè)節(jié)點(diǎn)上運(yùn)行了一個(gè)包含 Driver registra 容器的 Pod,當(dāng)然和節(jié)點(diǎn)相關(guān)的操作比如 Mount/Unmount 也是在這個(gè) Pod 里面執(zhí)行的,其他的比如 Provision、Attach 都是在另外的 csi-rbdplugin-provisioner-xxx Pod 中執(zhí)行的。