首頁>技術>

前面的章節中我們介紹了在 Kubernetes 中的持久化儲存的使用,瞭解了 PV、PVC 以及 StorageClass 的使用方法,從本地儲存到 Ceph 共享儲存都有學習,到這裡我們其實已經可以完成應用各種場景的資料持久化了,但是難免在實際的使用過程中會遇到各種各樣的問題,要解決這些問題最好的方式就是來了解下 Kubernetes 中儲存的實現原理。

Kubernetes 預設情況下就提供了主流的儲存卷接入方案,我們可以執行命令 kubectl explain pod.spec.volumes 檢視到支援的各種儲存卷,另外也提供了外掛機制,允許其他型別的儲存服務接入到 Kubernetes 系統中來,在 Kubernetes 中就對應 In-Tree 和 Out-Of-Tree 兩種方式, In-Tree 就是在 Kubernetes 原始碼內部實現的,和 Kubernetes 一起釋出、管理的,但是更新迭代慢、靈活性比較差, Out-Of-Tree 是獨立於 Kubernetes 的,目前主要有 CSI 和 FlexVolume 兩種機制,開發者可以根據自己的儲存型別實現不同的儲存外掛接入到 Kubernetes 中去,其中 CSI 是現在也是以後主流的方式,所以當然我們的重點也會是 CSI 的使用介紹。

NFS

我們這裡為了演示方便,先使用相對簡單的 NFS 這種儲存資源,接下來我們在節點 10.151.30.11 上來安裝 NFS 服務,資料目錄: /data/k8s/

關閉防火牆

$ systemctl stop firewalld.service$ systemctl disable firewalld.service

安裝配置 nfs

$ yum -y install nfs-utils rpcbind

共享目錄設定許可權:

$ mkdir -p /data/k8s/$ chmod 755 /data/k8s/

配置 nfs,nfs 的預設配置檔案在 /etc/exports 檔案下,在該檔案中新增下面的配置資訊:

$ vi /etc/exports/data/k8s  *(rw,sync,no_root_squash)

配置說明:

/data/k8s:是共享的資料目錄*:表示任何人都有許可權連線,當然也可以是一個網段,一個 IP,也可以是域名rw:讀寫的許可權sync:表示檔案同時寫入硬碟和記憶體no_root_squash:當登入 NFS 主機使用共享目錄的使用者是 root 時,其許可權將被轉換成為匿名使用者,通常它的 UID 與 GID,都會變成 nobody 身份

當然 nfs 的配置還有很多,感興趣的同學可以在網上去查詢一下。

啟動服務 nfs 需要向 rpc 註冊,rpc 一旦重啟了,註冊的檔案都會丟失,向他註冊的服務都需要重啟 注意啟動順序,先啟動 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 -wJul 10 20:57:29 master systemd[1]: Starting RPC bind service...Jul 10 20:57:29 master systemd[1]: Started RPC bind service.

看到上面的 Started 證明啟動成功了。

然後啟動 nfs 服務:

$ 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.serviceJul 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 啟動成功了。

另外我們還可以透過下面的命令確認下:

$ 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

檢視具體目錄掛載許可權:

$ 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 給安裝成功了,然後就是前往節點安裝 nfs 的客戶端來驗證,安裝 nfs 當前也需要先關閉防火牆:

$ systemctl stop firewalld.service$ systemctl disable firewalld.service

然後安裝 nfs

$ yum -y install nfs-utils rpcbind

安裝完成後,和上面的方法一樣,先啟動 rpc、然後啟動 nfs:

$ systemctl start rpcbind.service $ systemctl enable rpcbind.service $ systemctl start nfs.service    $ systemctl enable nfs.service

掛載資料目錄 客戶端啟動完成後,我們在客戶端來掛載下 nfs 測試下,首先檢查下 nfs 是否有共享目錄:

$ showmount -e 10.151.30.11Export 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

掛載成功後,在客戶端上面的目錄中新建一個檔案,然後我們觀察下 nfs 服務端的共享目錄下面是否也會出現該檔案:

$ touch /root/course/kubeadm/data/test.txt

然後在 nfs 服務端檢視:

$ ls -ls /data/k8s/total 44 -rw-r--r--. 1 root root 4 Jul 10 21:50 test.txt

如果上面出現了 test.txt 的檔案,那麼證明我們的 nfs 掛載成功了。

儲存架構

前面我們瞭解到了 PV、PVC、StorgeClass 的使用,但是他們是如何和我們的 Pod 關聯起來使用的呢?這就需要從 Volume 的處理流程和原理說起了。

如下所示,我們建立了一個 nfs 型別的 PV 資源物件:(volume.yaml)

apiVersion: v1kind: PersistentVolumemetadata:  name: nfs-pvspec:  storageClassName: manual  capacity:     storage: 1Gi  accessModes:  - ReadWriteOnce  persistentVolumeReclaimPolicy: Retain  nfs:    path: /data/k8s  # 指定nfs的掛載點    server: 10.151.30.11  # 指定nfs服務地址---apiVersion: v1kind: PersistentVolumeClaimmetadata:  name: nfs-pvcspec:  storageClassName: manual  accessModes:  - ReadWriteOnce  resources:    requests:      storage: 1Gi

我們知道使用者真正使用的是 PVC,而要使用 PVC 的前提就是必須要先和某個符合條件的 PV 進行一一繫結,比如儲存容器、訪問模式,以及 PV 和 PVC 的 storageClassName 欄位必須一樣,這樣才能夠進行繫結,當 PVC 和 PV 繫結成功後就可以直接使用這個 PVC 物件了:(pod.yaml)

apiVersion: v1kind: Podmetadata:  name: test-volumesspec:  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"

直接建立上面的資源物件即可:

$ kubectl apply -f volume.yaml$ kubectl apply -f pod.yaml

我們只是在 volumes 中指定了我們上面建立的 PVC 物件,當這個 Pod 被建立之後, kubelet 就會把這個 PVC 對應的這個 NFS 型別的 Volume(PV)掛載到這個 Pod 容器中的目錄中去。前面我們也提到了這樣的話對於普通使用者來說完全就不用關心後面的具體儲存在 NFS 還是 Ceph 或者其他了,只需要直接使用 PVC 就可以了,因為真正的儲存是需要很多相關的專業知識的,這樣就完全職責分離解耦了。

普通使用者直接使用 PVC 沒有問題,但是也會出現一個問題,那就是當普通使用者建立一個 PVC 物件的時候,這個時候系統裡面並沒有合適的 PV 來和它進行繫結,因為 PV 大多數情況下是管理員給我們建立的,這個時候啟動 Pod 肯定就會失敗了,如果現在管理員如果去建立一個對應的 PV 的話,PVC 和 PV 當然就可以綁定了,然後 Pod 也會自動的啟動成功,這是因為在 Kubernetes 中有一個專門處理持久化儲存的控制器 Volume Controller,這個控制器下面有很多個控制迴圈,其中一個就是用於 PV 和 PVC 繫結的 PersistentVolumeController。

PersistentVolumeController 會不斷地迴圈去檢視每一個 PVC,是不是已經處於 Bound(已繫結)狀態。如果不是,那它就會遍歷所有的、可用的 PV,並嘗試將其與未繫結的 PVC 進行繫結,這樣,Kubernetes 就可以保證使用者提交的每一個 PVC,只要有合適的 PV 出現,它就能夠很快進入繫結狀態。而所謂將一個 PV 與 PVC 進行 “繫結” ,其實就是將這個 PV 物件的名字,填在了 PVC 物件的 spec.volumeName 欄位上。

PV 和 PVC 繫結上了,那麼又是如何將容器裡面的資料進行持久化的呢,前面我們學習過 Docker 的 Volume 掛載,其實就是 將一個宿主機上的目錄和一個容器裡的目錄繫結掛載在了一起 ,具有持久化功能當然就是指的宿主機上面的這個目錄了,當容器被刪除或者在其他節點上重建出來以後,這個目錄裡面的內容依然存在,所以一般情況下實現持久化是需要一個遠端儲存的,比如 NFS、Ceph 或者雲廠商提供的磁碟等等。所以接下來需要做的就是持久化宿主機目錄這個過程。

當 Pod 被排程到一個節點上後,節點上的 kubelet 元件就會為這個 Pod 建立它的 Volume 目錄,預設情況下 kubelet 為 Volume 建立的目錄在 kubelet 工作目錄下面:

/var/lib/kubelet/pods/<Pod的ID>/volumes/kubernetes.io~<Volume型別>/<Volume名字>

比如上面我們建立的 Pod 對應的 Volume 目錄完整路徑為:

/var/lib/kubelet/pods/d4fcdb11-baf7-43d9-8d7d-3ede24118e08/volumes/kubernetes.io~nfs/nfs-pv

要獲取 Pod 的唯一標識 uid,可透過命令 kubectl get pod pod名 -o jsonpath={.metadata.uid} 獲取。

然後就需要根據我們的 Volume 型別來決定需要做什麼操作了,比如上節課我們用的 Ceph RBD,那麼 kubelet 就需要先將 Ceph 提供的 RBD 掛載到 Pod 所在的宿主機上面,這個階段在 Kubernetes 中被稱為 Attach 階段。Attach 階段完成後,為了能夠使用這個塊裝置,kubelet 還要進行第二個操作,即:格式化這個塊裝置,然後將它掛載到宿主機指定的掛載點上。這個掛載點,也就是上面我們提到的 Volume 的宿主機的目錄。將塊裝置格式化並掛載到 Volume 宿主機目錄的操作,在 Kubernetes 中被稱為 Mount 階段。上節課我們使用 Ceph RBD 持久化的 Wordpress 的 MySQL 資料,我們可以檢視對應的 Volume 資訊:

$ kubectl get pods -o wide -l app=wordpressNAME                              READY   STATUS    RESTARTS   AGE   IP             NODE         NOMINATED NODE   READINESS GATESwordpress-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 執行在 node4 節點上,然後可以在該節點上檢視 Volume 資訊,Pod 對應的 uid 可以透過如下命令獲取:

$ 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/mountTARGET                                                                                            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 這個裝置上面的,透過 df 命令也是可以看到的:

$ df -h |grep devdevtmpfs        3.9G     0  3.9G   0% /devtmpfs           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

這裡我們就經過了 Attach 和 Mount 兩個階段完成了 Volume 的持久化。但是對於上面我們使用的 NFS 就更加簡單了, 因為 NFS 儲存並沒有一個裝置需要掛載到宿主機上面,所以這個時候 kubelet 就會直接進入第二個 Mount 階段,相當於直接在宿主機上面執行如下的命令:

$ 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 所在節點檢視 Volume 的掛載資訊:

$ findmnt /var/lib/kubelet/pods/d4fcdb11-baf7-43d9-8d7d-3ede24118e08/volumes/kubernetes.io~nfs/nfs-pvTARGET                                                                               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,

我們可以看到這個 Volume 被掛載到了 NFS(10.151.30.11:/data/k8s)下面,以後我們在這個目錄裡寫入的所有檔案,都會被儲存在遠端 NFS 伺服器上。

這樣在經過了上面的兩個階段過後,我們就得到了一個持久化的宿主機上面的 Volume 目錄了,接下來 kubelet 只需要把這個 Volume 目錄掛載到容器中對應的目錄即可,這樣就可以為 Pod 裡的容器掛載這個持久化的 Volume 了,這一步其實也就相當於執行了如下所示的命令:

$ docker run -v /var/lib/kubelet/pods/<Pod的ID>/volumes/kubernetes.io~<Volume型別>/<Volume名字>:/<容器內的目標目錄> 我的映象 ...

整個儲存的架構可以用下圖來說明:

PV Controller:負責 PV/PVC 的繫結,並根據需求進行資料卷的 Provision/Delete 操作AD Controller:負責儲存裝置的 Attach/Detach 操作,將裝置掛載到目標節點Volume Manager:管理卷的 Mount/Unmount 操作、卷裝置的格式化等操作Volume Plugin:擴充套件各種儲存型別的卷管理能力,實現第三方儲存的各種操作能力和 Kubernetes 儲存系統結合

我們上面使用的 NFS 就屬於 In-Tree 這種方式,而上節課使用的 Ceph RBD 就是 Out-Of-Tree 的方式,而且是使用的是 CSI 外掛。下面我們再來了解下 FlexVolume 和 CSI 兩種外掛方式。

FlexVolume

FlexVolume 提供了一種擴充套件 Kubernetes 儲存外掛的方式,使用者可以自定義自己的儲存外掛。要使用 FlexVolume 需要在每個節點上安裝儲存外掛二進位制檔案,該二進位制需要實現 FlexVolume 的相關介面,預設儲存外掛的存放路徑為 /usr/libexec/kubernetes/kubelet-plugins/volume/exec/<vendor~driver>/<driver> , VolumePlugins 元件會不斷 watch 這個目錄來實現外掛的新增、刪除等功能。

其中 vendor~driver 的名字需要和 Pod 中 flexVolume.driver 的欄位名字匹配,例如:

/usr/libexec/kubernetes/kubelet-plugins/volume/exec/foo~cifs/cifs

對應的 Pod 中的 flexVolume.driver 屬性為: foo/cifs 。

在我們實現自定義儲存外掛的時候,需要實現 FlexVolume 的部分介面,因為要看實際需求,並不一定所有介面都需要實現。比如對於類似於 NFS 這樣的儲存就沒必要實現 attach/detach 這些介面了,因為不需要,只需要實現 init/mount/umount 3個介面即可。

init: <driver executable> init - kubelet/kube-controller-manager 初始化儲存外掛時呼叫,外掛需要返回是否需要要 attach 和 detach 操作attach: <driver executable> attach <json options> <node name> - 將儲存卷掛載到 Node 節點上detach: <driver executable> detach <mount device> <node name> - 將儲存卷從 Node 上解除安裝waitforattach: <driver executable> waitforattach <mount device> <json options> - 等待 attach 操作成功(超時時間為 10 分鐘)isattached: <driver executable> isattached <json options> <node name> - 檢查儲存卷是否已經掛載mountdevice: <driver executable> mountdevice <mount dir> <mount device> <json options> - 將裝置掛載到指定目錄中以便後續 bind mount 使用unmountdevice: <driver executable> unmountdevice <mount device> - 將裝置取消掛載mount: <driver executable> mount <mount dir> <json options> - 將儲存卷掛載到指定目錄中unmount: <driver executable> unmount <mount dir> - 將儲存卷取消掛載

實現上面的這些介面需要返回如下所示的 JSON 格式的資料:

{    "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)>    }}

比如我們來實現一個 NFS 的 FlexVolume 外掛,最簡單的方式就是寫一個指令碼,然後實現 init、mount、unmount 3個命令即可,然後按照上面的 JSON 格式返回資料,最後把這個指令碼放在節點的 FlexVolume 外掛目錄下面即可。

下面就是官方給出的一個 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=$1if ! 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 1fiif [ "$op" = "init" ]; then log '{"status": "Success", "capabilities": {"attach": false}}' exit 0fiif [ $# -lt 2 ]; then usagefishiftcase "$op" in mount)  domount $*  ;; unmount)  unmount $*  ;; *)  log '{"status": "Not supported"}'  exit 0esacexit 1

將上面指令碼命名成 nfs,放置到 node1 節點對應的外掛下面: /usr/libexec/kubernetes/kubelet-plugins/volume/exec/ydzs~nfs/nfs ,並設定許可權為 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

這個時候我們部署一個應用到 node1 節點上,並用 flexVolume 來持久化容器中的資料(當然也可以透過定義 flexvolume 型別的 PV、PVC 來使用),如下所示:(test-flexvolume.yaml)

apiVersion: v1kind: Podmetadata:  name: test-flexvolumespec:  nodeSelector:    kubernetes.io/hostname: ydzs-node1  volumes:  - name: test    flexVolume:      driver: "ydzs/nfs"  # 定義外掛型別,根據這個引數在對應的目錄下面找到外掛的可執行檔案      fsType: "nfs"  # 定義儲存卷檔案系統型別      options:  # 定義所有與儲存相關的一些具體引數        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 對應的 ydzs/nfs 名稱, flexVolume.options 中根據上面的 nfs 指令碼可以得知裡面配置的是 NFS 的 Server 地址和掛載目錄路徑,直接建立上面的資源物件:

$ kubectl apply -f test-flexvolume.yaml$ kubectl get pods NAME                                      READY   STATUS    RESTARTS   AGEtest-flexvolume                           1/1     Running   0          13h......$ kubectl exec -it test-flexvolume mount |grep test10.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 test10.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 外掛是正常的。

當我們要去真正的 mount NFS 的時候,就是透過 kubelet 呼叫 VolumePlugin,然後直接執行命令 /usr/libexec/kubernetes/kubelet-plugins/volume/exec/ydzs~nfs/nfs mount <mount dir> <json param> 來完成的,就相當於平時我們在宿主機上面手動掛載 NFS 的方式一樣的,所以儲存外掛 nfs 是一個可執行的二進位制檔案或者 shell 指令碼都是可以的。

CSI

既然已經有了 FlexVolume 外掛了,為什麼還需要 CSI 外掛呢?上面我們使用 FlexVolume 外掛的時候可以看出 FlexVolume 外掛實際上相當於就是一個普通的 shell 命令,類似於平時我們在 Linux 下面執行的 ls 命令一樣,只是返回的資訊是 JSON 格式的資料,並不是我們通常認為的一個常駐記憶體的程序,而 CSI 是一個更加完善、編碼更加方便友好的一種儲存外掛擴充套件方式。

CSI 是由來自 Kubernetes、Mesos、 Cloud Foundry 等社群成員聯合制定的一個行業標準介面規範,旨在將任意儲存系統暴露給容器化應用程式。CSI 規範定義了儲存提供商實現 CSI 相容外掛的最小操作集合和部署建議,CSI 規範的主要焦點是宣告外掛必須實現的介面。

在 Kubernetes 上整合 CSI 外掛的整體架構如下圖所示:

Kubernetes CSI 儲存體系主要由兩部分組成:

Kubernetes 外部元件:包含 Driver registrar、External provisioner、External attacher 三部分,這三個元件是從 Kubernetes 原本的 in-tree 儲存體系中剝離出來的儲存管理功能,實際上是 Kubernetes 中的一種外部 controller ,它們 watch kubernetes 的 API 資源物件,根據 watch 到的狀態來呼叫下面提到的第二部分的 CSI 外掛來實現儲存的管理和操作。這部分是 Kubernetes 團隊維護的,外掛開發者完全不必關心其實現細節。Driver registra:用於將外掛註冊到 kubelet 的 sidecar 容器,並將驅動程式自定義的 NodeId 新增到節點的 Annotations 上,透過與 CSI 上面的 Identity 服務進行通訊呼叫 CSI 的 GetNodeId 方法來完成該操作。External provisioner:用於 watch Kubernetes 的 PVC 物件並呼叫 CSI 的 CreateVolume 和 DeleteVolume 操作。External attacher:用於 Attach/Detach 階段,透過 watch Kubernetes 的 VolumeAttachment 物件並呼叫 CSI 的 ControllerPublish 和 ControllerUnpublish 操作來完成對應的 Volume 的 Attach/Detach。而 Volume 的 Mount/Unmount 階段並不屬於外部元件,當真正需要執行 Mount 操作的時候,kubelet 會去直接呼叫下面的 CSI Node 服務來完成 Volume 的 Mount/UnMount 操作。CSI 儲存外掛: 這部分正是開發者需要實現的 CSI 外掛部分,都是透過 gRPC 實現的服務,一般會用一個二進位制檔案對外提供服務,主要包含三部分:CSI Identity、CSI Controller、CSI Node。CSI Identity — 主要用於負責對外暴露這個外掛本身的資訊,確保外掛的健康狀態。service Identity {// 返回外掛的名稱和版本rpc GetPluginInfo(GetPluginInfoRequest)returns (GetPluginInfoResponse) {}// 返回這個外掛的包含的功能,比如非塊儲存型別的 CSI 外掛不需要實現 Attach 功能,GetPluginCapabilities 就可以在返回中標註這個 CSI 外掛不包含 Attach 功能rpc GetPluginCapabilities(GetPluginCapabilitiesRequest)returns (GetPluginCapabilitiesResponse) {}// 外掛外掛是否正在執行rpc Probe (ProbeRequest)returns (ProbeResponse) {}}CSI Controller - 主要實現 Volume 管理流程當中的 Provision 和 Attach 階段,Provision 階段是指建立和刪除 Volume 的流程,而 Attach 階段是指把儲存卷附著在某個節點或脫離某個節點的流程,另外只有塊儲存型別的 CSI 外掛才需要 Attach 功能。

service Controller {// 建立儲存卷,包括雲端儲存介質以及PV物件rpc CreateVolume (CreateVolumeRequest)returns (CreateVolumeResponse) {}// 刪除儲存卷rpc DeleteVolume (DeleteVolumeRequest)returns (DeleteVolumeResponse) {}// 掛載儲存卷,將儲存介質掛載到目標節點rpc ControllerPublishVolume (ControllerPublishVolumeRequest)returns (ControllerPublishVolumeResponse) {}// 解除安裝儲存卷rpc ControllerUnpublishVolume (ControllerUnpublishVolumeRequest)returns (ControllerUnpublishVolumeResponse) {}// 例如:是否可以同時用於多個節點的讀/寫rpc ValidateVolumeCapabilities (ValidateVolumeCapabilitiesRequest)returns (ValidateVolumeCapabilitiesResponse) {}// 返回所有可用的 volumesrpc ListVolumes (ListVolumesRequest)returns (ListVolumesResponse) {}// 可用儲存池的總容量rpc GetCapacity (GetCapacityRequest)returns (GetCapacityResponse) {}// 例如. 外掛可能未實現 GetCapacity、Snapshottingrpc ControllerGetCapabilities (ControllerGetCapabilitiesRequest)returns (ControllerGetCapabilitiesResponse) {}// 建立快照rpc CreateSnapshot (CreateSnapshotRequest)returns (CreateSnapshotResponse) {}// 刪除指定的快照rpc DeleteSnapshot (DeleteSnapshotRequest)returns (DeleteSnapshotResponse) {}// 獲取所有的快照rpc ListSnapshots (ListSnapshotsRequest)returns (ListSnapshotsResponse) {}}

CSI Node — 負責控制 Kubernetes 節點上的 Volume 操作。其中 Volume 的掛載被分成了 NodeStageVolume 和 NodePublishVolume 兩個階段。NodeStageVolume 介面主要是針對塊儲存型別的 CSI 外掛而提供的,塊裝置在 "Attach" 階段被附著在 Node 上後,需要掛載至 Pod 對應目錄上,但因為塊裝置在 linux 上只能 mount 一次,而在 kubernetes volume 的使用場景中,一個 volume 可能被掛載進同一個 Node 上的多個 Pod 例項中,所以這裡提供了 NodeStageVolume 這個介面,使用這個介面把塊裝置格式化後先掛載至 Node 上的一個臨時全域性目錄,然後再呼叫 NodePublishVolume 使用 linux 中的 bind mount 技術把這個全域性目錄掛載進 Pod 中對應的目錄上。

service Node {// 在節點上初始化儲存卷(格式化),並執行掛載到Global目錄rpc NodeStageVolume (NodeStageVolumeRequest)returns (NodeStageVolumeResponse) {}// umount 儲存卷在節點上的 Global 目錄rpc NodeUnstageVolume (NodeUnstageVolumeRequest)returns (NodeUnstageVolumeResponse) {}// 在節點上將儲存卷的 Global 目錄掛載到 Pod 的實際掛載目錄rpc NodePublishVolume (NodePublishVolumeRequest)returns (NodePublishVolumeResponse) {}// unmount 儲存卷在節點上的 Pod 掛載目錄rpc NodeUnpublishVolume (NodeUnpublishVolumeRequest)returns (NodeUnpublishVolumeResponse) {}// 獲取節點上Volume掛載檔案系統統計資訊(總空間、可用空間等)rpc NodeGetVolumeStats (NodeGetVolumeStatsRequest)returns (NodeGetVolumeStatsResponse) {}// 獲取節點的唯一 IDrpc NodeGetId (NodeGetIdRequest)returns (NodeGetIdResponse) {option deprecated = true;}// 返回節點外掛的能力rpc NodeGetCapabilities (NodeGetCapabilitiesRequest)returns (NodeGetCapabilitiesResponse) {}// 獲取節點的一些資訊rpc NodeGetInfo (NodeGetInfoRequest)returns (NodeGetInfoResponse) {}}

只需要實現上面的介面就可以實現一個 CSI 外掛了。雖然 Kubernetes 並未規定 CSI 外掛的打包安裝,但是提供了以下建議來簡化我們在 Kubernetes 上容器化 CSI Volume 驅動程式的部署方案,具體的方案介紹可以檢視 CSI 規範介紹文件 https://github.com/kubernetes/community

container storage interface deploy

按照上圖的推薦方案,CSI Controller 部分以 StatefulSet 或者 Deployment 方式部署,CSI Node 部分以 DaemonSet 方式部署。因為這兩部分實現在同一個 CSI 外掛程式中,因此只需要把這個 CSI 外掛與 External Components 以容器方式部署在同一個 Pod中,把這個 CSI 外掛與 Driver registrar 以容器方式部署在 DaemonSet 的 Pod 中,即可完成 CSI 的部署。

前面我們使用的 Rook 部署的 Ceph 叢集就是實現了 CSI 外掛的:

$ kubectl get pods -n rook-ceph |grep plugincsi-cephfsplugin-2s9d5                                 3/3     Running     0          21dcsi-cephfsplugin-fgp4v                                 3/3     Running     0          17dcsi-cephfsplugin-fv5nx                                 3/3     Running     0          21dcsi-cephfsplugin-mn8q4                                 3/3     Running     0          17dcsi-cephfsplugin-nf6h8                                 3/3     Running     0          21dcsi-cephfsplugin-provisioner-56c8b7ddf4-68h6d          4/4     Running     0          21dcsi-cephfsplugin-provisioner-56c8b7ddf4-rq4t6          4/4     Running     0          21dcsi-cephfsplugin-xwnl4                                 3/3     Running     0          21dcsi-rbdplugin-7r88w                                    3/3     Running     0          21dcsi-rbdplugin-95g5j                                    3/3     Running     0          21dcsi-rbdplugin-bnzpr                                    3/3     Running     0          21dcsi-rbdplugin-dvftb                                    3/3     Running     0          21dcsi-rbdplugin-jzmj2                                    3/3     Running     0          17dcsi-rbdplugin-provisioner-6ff4dd4b94-bvtss             5/5     Running     0          21dcsi-rbdplugin-provisioner-6ff4dd4b94-lfn68             5/5     Running     0          21dcsi-rbdplugin-trxb4                                    3/3     Running     0          17d

這裡其實是實現了 RBD 和 CephFS 兩種 CSI,用 DaemonSet 在每個節點上運行了一個包含 Driver registra 容器的 Pod,當然和節點相關的操作比如 Mount/Unmount 也是在這個 Pod 裡面執行的,其他的比如 Provision、Attach 都是在另外的 csi-rbdplugin-provisioner-xxx Pod 中執行的。

22
最新評論
  • BSA-TRITC(10mg/ml) TRITC-BSA 牛血清白蛋白改性標記羅丹明
  • 虛擬機器搭建 k8s 叢集實驗環境及 Harbor 私有倉庫