K8S儲存外掛-FlexVolume && CSI

鍾大發發表於2020-12-22

K8S的自定義儲存外掛

和K8S的網路不太一樣,K8S的網路只有CNI一種介面暴露方式,所有的網路實現基於第三方進行開發實現,但儲存的內建實現就多達20多種,#K8S目前支援的外掛型別#。但內建的往往不滿足定製化的需求,所以和CNI 一樣,K8S 也暴露了對外的儲存介面,和CNI 一樣,通過實現對應的介面方法,即可建立屬於自己的儲存外掛,但和CNI 有點區別的是,K8S的儲存外掛的自定義實現方式,有FlexVolume 和 CSI 兩種,兩者的差別可以看做是新老功能的差異,但目前為止,FlexVolume 同樣也有用武之地。

FlexVolume

熟悉CNI的編寫方法的,對FlexVolume的編寫一定不陌生,CNI編寫完成後,是會拆分為2個二進位制檔案(CNI,IPAM)和一個配置檔案,放在每個Node節點上,kubelet在建立Pod 時候,會呼叫對應的二進位制檔案進行網路的建立,同樣也可以使用daemonset的方式容器化部署,FlexVolume 也一樣,編寫完成後一樣以二進位制的方式進行部署,和CNI 一樣,FlexVolume需要實現類似CNI的cmdadd,cmddel的方法,具體需要實現以下幾個方法:

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

基本返回格式:

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

那麼kublet和他的呼叫關係是啥?看一下kubelet呼叫的FlexVolume的一段程式碼(pod mount dir):

// SetUpAt creates new directory.
func (f *flexVolumeMounter) SetUpAt(dir string, fsGroup *int64) error {
  ...
  call := f.plugin.NewDriverCall(mountCmd)
  
  // Interface parameters
  call.Append(dir)
  
  extraOptions := make(map[string]string)
  
  // pod metadata
  extraOptions[optionKeyPodName] = f.podName
  extraOptions[optionKeyPodNamespace] = f.podNamespace
  
  ...
  
  call.AppendSpec(f.spec, f.plugin.host, extraOptions)
  
  _, err = call.Run()
  
  ...
  
  return nil
}

再看下一個PV的yml的栗子對比一下:

apiVersion: v1
kind: PersistentVolume
metadata:
  name: pv-flex-nfs
spec:
  capacity:
    storage: 1Gi
  accessModes:
    - ReadWriteMany
  flexVolume:
    driver: "k8s/nfs"
    fsType: "nfs"
    options:
      server: "1.1.1.1" 
      share: "export"

先只看FlexVolume, dirver這裡的k8s/nfs 就是FlexVolume的具體位置,注意k8s~nfs解析出來的就是k8s/nfs, FlexVolume的預設位置在/usr/libexec/kubernetes/kubelet-plugins/volume/exec/,所以上述yml用的FlexVolume外掛具體位置在/usr/libexec/kubernetes/kubelet-plugins/volume/exec/k8s~nfs/nfs

pv的yml 裡面有對應的options, 就是kubelet程式碼裡的extraOptions := make(map[string]string),是一個map型別,kubelet解析yml裡面的option引數,傳入該map變數,然後執行FlexVolume的具體方法,比如栗子裡的mount,將options的引數傳入,實現瞭如下的效果:

/usr/libexec/kubernetes/kubelet-plugins/volume/exec/k8s~nfs/nfs mount <mount dir> <json param>

git 上fork的幾個demo,方便查詢。

可以看到FlexVolume實現邏輯非常簡單,但解決的問題也非常有限,使用者還是需要手動建立PV,且每次呼叫外掛執行的都是偏原子操作的,即單次只執行attach,mount,umount等動作,所以,其作用,只是一個針對不同儲存自定義建立PV的外掛,假設我需要動態生成PV 並動態繫結PVC,FlexVolume就不具備可操作性,所以CSI來了。

CSI

先說下CSI和FlexVolume的基本差異,FlexVolume實現了Attach(掛載儲存到Node)和Mount(Node的目錄掛載到Pod), 缺少了PV的動態生成(需要運維手動在儲存上配置然後再建立手動建立PV),而CSI就是在FlexVolume的基礎上,實現了PV的動態生成。

CSI的呼叫和FlexVolume不一樣,在呼叫的時候,需要有一個註冊的過程,找個CSI的程式碼看下:

簡單先描述一下呼叫順序:

  1. driver.go, 主要是Run()方法,啟動RPC服務,註冊到CSI.
  2. identity.go, 主要是GetPluginInfo()方法,獲取外掛的各類資訊,如外掛名字等,GetPluginCapabilities,獲取CSI外掛的各項功能,比如是否支援Attach之類的,還有個Probe()方法,提供給K8s的探針,用於健康檢查CSI的狀態。
  3. controller.go, CSI實現的具體方法,比如操作儲存(CreateVolume 和 DeleteVolume),Attach儲存(ControllerPublishVolume 和 ControllerUnpublishVolume方法)
  4. node.go, 這一步主要實現的是mount的操作,即將目錄掛載到Pod裡,但是和簡單的mount方式不一樣,這裡分為了MountDevice(預處理,掛載到一個臨時目錄進行格式化) 和 SetUp(最終繫結,將臨時目錄繫結到實際的目錄), 分別對應了NodeStageVolume/NodeUnstageVolume 和 NodePublishVolume/NodeUnpublishVolume 方法。

上面只是描述了CSI外掛的呼叫順序,那麼問題來了,每一步,分別是誰去呼叫的呢?
回過頭先看下CSI的架構圖:
在這裡插入圖片描述


發現CSI的架構實際分了3塊,第一塊K8S-Core,即K8S的核心元件,第二塊Kubernetes External Component, 這是Kubernetes支援CSI的擴充套件元件,第三塊External Component:傳統意義上的CSI,即上面的那個demo程式碼。從官網架構圖的箭頭可以看到整體的呼叫關係:

  1. daemonset在每臺主機上執行了driver-registrar的container,和kubelet一一對應。
  2. kubelet呼叫driver-registrar,driver-registrar向CSI indentity(即程式碼裡的driver.go和identity.go)進行了註冊,並獲取了CSI 外掛的基本功能以及資訊。
  3. External provisioner watch 了master的apiserver,監聽pvc物件,一旦發現有建立, 則External provisioner會呼叫CSI Controller(呼叫了controller.go 裡的CreateVolume/DeleteVolume方法)進行了儲存端的建立,即自動生成了PV。
  4. External attacher,監聽了VolumeAttachment物件,一個發現有掛載,則呼叫CSI Controller 和 CSI Node進行卷的Attach和mount操作(分別呼叫了controller.go和node.go)。

第二塊External Component的下載地址

從上面的呼叫邏輯可以看出,出了CSI本體(這裡暫稱為CSI Driver),還需要部署External Component裡的三個container,且這3個container裡只有driver-registrar需要和kubelet呼叫,所以在實際部署中,需要以daemonset的方式,將driver-registrar和CSI Driver作為side-car模式的部署方式進行部署,其他2個External provisioner和 External attacher以statefulset的方式,和CSI Driver一起作為side-car模式的部署方式進行部署。
簡單畫個部署圖:
在這裡插入圖片描述

個人公眾號, 分享一些日常開發,運維工作中的日常以及一些學習感悟,歡迎大家互相學習,交流

在這裡插入圖片描述

相關文章