k8s存储插件之CSI

1,907 阅读6分钟

image.png

1、CSI 概述

CSI 全称 Container Storage Interface,是容器编排系统(CO)如k8s等扩展容器存储的一种实现方式,基于gRPC实现,是当前主流的存储扩展方式

2、插件组成

CSI 插件通过gRPC的方式调用,其内部组成可以分成Node服务和Controller服务两部分,如下文图所示:

  • Node服务:负责k8s负载节点上的卷配置,每个节点都有一个Node提供服务
  • Controller服务:负责将卷与具体节点进行配置,每个集群中只需要有一个Controller提供服务

image.png

image.png

此外,还有Identity gRPC服务,开发 CSI 插件时,需要为每个单独的服务实现Identity服务。CSI 插件的实现可以有几种形式:

  • Node 和 Controller 服务集成到一个二进制文件中,并为这两者添加一个共同的Identity服务
  • Node 和 Controller 服务分别实现,打包在两个不同的二进制文件中,此时需要为二者分别实现Identity服务

image.png

image.png

3、服务接口

Identity

  • GetPluginInfo:此方法需要返回插件的版本和名称。
  • GetPluginCapabilities:此方法返回插件的功能。当前,它报告插件是否具有提供Controller接口的功能。 CO根据此方法是否返回功能来调用Controller接口方法。
  • Probe:CO调用此命令只是为了检查插件是否正在运行。此方法不需要返回任何内容。目前,规范并没有规定您应该返回什么。因此,返回一个空响应。
service Identity {
  rpc GetPluginInfo(GetPluginInfoRequest)
	returns (GetPluginInfoResponse) {}

  rpc GetPluginCapabilities(GetPluginCapabilitiesRequest)
	returns (GetPluginCapabilitiesResponse) {}

  rpc Probe (ProbeRequest)
	returns (ProbeResponse) {}
}

Golang实现例子:

type IdentityServer interface {
   GetPluginInfo(context.Context, *GetPluginInfoRequest) (*GetPluginInfoResponse, error)
   GetPluginCapabilities(context.Context, *GetPluginCapabilitiesRequest) (*GetPluginCapabilitiesResponse, error)
   Probe(context.Context, *ProbeRequest) (*ProbeResponse, error)
}

type DefaultIdentityServer struct {
   Driver *CSIDriver
}

func (ids *DefaultIdentityServer) GetPluginInfo(ctx context.Context, req *csi.GetPluginInfoRequest) (*csi.GetPluginInfoResponse, error) {
   glog.V(5).Infof("Using default GetPluginInfo")

   if ids.Driver.name == "" {
      return nil, status.Error(codes.Unavailable, "Driver name not configured")
   }

   if ids.Driver.version == "" {
      return nil, status.Error(codes.Unavailable, "Driver is missing version")
   }

   return &csi.GetPluginInfoResponse{
      Name:          ids.Driver.name,
      VendorVersion: ids.Driver.version,
   }, nil
}

func (ids *DefaultIdentityServer) Probe(ctx context.Context, req *csi.ProbeRequest) (*csi.ProbeResponse, error) {
   return &csi.ProbeResponse{}, nil
}

func (ids *DefaultIdentityServer) GetPluginCapabilities(ctx context.Context, req *csi.GetPluginCapabilitiesRequest) (*csi.GetPluginCapabilitiesResponse, error) {
   glog.V(5).Infof("Using default capabilities")
   return &csi.GetPluginCapabilitiesResponse{
      Capabilities: []*csi.PluginCapability{
         {
            Type: &csi.PluginCapability_Service_{
               Service: &csi.PluginCapability_Service{
                  Type: csi.PluginCapability_Service_CONTROLLER_SERVICE,
               },
            },
         },
      },
   }, nil
}

Node

  • NodeStageVolume: 将卷挂载到宿主机上的全局目录(如果没格式化,需要进行格式化操作)
  • NodeUnstageVolume: 将卷从全局目录卸载,与NodeStageVolume相反
  • NodePublishVolume: 调用此方法可将卷从全局目录挂载到目标路径。通常执行的是bind mount操作。bind mount允许您将路径安装到其他路径(而不是将设备安装到路径)
  • NodeUnpublishVolume: 这与NodePublishVolume相反。它从目标路径卸载该卷。
  • NodeGetId: 此方法应返回运行此插件的节点的唯一ID
  • NodeGetCapabilities: 与ControllerGetCapabilities一样,它返回Node插件的功能
service Node {
  rpc NodeStageVolume (NodeStageVolumeRequest)
	returns (NodeStageVolumeResponse) {}

  rpc NodeUnstageVolume (NodeUnstageVolumeRequest)
	returns (NodeUnstageVolumeResponse) {}

  rpc NodePublishVolume (NodePublishVolumeRequest)
	returns (NodePublishVolumeResponse) {}

  rpc NodeUnpublishVolume (NodeUnpublishVolumeRequest)
	returns (NodeUnpublishVolumeResponse) {}

  rpc NodeGetId (NodeGetIdRequest)
	returns (NodeGetIdResponse) {}

  rpc NodeGetCapabilities (NodeGetCapabilitiesRequest)
	returns (NodeGetCapabilitiesResponse) {}
}

Controller

Controller服务接口:该接口负责控制和管理卷,例如:创建,删除,附加/分离,快照等

  • CreateVolume: 创建卷,一般该接口对云存储服务进行API调用并创建一个卷
  • DeleteVolume: 删除由CreateVolume创建的卷
  • ControllerPublishVolume: 此方法用于创建的卷附加到指定的节点
  • ControllerUnpublishVolume: 此方法卷与特定节点分离
  • ValidateVolumeCapabilities: 此方法用于返回卷的功能,例如是否可以同时用于多个节点的读取/写入,或者仅用于单个节点的读取/写入?例如,块存储一般只能以读/写模式连接到单个节点。
  • ListVolumes: 此方法应返回所有可用的卷
  • GetCapacity: 这将返回总可用存储池的容量。如果存储容量有限,则需要这样做。假设您知道只能提供1TB的存储空间。在配置和创建新卷时,应反映它并返回剩余的可用存储。
  • ControllerGetCapabilities: 返回Controller插件的功能。某些控制器插件可能未实现GetCapacity(例如云提供商,因为它对用户隐藏了),而某些插件可能未提供Snapshotting。该方法需要返回其支持的功能列表。
service Controller {
  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) {}

  rpc ListVolumes (ListVolumesRequest)
	returns (ListVolumesResponse) {}

  rpc GetCapacity (GetCapacityRequest)
	returns (GetCapacityResponse) {}

  rpc ControllerGetCapabilities (ControllerGetCapabilitiesRequest)
	returns (ControllerGetCapabilitiesResponse) {}  
}

4、实现细节

  • 请求的幂等性:所有的接口实现都需要注意幂等性,例如创建卷时,需要注意是否有同名的卷;挂载卷时,需要注意是否已经是挂载点等
  • 请求的异步性:部分请求是异步的,创建或附加卷不是即时操作,需要等待处理异步返回结果
  • 日志记录:重要操作需要有适当的日志记录,方便调试和观察驱动程序的工作,可以将日志落盘或者打印到标准输出中
  • 添加测试框架:csi-test,此软件包可直接针对CSI API运行单元测试

Node服务实现案例:node.go

Controller服务实现案例:controller.go

5、部署方案

image.png

  • Node 服务是 gRPC 服务,需要在将卷配置到的Node上运行,它必须在所有节点上的原因是,它需要能够格式化和挂载已经附加到节点的卷,可以将Node插件部署为Daemonset,这样可以确保Node服务在所有节点上运行
  • Controller 服务是 gRPC 服务,可以在任何地方运行,但需要作为单个副本运行(请求的幂等性),因为Controller负责执行创建/删除和附加/分离卷。如果运行多个副本或在其前面放置一个负载均衡器,则可能是两个控制器服务尝试创建相同的卷,或者它们可能都试图同时附加该卷。对于Controller服务,我们可以将其部署为StatefulSet。 StatefulSet还带有缩放保证,可以将Controller插件用作StatefulSet,并将副本字段设置为1。

上文提到,csi组件有多种实现形式:

  • 创建一个统一的二进制文件。单个二进制文件,包含 Node 和 Controller 服务的所有方法。
  • 创建两个二进制文件。一个提供 Node 服务的二进制文件,另一个提供 Controller 服务驱动程序的二进制文件。
  • 创建一个仅提供Node服务的二进制文件。仅Node服务提供rRPC服务,其GetPluginCapabilities RPC不报告CONTROLLER_SERVICE功能。

对于需要提供Controller服务的情况,推荐使用方法1,对于不需要提供controller服务的情况,推荐使用方法3。方法1、3都只需要维护一个二进制文件,镜像只需要打一个,方便后期的维护和部署

那么,如何区分是Node服务还是Controller服务呢?

为了解决这个问题,Kubernetes具有以下为我们完成此任务的Sidercar容器:

  • driver-registrar:Sidecar容器,其 1)向kubelet注册CSI驱动程序,以及 2)将驱动程序自定义NodeId添加到Kubernetes节点API对象的标签上,与Node服务一起部署
  • external-provisioner:Sidecar容器,用于监视Kubernetes PersistentVolumeClaim对象并触发CreateVolume/DeleteVolume,与Controller服务一起部署
  • external-attacher:Sidecar容器,用于监视Kubernetes VolumeAttachment对象并触发ControllerPublish/Unpublish,与Controller服务一起部署

image.png