kubernetes CSI(上)

随着应用容器化的趋势,越来越多的应用部署到了kubernetes平台,同时日益复杂的业务场景,也使得kubernetes需要支持越来越多类别的存储。kubernete对存储的支持,大致可以分为三个历程:

  • in-tree
  • flexVolume
  • CSI

in-tree

最开始kubernetes支持的存储逻辑代码都在kubernetes项目中的,跟着kubernetes组件一起编译和发版,这种模式叫作in-tree。in-tree模式有很多局限性,例如:

  1. kubernetes代码和存储代码放在一起,都是由kubernetes社区来维护,这样会使得kubernetes维护人员既要关注kubernetes本身的发展,又要关注种类繁多和日益增加的存储需求;
  2. 存储的发版跟着kubernetes版本发布,如果某个存储需要更新升级,必须等到下一个kubernetes版本才能发布,这极大的限制了存储更新迭代的灵活性。

以in-tree模式下支持的nfs存储为例,我们来看看pod如何使用:

apiVersion: v1
kind: Pod
metadata:
  name: nginx
spec:
  containers:
  - name: nginx
    image: nginx
    volumeMounts:
    - name: nfs
      mountPath: /data
  volumes:
  - name: nfs
    nfs:
      server: "192.168.0.1"
      path: "/data/nfs"

flexVolume

考虑到in-tree模式的局限性,kubernetes决定“授人以渔”:支持用户自定义插件来实现存储对接。所谓的插件其实就是在需要处理存储逻辑时,kubelet会通过cmd的方式调用对应的可执行文件,再由这个可执行文件去对接具体的存储,这种模式叫作flexVolume。flexVolume插件需要注意两点:

  • 插件的位置

flexVolume插件的调用方是kubelet,而kubelet是分布在各个节点上的,因此flexVolume插件也需要放置到各个节点上,具体目录为:/usr/libexec/kubernetes/kubelet-plugins/volume/exec/{driver}/{fsType}

我们用一个示例来详细说明:

apiVersion: v1
kind: Pod
metadata:
  name: nginx
spec:
  containers:
  - name: nginx
    image: nginx
    volumeMounts:
    - name: nfs
      mountPath: /data
  volumes:
  - name: nfs
    flexVolume:
      driver: "k8s/nfs"
      fsType: "nfs"
      options:
        server: "192.168.0.1"
        share: "share"

如上述yaml,pod使用了一个flexVolume。我们先关注flexVolume下的driverfsType两个字段,通过前面的说明,这两个字段确定了插件可执行文件的路径,因此这个flexVolume的可执行文件就应该是节点上的/usr/libexec/kubernetes/kubelet-plugins/volume/exec/k8s~nfs/nfs(注意driver字段的“/”被替换成了“~”)。

再看看options字段,它对应的是go语言中的map[string]string,作用是作为kubelet以cmd形式调用插件的时候携带的相关参数。例如这里的server指nfs服务器的IP,share指nfs服务器的共享目录。很显然,options中的数据应该是插件逻辑中支持的才有意义。

  • 插件要实现的方法

flexVolume插件需要实现下面部分方法(方法名由kubelet通过cmd ags传入):

  • init
  • attach
  • detach
  • waitforattach
  • isattached
  • mountdevice
  • unmountdevice
  • mount
  • unmount

不同插件可以选择性地实现对应方法,但是均要实现init方法,因为init方法用于告知kubelet本插件支持哪些功能。以官方提供的nfs flexVolume示例(https://github.com/kubernetes/examples/blob/master/staging/volumes/flexvolume/nfs)来说,它只实现了init、mount、umount方法。

flexVolume虽然支持了用户自定义存储插件,但是仍存在一定的局限性。比如:

  • flexVolume不支持Dynamic Provisioning,也就是创建PVC后不能自动创建PV和volume,除非再单独实现一个provisioner。

  • kubelet每一次对flexVolume插件的调用都是完全独立的操作,某一个动作(例如mount)生成的一些数据无法被下一个动作(例如umount)使用。

CSI

考虑到flexVolume的局限性,kubernetes推出了更加灵活的CSI(Container Storage Interface)模式。

在介绍CSI之前,先梳理一下kubernetes中使用存储有哪些步骤:

  1. 创建pv对象
  2. 存储服务器上创建一个volume
  3. 把创建的volume挂载到宿主机上
  4. 格式化volume
  5. 把格式化的volume mount到pod的volume目录
  6. 创建pvc对象并和pv对象绑定
  7. pod使用pvc

其中1、2、6步是前文《Dynamic Provisioning原理分析》中说到的Dynamic Provisioning过程(创建pvc后自动创建pv和volume并绑定),第3步把存储volume挂载到宿主机的过程叫作Attach(逆过程叫Detach),第4、5两步把volume格式化并mount到pod目录的过程叫作Mount(逆过程叫Unmount)。

CSI可以理解为kubernetes为了支持Dynamic ProvisioningAttach/DetachMount/Unmount等功能的抽象,实现CSI一般需要在一个进程里实现三个gRPC service:

  • IdentityServer
  • ControllerServer
  • NodeServer
IdentityServer

IdentityServer定义如下:

// 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)
}

IdentityServer服务负责对外暴露插件本身信息,包括插件名称以及插件所能提供的能力(capabilities)等。

ControllerServer

ControllerServer定义如下:

// 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)
}

ControllerServer名称中的controller可以理解为kubernetes的controller,即通过list&watch某些资源的事件做对应的处理,其实也就是对存储的volume做管理,包括如下等内容:

  • CreateVolume/DeleteVolume:创建和删除volume,对应的是Dynamic Provisioning过程
  • ControllerPublishVolume/ControllerUnpublishVolume:对应的是前文提到的Attach/Detach过程
  • CreateSnapshot/DeleteSnapshot:对应的快照功能
  • ControllerExpandVolume:对应扩容功能

这里的controller服务是有状态且部署上与节点无关的,因此最终的服务部署类型不会是daemonSet,而是带选举的deployment或者副本数为1的statefulSet。

NodeServer

NodeServer定义如下:

// 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)
}

NodeServer显然就是一些与节点强相关的工作,例如前文提到的Mount,对应的是NodeStageVolumeNodePublishVolume两个方法,在部署上通常是daemonSet。

CSI的部署

到这里很多读者可能会很迷惑,因为前文提到CSI需要在一个进程里实现三个gRPC服务,但是同一进程里的三个gRPC服务有的需要deployment/statefulSet部署,有的需要daemonSet部署,这不是一个矛盾话题吗?

在解释这个问题之前我们先了解几个kubernetes社区(https://github.com/kubernetes-csi)维护的项目:

  • node-driver-registrar:项目地址:https://github.com/kubernetes-csi/node-driver-registrar,主要负责把CSI注册到kubernetes
  • external-provisioner:项目地址:https://github.com/kubernetes-csi/external-provisioner,主要负责调CSI中ControllerServer下的CreateVolumeDeleteVolume方法实现Dynamic Provisioning功能。
  • external-attacher:项目地址:https://github.com/kubernetes-csi/external-attacher,主要负责调CSI中ControllerServer下的ControllerPublishVolumeControllerUnpublishVolume方法实现前文提到的Attach/Detach操作。
  • external-snapshotter:项目地址:https://github.com/kubernetes-csi/external-snapshotter,主要负责调CSI中ControllerServer下的CreateSnapshotDeleteSnapshot实现快照功能。
  • external-resizer:项目地址:https://github.com/kubernetes-csi/external-resizer,主要负责调CSI中ControllerServer下的ControllerExpandVolume方法实现扩容功能。
  • external-health-monitor:项目地址:https://github.com/kubernetes-csi/external-health-monitor,主要负责对volume的健康检查

这些项目都有官方提供的镜像,是不是都需要和自己写的CSI进程一起部署起来呢?答案是除了node-driver-registrar必须选择外,其它的项目都是根据存储特性和业务需求自由选择的。例如nfs没有Attach/Detach过程,我就不需要部署external-attacher服务;再比如我不需要Dynamic Provisioning功能,我就不需要部署external-provisioner服务。

我们再从服务的搭配和部署角度来看看具体应该怎么操作。假设我们现在要部署一个自己开发的nfs CSI,在这个CSI里我需要Dynamic Provisioning功能和处理Mount/Unmount流程,那么:

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

我们再用个图来总结下整个nfs CSI的逻辑:

在这里插入图片描述

总结

本文简单讲述了kubernetes存储的发展历程:in-tree -> flexVolume -> CSI。实现一个CSI需要在一个进程中实现IdentityServerControllerServerNodeServer三个gRPC服务,在部署上通常会有两个负载:一个daemonSet,包含node-driver-registrar和CSI两个服务;一个deployment/statefulSet,包含external-xxx服务和CSI服务(注意这两种负载中都有CSI进程,external-xxx服务和CSI部署在同一个pod里,通常也被叫做sidecar)。

微信公众号卡巴斯同步发布,欢迎大家关注。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值