kubernetes CSI 插件機制學習筆記


前言

最近在極客時間訂閱了kubernetes的專欄,這篇文章是想記錄一下自己學習 CSI 插件機制 (container-storage-interface) 的過程,加深一下記憶。

准備工作

老師用的是 csi-digitalocean,還會用到 CSI 的 proto 文件,這是 git 地址container-storage-interface/spec, 在開始本文之前可以先把這兩個 repo 下載下來。
csi-digitalocean,這個是主要分析的代碼,需要放到 GOPATH 下,就能用 goland 來分析了。

mkdir $GOPATH/src/github.com/digitalocean
cd $GOPATH/src/github.com/digitalocean
git clone https://github.com/digitalocean/csi-digitalocean.git

然后用 goland 打開這個 csi-digitalocean 目錄就行了。在用 sublime 或者其他的工具打開 container-storage-interface/spec 就行了,這個git地址我們主要是用根目錄下的 csi.proto 和 spec.md 。

背景知識

CSI 插件機制主要是三部分

  • kube-apiserver,kubelet
  • External Components
  • Custom Component

external components 的三部分

  • Driver Registrar
  • External Provisioner
  • External Attacher

custom components 的三部分

  • Identity Service
  • Controller Service
  • Node Service

上面的三部分是需要 CSI plugin 必須要提供的是三個 GRPC 的 endpoint 。詳情可見。 下面是后面用到的這個文章里面的文字

A CO interacts with an Plugin through RPCs.
Each SP MUST provide:

  • Node Plugin: A gRPC endpoint serving CSI RPCs that MUST be run on the Node whereupon an SP-provisioned volume will be published.
  • Controller Plugin: A gRPC endpoint serving CSI RPCs that MAY be run anywhere.
  • In some circumstances a single gRPC endpoint MAY serve all CSI RPCs (see Figure 3 in Architecture).

There are three sets of RPCs:

  • Identity Service: Both the Node Plugin and the Controller Plugin MUST implement this sets of RPCs.
  • Controller Service: The Controller Plugin MUST implement this sets of RPCs.
  • Node Service: The Node Plugin MUST implement this sets of RPCs.

其中 Driver Registrar 負責請求 Identity Service 來獲取插件信息並且注冊到 kubelet 。可以看下 $GOPATH/src/github.com/digitalocean/csi-digitalocean/driver 的內容:

tree $GOPATH/src/github.com/digitalocean/csi-digitalocean/driver
$GOPATH/GoglandProjects/src/github.com/digitalocean/csi-digitalocean/driver
├── controller.go
├── driver.go
├── driver_test.go
├── identity.go
├── mounter.go
└── node.go

具體場景分析

具體的場景 container-storage-interface/spec 這個下面的 spec.md 里面都有,大家可以詳細看看。

這里說一下 csi-digitalocean 這個下面 README.md 里面的例子。本文忽略了一些地方只做筆記回顧知識用,具體可以參考源站。

部署CSI插件

執行下面的命令

kubectl apply -f https://raw.githubusercontent.com/digitalocean/csi-digitalocean/master/deploy/kubernetes/releases/csi-digitalocean-v0.3.1.yaml 

大家可以打開這個文件看下,主要是 Node plugin 和 Controller plugin 。

$ egrep 'StatefulSet|DaemonSet|image:' deploy/kubernetes/releases/csi-digitalocean-v0.3.1.yaml 
kind: StatefulSet
          image: quay.io/k8scsi/csi-provisioner:v0.4.1
          image: quay.io/k8scsi/csi-attacher:v0.4.1
          image: digitalocean/do-csi-plugin:v0.3.1
kind: DaemonSet
          image: quay.io/k8scsi/driver-registrar:v0.4.1
          image: digitalocean/do-csi-plugin:v0.3.1

其中 Controller plugin 是以 StatefulSet 部署的。digitalocean/do-csi-plugin:v0.3.1 這個 image 主要是 $GOPATH/src/github.com/digitalocean/csi-digitalocean/driver 這里面代碼實現 custom components 需要提供的服務。 Node plugin 是 DaemonSet 主要和 kubelet 交互。

創建 pvc 和創建使用該 pvc 的 pod。

主要是下面這個流程,分為三階段就是 Provision Attach Mount。

   CreateVolume +------------+ DeleteVolume
 +------------->|  CREATED   +--------------+
 |              +---+----+---+              |
 |       Controller |    | Controller       v
+++         Publish |    | Unpublish       +++
|X|          Volume |    | Volume          | |
+-+             +---v----+---+             +-+
                | NODE_READY |
                +---+----^---+
               Node |    | Node
              Stage |    | Unstage
             Volume |    | Volume
                +---v----+---+
                |  VOL_READY |
                +------------+
               Node |    | Node
            Publish |    | Unpublish
             Volume |    | Volume
                +---v----+---+
                | PUBLISHED  |
                +------------+

The lifecycle of a dynamically provisioned volume, from
creation to destruction, when the Node Plugin advertises the
STAGE_UNSTAGE_VOLUME capability.

使用 kubectl apply 下面的 yaml .

apiVersion: v1
kind: PersistentVolumeClaim
metadata:
  name: csi-pvc
spec:
  accessModes:
  - ReadWriteOnce
  resources:
    requests:
      storage: 5Gi
  storageClassName: do-block-storage

這個時候 External Provisioner 監聽到了 PVC 對象的創建,然后 External Provisioner 就會調用 CSI Controller 的 CreateVolume 方法創建 PV , 創建完 PV 之后 kube-apiserver 中的 VolumeController 的 PersistentVolumeController reconcile loop 就會 watch 到這對 PV 和 PVC 的大小和 storageclass 是一樣的。然后就把 PV 和 PVC 進行綁定然后就是上圖中的 CREATED 。 這個階段就是 Provision。

$ kubectl get pv
NAME                                       CAPACITY   ACCESS MODES   RECLAIM POLICY   STATUS    CLAIM             STORAGECLASS       REASON    AGE
pvc-0879b207-9558-11e8-b6b4-5218f75c62b9   5Gi        RWO            Delete           Bound     default/csi-pvc   do-block-storage             3m

之后創建 Pod

kind: Pod
apiVersion: v1
metadata:
  name: my-csi-app
spec:
  containers:
    - name: my-frontend
      image: busybox
      volumeMounts:
      - mountPath: "/data"
        name: my-do-volume
      command: [ "sleep", "1000000" ]
  volumes:
    - name: my-do-volume
      persistentVolumeClaim:
        claimName: csi-pvc 

這時這個 Pod 會被調度到一個機器 A 上,然后 VolumeController 的 AttachDetachController reconcile loop 就會 watch 到這個 PVC 需要 Attach 到 A 上,之后這個 AttachDetachController 就會創建一個 VolumeAttach 對象。 這時 quay.io/k8scsi/csi-attacher 這個容易就會 watch 到這個變化,通過 GRPC 調用 CSI 里面的 Controller Service 的 ControllerPublishVolume 方法把 PV 調度到這個機器上 進入流程圖中的 NODE_READY 。到現在為止是 Attach 階段。

之后就是 Mount 階段了。 Mount 階段如上述流程圖是分為 NodeStageVolume 和 NodePublishVolume 兩個階段,這兩個階段都在 driver/node.go 里面。 NodeStageVolume :

func (d *Driver) NodeStageVolume(ctx context.Context, req *csi.NodeStageVolumeRequest) (*csi.NodeStageVolumeResponse, error) 

	_, ok := req.VolumeAttributes[annNoFormatVolume]
	if !ok {
		formatted, err := d.mounter.IsFormatted(source)
		if err != nil {
			return nil, err
		}

		if !formatted {
			ll.Info("formatting the volume for staging")
			if err := d.mounter.Format(source, fsType); err != nil {
				return nil, status.Error(codes.Internal, err.Error())
			}
		} else {
			ll.Info("source device is already formatted")
		}

	} else {
		ll.Info("skipping formatting the source device")
	}
	if !mounted {
		if err := d.mounter.Mount(source, target, fsType, options...); err != nil {
			return nil, status.Error(codes.Internal, err.Error())
		}
	} else {
		ll.Info("source device is already mounted to the target path")
	}

主要是就是 Format 和 Mount ,其他的代碼我都省略了,詳情可以看源碼。這里面的掛載是一個臨時的掛載點 target := req.StagingTargetPath ,然后 NodePublishVolume

func (d *Driver) NodePublishVolume(ctx context.Context, req *csi.NodePublishVolumeRequest) 

	// Perform a bind mount to the full path to allow duplicate mounts of the same PD.
	options = append(options, "bind")

	if !mounted {
		ll.Info("mounting the volume")
		if err := d.mounter.Mount(source, target, fsType, options...); err != nil {
			return nil, status.Error(codes.Internal, err.Error())
		}
	} else {
		ll.Info("volume is already mounted")
	}

主要就是通過 bind 再把這個 StagingTargetPath mount 到 TargetPath。到此一個 dynamically provisioned volume 的流程就結束了。

總結

本文的主要目的就是把各種資料和連接放到這里,然后把整個流程大致梳理一遍。雖然抄了很多東西,但是自己還是把代碼都下載了,仔細梳理了一遍,加上一些自己的東西,感覺還是很有意義的。


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM