简介

外部存储接入 Kubernetes 的方式主要有两种:In-Tree 和 Out-of-Tree。其中 In-Tree 是指存储驱动的源码都在 Kubernetes 代码库中,与 Kubernetes 一起发布、迭代、管理,这种方式灵活性较差,且门槛较高。Out-of-Tree 是指存储插件由第三方编写、发布、管理,作为一种扩展与 Kubernetes 配合使用。Out-of-Tree 主要有 FlexVolume 和 CSI 两种实现方式,其中,FlexVolume 因为其命令式的特点,不易维护和管理,从 Kubernetes v1.23 版本开始已被弃用。因此 CSI 已经成为 Kubernetes 存储扩展( Out-of-Tree )的唯一方式。

在介绍CSI之前,先梳理一下kubernetes中使用存储有哪些步骤:
1、创建pv对象
2、存储服务器上创建一个volume
3、把创建的volume挂载到宿主机上
4、格式化volume
5、把格式化的volume mount到pod的volume目录
6、创建pvc对象并和pv对象绑定
7、pod使用pvc

代码工程:https://github.com/mospany/nfscsi.git

术语

缩写 英文全称 中文全称 描述
pvc PersistentVolumeClaim 持久卷声明 描述的是pod希望使用的持久化存储的属性
pv PersistentVolume 持久卷 描述的是具体的持久化存储数据卷信息
storageClass storageClass 存储类 创建pv的模板

CSI组成

img

通常情况下:CSI Driver = DaemonSet + Deployment(StatefuleSet) 。
其中:

  • 绿色部分:Identity、Node、Controller 是需要开发者自己实现的,被称为 Custom Components。
  • 粉色部分:node-driver-registrar、external-attacher、external-provisioner 组件是 Kubernetes 团队开发和维护的,被称为 External Components,它们都是以 sidecar 的形式与 Custom Components 配合使用的。

Custom Components

Custom Components 本质是 3 个 gRPC Services:

  • Identity Service 顾名思义,主要用于对外暴露这个插件本身的信息,比如驱动的名称、驱动的能力等:

    //github.com/container-storage-interface/spec/lib/go/csi/csi.pb.go
    type IdentityServer interface {
        GetPluginInfo(context.Context, *GetPluginInfoRequest) (*GetPluginInfoResponse, error)
        GetPluginCapabilities(context.Context, *GetPluginCapabilitiesRequest) (*GetPluginCapabilitiesResponse, error)
        Probe(context.Context, *ProbeRequest) (*ProbeResponse, error)
    }
    
  • Controller Service 主要定义一些 无需在宿主机上执行的操作,这也是与下文的 Node Service 最根本的区别。以 CreateVolume 为例,k8s 通过调用该方法创建底层存储。比如底层使用了某云供应商的云硬盘服务,开发者在 CreateVolume 方法实现中应该调用云硬盘服务的创建 / 订购云硬盘的 API,调用 API 这个操作是不需要在特定宿主机上执行的。

    // github.com/container-storage-interface/spec/lib/go/csi/csi.pb.go
    type ControllerServer interface {
        CreateVolume(context.Context, *CreateVolumeRequest) (*CreateVolumeResponse, error)
        DeleteVolume(context.Context, *DeleteVolumeRequest) (*DeleteVolumeResponse, error)
        ControllerPublishVolume(context.Context, *ControllerPublishVolumeRequest) (*ControllerPublishVolumeResponse, error)
        ControllerUnpublishVolume(context.Context, *ControllerUnpublishVolumeRequest) (*ControllerUnpublishVolumeResponse, error)
        ValidateVolumeCapabilities(context.Context, *ValidateVolumeCapabilitiesRequest) (*ValidateVolumeCapabilitiesResponse, error)
        ListVolumes(context.Context, *ListVolumesRequest) (*ListVolumesResponse, error)
        GetCapacity(context.Context, *GetCapacityRequest) (*GetCapacityResponse, error)
        ControllerGetCapabilities(context.Context, *ControllerGetCapabilitiesRequest) (*ControllerGetCapabilitiesResponse, error)
        CreateSnapshot(context.Context, *CreateSnapshotRequest) (*CreateSnapshotResponse, error)
        DeleteSnapshot(context.Context, *DeleteSnapshotRequest) (*DeleteSnapshotResponse, error)
        ListSnapshots(context.Context, *ListSnapshotsRequest) (*ListSnapshotsResponse, error)
        ControllerExpandVolume(context.Context, *ControllerExpandVolumeRequest) (*ControllerExpandVolumeResponse, error)
        ControllerGetVolume(context.Context, *ControllerGetVolumeRequest) (*ControllerGetVolumeResponse, error)
    }
    
  • Node Service 定义了 需要在宿主机上执行的操作,比如:mount、unmount。在前面的部署架构图中,Node Service 使用 Daemonset 的方式部署,也是为了确保 Node Service 会被运行在每个节点,以便执行诸如 mount 之类的指令。

    // github.com/container-storage-interface/spec/lib/go/csi/csi.pb.go
    type NodeServer interface {
        NodeStageVolume(context.Context, *NodeStageVolumeRequest) (*NodeStageVolumeResponse, error)
        NodeUnstageVolume(context.Context, *NodeUnstageVolumeRequest) (*NodeUnstageVolumeResponse, error)
        NodePublishVolume(context.Context, *NodePublishVolumeRequest) (*NodePublishVolumeResponse, error)
        NodeUnpublishVolume(context.Context, *NodeUnpublishVolumeRequest) (*NodeUnpublishVolumeResponse, error)
        NodeGetVolumeStats(context.Context, *NodeGetVolumeStatsRequest) (*NodeGetVolumeStatsResponse, error)
        NodeExpandVolume(context.Context, *NodeExpandVolumeRequest) (*NodeExpandVolumeResponse, error)
        NodeGetCapabilities(context.Context, *NodeGetCapabilitiesRequest) (*NodeGetCapabilitiesResponse, error)
        NodeGetInfo(context.Context, *NodeGetInfoRequest) (*NodeGetInfoResponse, error)
    }
    

原理

img
1、首先肯定是要在一个进程里编程实现前面提到的三个gRPC服务对应的方法,例如IdentityServer下的三个方法、ControllerServer下的CreateVolume/DeleteVolume方法以及NodeServer下的NodePublishVolume/NodeUnpublishVolume方法等,并以unix sock的方式提供gRPC服务(后文会用到),并打包成镜像;

2、把node-driver-registrar和CSI服务作为两个容器放到一个pod里,这样node-driver-registrar服务就可以用unix sock的方式访问CSI进程里的gRPC服务并且向kubelet注册;

3、node-driver-registrar完成注册后,后续的Mount/Unmount等操作kubelet会直接通过unix sock访问CSI。这里有两层含义:第一层含义是kubelet会直接通过unix sock访问CSI,因此CSI需要用hostPath的方式把自己unix sock文件暴露;第二层含义是kubelet直接调用CSI服务,这意味着node-driver-registrar和CSI的这个pod应该是daemonSet形式部署的;

4、把external-provisioner和CSI服务作为两个容器放到一个pod里,去实现Dynamic Provisioning功能。因为Dynamic Provisioning设计创建卷和删除卷,因此这个pod应该看做是有状态的,在部署上通常是带有选举的deployment部署或者副本数为1的statefulSet部署(如果需要Attach/Detach功能,也可以再加个容器把external-attacher放到这个pod中)。

CSI注册流程

img
相关的步骤释义如下:

1、kubelet启动后基于fsnotify监听/var/lib/kubelet/plugins_registry目录;
2、node-driver-registrar启动通过启动参数中配置的CSI进程的sock文件,调CSI进程的GetPluginInfo方法获取CSI插件名称;
3、node-driver-registrar启动后在/var/lib/kubelet/plugins_registry目录下创建自己的sock文件{csiName}-reg.sock;
4、kubelet的watcher监听到/var/lib/kubelet/plugins_registry目录下有sock文件创建,把该sock文件信息存入内存中的desiredStateOfWorld对象中;
5、kubelet中有个reconciler协程周期性的检查desiredStateOfWorld对象和actualStateOfWorld对象中的数据差异,发现有新的CSI插件需要执行注册过程;
6、reconciler通过/var/lib/kubelet/plugins_registry/{csiName}-reg.sock,调用node-driver-registrar下的GetInfo方法获取CSI插件的名称和CSI进程的sock文件路径等信息;
7、reconciler通过上一步拿到的CSI进程sock文件,调用CSI进程下NodeGetInfo方法获取一些数据用于后续的Node和CSINode对象;
8、组装数据调apiServer接口更新本节点对应的Node对象的annotation;
9、组装数据调apiServer接口创建/更新对应的CSINode对象;
10、reconciler通过/var/lib/kubelet/plugins_registry/{csiName}-reg.sock,调用node-driver-registrar的NotifyRegistrationStatus方法,告知其注册结果。

NFS搭建

由于实现CSI需要一个后端存储,Linux提供NFS功能可以免费搭建一个NSC存储功能用来验证。
搭建办法详见: CentOS7 下安装和配置 NFS

部署

部署node

进入代码工程中的deploy下运行命令`kubectl apply -f node.yaml`

[root@k8s-master nfscsi]# pwd
/root/nfscsi
[root@k8s-master nfscsi]# cd deploy/
[root@k8s-master deploy]# ls
node.yaml  provisioner.yaml
[root@k8s-master deploy]# ll
总用量 12
-rw-r--r-- 1 root root 3720 1月   8 22:18 node.yaml
-rw-r--r-- 1 root root 4413 1月   8 22:16 provisioner.yaml

[root@k8s-master deploy]# k apply -f node.yaml
serviceaccount/nfs-csi-node created
clusterrole.rbac.authorization.k8s.io/nfs-csi-node created
clusterrolebinding.rbac.authorization.k8s.io/nfs-csi-node created
daemonset.apps/nfs-csi-node created
[root@k8s-master deploy]#

查看POD的运行状态:

[root@k8s-master deploy]# k get pod -A -o wide | grep nfs
kube-system   nfs-csi-node-x28zj                         2/2     Running   0                10m   172.25.140.216   k8s-master   <none>           <none>
[root@k8s-master deploy]#
[root@k8s-master deploy]# k get ds -A
NAMESPACE     NAME           DESIRED   CURRENT   READY   UP-TO-DATE   AVAILABLE   NODE SELECTOR                                              AGE
kube-system   calico-node    3         3         3       3            3           kubernetes.io/os=linux                                     19d
kube-system   kube-proxy     3         3         3       3            3           kubernetes.io/os=linux                                     52d
kube-system   nfs-csi-node   1         1         1       1            1           kubernetes.io/hostname=k8s-master,kubernetes.io/os=linux   10m
[root@k8s-master deploy]#

pod正常启动后,先查看node-driver-registrar的日志,注册正常:

[root@k8s-master nfscsi]# kubectl logs -n kube-system nfs-csi-node-x28zj node-driver-registrar
I0115 07:45:31.451826       1 main.go:166] Version: v2.5.0
I0115 07:45:31.451864       1 main.go:167] Running node-driver-registrar in mode=registration
I0115 07:45:31.452324       1 main.go:191] Attempting to open a gRPC connection with: "/csi/csi.sock"
I0115 07:45:32.455714       1 main.go:198] Calling CSI driver to discover driver name
I0115 07:45:32.457464       1 main.go:208] CSI driver name: "nfscsi"
I0115 07:45:32.457493       1 node_register.go:53] Starting Registration Server at: /registration/nfscsi-reg.sock
I0115 07:45:32.457582       1 node_register.go:62] Registration Server started at: /registration/nfscsi-reg.sock
I0115 07:45:32.458992       1 node_register.go:92] Skipping HTTP server because endpoint is set to: ""
I0115 07:45:33.018081       1 main.go:102] Received GetInfo call: &InfoRequest{}
I0115 07:45:33.018292       1 main.go:109] "Kubelet registration probe created" path="/var/lib/kubelet/plugins/csi-nfsplugin/registration"
I0115 07:45:33.623838       1 main.go:120] Received NotifyRegistrationStatus call: &RegistrationStatus{PluginRegistered:true,Error:,}
[root@k8s-master nfscsi]# ll /var/lib/kubelet/plugins/csi-nfsplugin/csi.sock
srwxr-xr-x 1 root root 0 1月  15 15:45 /var/lib/kubelet/plugins/csi-nfsplugin/csi.sock
[root@k8s-master nfscsi]#

再看看自己编码nfs-csi容器日志,注册过程只调用了GetPluginInfo和NodeGetInfo
方法:

[root@k8s-master nfscsi]# kubectl logs -n kube-system nfs-csi-node-x28zj nfs-csi
2023/01/15 07:45:31 driverName: nfscsi, version: N/A, nodeID: k8s-master
2023/01/15 07:45:31 grpc server start
2023/01/15 07:45:32 GetPluginInfo request
2023/01/15 07:45:33 NodeGetInfo request
[root@k8s-master nfscsi]#

node-driver-registrar和CSI进程的sock文件:

[root@k8s-master nfscsi]# ll /var/lib/kubelet/plugins_registry/
总用量 0
srwx------ 1 root root 0 1月  15 15:45 nfscsi-reg.sock
[root@k8s-master nfscsi]#

node对象的annotation:

[root@k8s-master nfscsi]# kubectl get node k8s-master  -oyaml| grep annotations -A 9
annotations:
  csi.volume.kubernetes.io/nodeid: '{"nfscsi":"k8s-master"}'
  kubeadm.alpha.kubernetes.io/cri-socket: /var/run/dockershim.sock
  node.alpha.kubernetes.io/ttl: "0"
  projectcalico.org/IPv4Address: 172.25.140.216/20
  projectcalico.org/IPv4IPIPTunnelAddr: 10.244.235.192
  volumes.kubernetes.io/controller-managed-attach-detach: "true"
creationTimestamp: "2022-11-23T15:34:38Z"
labels:
  app: hdls-csi-controller
[root@k8s-master nfscsi]#

最后验证CSINode对象:

[root@k8s-master nfscsi]# k get csinode -o wide
NAME         DRIVERS   AGE
k8s-master   1         52d
k8s-work1    0         52d
k8s-work2    0         52d
[root@k8s-master nfscsi]# kubectl get csinode k8s-master -o yaml
apiVersion: storage.k8s.io/v1
kind: CSINode
metadata:
  annotations:
    storage.alpha.kubernetes.io/migrated-plugins: kubernetes.io/aws-ebs,kubernetes.io/azure-disk,kubernetes.io/cinder,kubernetes.io/gce-pd
  creationTimestamp: "2022-11-23T15:34:38Z"
  name: k8s-master
  ownerReferences:
  - apiVersion: v1
    kind: Node
    name: k8s-master
    uid: cb4c3e59-66e5-40a0-a385-aa2072719381
  resourceVersion: "278990"
  uid: cf0f93f0-495e-49f2-88b4-6f7126ca6176
spec:
  drivers:
  - name: nfscsi
    nodeID: k8s-master
    topologyKeys: null
[root@k8s-master nfscsi]#

到这里我们成功完成并验证了CSI的注册。

[root@k8s-master deploy]# k logs -n kube-system nfs-csi-node-vxkc8  nfs-csi
2023/01/15 08:33:22 driverName: nfscsi, version: N/A, nodeID: k8s-master
2023/01/15 08:33:22 grpc server start
2023/01/15 08:33:23 GetPluginInfo request
2023/01/15 08:33:23 NodeGetInfo request
2023/01/15 09:42:25 NodeGetCapabilities request
2023/01/15 09:42:25 NodeGetCapabilities request
2023/01/15 09:42:25 NodeGetCapabilities request
2023/01/15 09:42:25 NodeGetCapabilities request
2023/01/15 09:42:25 NodePublishVolume request
2023/01/15 09:42:25 source: 127.0.0.1:/data/pvc-910384b5-b5eb-4196-b6d3-876f9679ed05, targetPath: /var/lib/kubelet/pods/e86fa053-7d1b-4330-a6f2-555d350b45e9/volumes/kubernetes.io~csi/pvc-910384b5-b5eb-4196-b6d3-876f9679ed05/mount, options: []
2023/01/15 09:43:20 NodeGetCapabilities request

当POD执行CreateVolume是将调用nodeService里的NodePublishVolume方法进行mount操作,即把nfs里的pvc-子目录mount到宿主机pod目录卷下。

部署provisioner

Dynamic Provisioning原理:所谓的Dynamic Provisioning,其实就是创建pvc后会自动创建卷和pv,并把pv和pvc绑定

img

部署运行:

[root@k8s-master deploy]# kubectl apply -f provisioner.yaml
storageclass.storage.k8s.io/nfscsi created
csidriver.storage.k8s.io/nfscsi created
serviceaccount/nfs-csi-provisioner created
clusterrole.rbac.authorization.k8s.io/nfs-csi-provisioner created
clusterrolebinding.rbac.authorization.k8s.io/nfs-csi-provisioner created
deployment.apps/nfs-csi-provisioner created
[root@k8s-master deploy]#

观察对应的provisioner是否起来:

[root@k8s-master deploy]# k get pod -A -o wide | grep nfs
kube-system   nfs-csi-node-vxkc8                         2/2     Running   0               10m     172.25.140.216   k8s-master   <none>           <none>
kube-system   nfs-csi-provisioner-6f7db46646-77lqv       2/2     Running   0               2m20s   172.25.140.216   k8s-master   <none>           <none>
[root@k8s-master deploy]#

创建PVC

准备一个如下的pvc yaml,apply该yaml:

[root@k8s-master test]# pwd
/root/nfscsi/test
[root@k8s-master test]# cat pvc.yaml
apiVersion: v1
kind: PersistentVolumeClaim
metadata:
  name: test-pvc
spec:
  accessModes:
    - ReadWriteOnce
  storageClassName: nfscsi
  resources:
    requests:
      storage: 1Gi

查看是否会自动创建卷、pv,并和pvc绑定:

[root@k8s-master test]# kubectl apply -f pvc.yaml
persistentvolumeclaim/test-pvc created
[root@k8s-master test]# k get pvc
NAME       STATUS   VOLUME                                     CAPACITY   ACCESS MODES   STORAGECLASS   AGE
test-pvc   Bound    pvc-910384b5-b5eb-4196-b6d3-876f9679ed05   1Gi        RWO            nfscsi         20s
[root@k8s-master test]# k get pv
NAME                                       CAPACITY   ACCESS MODES   RECLAIM POLICY   STATUS     CLAIM              STORAGECLASS   REASON   AGE
pvc-910384b5-b5eb-4196-b6d3-876f9679ed05   1Gi        RWO            Delete           Bound      default/test-pvc   nfscsi                  35s
[root@k8s-master test]# ll /mnt/nfs-data/
总用量 16
drwxr-xr-x 2 root root 4096 1月  15 17:06 pvc-910384b5-b5eb-4196-b6d3-876f9679ed05
[root@k8s-master test]# ll /data/
总用量 16
drwxr-xr-x 2 root root 4096 1月   8 22:22 pvc-7115a52d-ba4f-4571-adbc-25e71941ca55
[root@k8s-master test]#

查看provisioner pod日志:

[root@k8s-master test]# k logs -n kube-system  nfs-csi-provisioner-6f7db46646-77lqv nfs-csi
2023/01/15 08:41:03 driverName: nfscsi, version: N/A, nodeID: k8s-master
2023/01/15 08:41:03 grpc server start
2023/01/15 08:41:04 Probe request
2023/01/15 08:41:04 GetPluginInfo request
2023/01/15 08:41:04 GetPluginCapabilities request
2023/01/15 08:41:04 ControllerGetCapabilities request
...
2023/01/15 09:06:26 CreateVolume request
2023/01/15 09:06:26 req name:  pvc-910384b5-b5eb-4196-b6d3-876f9679ed05

可以看出创建pvc时自动绑定pv成功。

删除PVC

同理。

测试

验证POD使用CSI

先准备POD的yaml

[root@k8s-master test]# cat pod.yaml
apiVersion: v1
kind: Pod
metadata:
  name: test-nginx-sci-pod
spec:
  nodeName: k8s-master  # 运行在安装了csi插件的node上
  containers:
  - name: nginx
    image: nginx:latest
    imagePullPolicy: IfNotPresent
    volumeMounts:
    - name: nfs-pvc
      mountPath: /var/log/nginx
  volumes:
  - name: nfs-pvc
    persistentVolumeClaim:
      claimName: test-pvc
   [root@k8s-master test]#  

运行

[root@k8s-master test]# kubectl apply -f pod.yaml
pod/test-nginx-sci-pod created
[root@k8s-master test]# kubectl get pod -A | grep test-nginx
default       test-nginx-sci-pod                         1/1     Running   0                18s
[root@k8s-master test]#

查看nginx日志

[root@k8s-master test]# ll /mnt/nfs-data/pvc-910384b5-b5eb-4196-b6d3-876f9679ed05/
总用量 4
-rw-r--r-- 1 root root    0 1月  15 17:48 access.log
-rw-r--r-- 1 root root 1641 1月  15 17:52 error.log
[root@k8s-master test]#
[root@k8s-master test]# tail /mnt/nfs-data/pvc-910384b5-b5eb-4196-b6d3-876f9679ed05/access.log
[root@k8s-master test]# tail /mnt/nfs-data/pvc-910384b5-b5eb-4196-b6d3-876f9679ed05/error.log
2023/01/15 09:49:20 [notice] 1#1: worker process 33 exited with code 0
2023/01/15 09:49:20 [notice] 1#1: exit
2023/01/15 09:51:39 [notice] 1#1: using the "epoll" event method
2023/01/15 09:51:39 [notice] 1#1: nginx/1.21.5
2023/01/15 09:51:39 [notice] 1#1: built by gcc 10.2.1 20210110 (Debian 10.2.1-6)
2023/01/15 09:51:39 [notice] 1#1: OS: Linux 3.10.0-957.21.3.el7.x86_64
2023/01/15 09:51:39 [notice] 1#1: getrlimit(RLIMIT_NOFILE): 65536:65536
2023/01/15 09:51:39 [notice] 1#1: start worker processes
2023/01/15 09:51:39 [notice] 1#1: start worker process 32
2023/01/15 09:51:39 [notice] 1#1: start worker process 33
[root@k8s-master test]#
[root@k8s-master test]# ll /data/pvc-910384b5-b5eb-4196-b6d3-876f9679ed05/
总用量 4
-rw-r--r-- 1 root root    0 1月  15 17:48 access.log
-rw-r--r-- 1 root root 1641 1月  15 17:52 error.log
[root@k8s-master test]# 

可以看出宿主机和nfs服务器上该pvc下已经有nginx日志生成达到了目的。

csc功能测试命令

  • 安装

    go install -v  github.com/rexray/gocsi/csc@latest
    

    使用go env查看GOPATH, go install 的程序一般就放在第一个路径下的bin

    把它拷贝到目标机上并加可执行权限。

  • 命令

    [root@k8s-master csi-hdls]# ./csc
    NAME
        csc -- a command line container storage interface (CSI) client
    
    SYNOPSIS
        csc [flags] CMD
    
    AVAILABLE COMMANDS
        controller
        identity
        node
    
    Use "csc -h,--help" for more information
    [root@k8s-master csi-hdls]# ./csc controller help
    NAME
        controller -- the csi controller service rpcs
    
    SYNOPSIS
        csc controller [flags] CMD
    
    AVAILABLE COMMANDS
        create-snapshot
        create-volume
        delete-snapshot
        delete-volume
        expand-volume
        get-capabilities
        get-capacity
        list-snapshots
        list-volumes
        publish
        unpublish
        validate-volume-capabilities
    
    Use "csc controller -h,--help" for more information
    [root@k8s-master csi-hdls]#
    

    emptyDir的位置应该位于运行pod的给定节点上的/var/lib/kubelet/pods/{podid}/volumes/kubernetes.io~empty-dir/中

参考

【01】如何实现一个 Kubernetes CSI Driver
【02】Kubernetes CSI 驱动开发指南
【03】gRPC详细入门介绍
【04】如何编写一个 CSI 插件
【05】Kubernetes K8S之固定节点nodeName和nodeSelector调度详解
【06】kubernetes CSI(下)
【07】阿里云服务器 CentOS7 下安装和配置 NFS

微信扫一扫

作者:mospan
微信关注:墨斯潘園
本文出处:http://mospany.github.io/2022/12/22/k8s-sci-driver/
文章版权归本人所有,欢迎转载,但未经作者同意必须保留此段声明,且在文章页面明显位置给出原文连接,否则保留追究法律责任的权利。