1、CSI 概述
CSI 全称 Container Storage Interface,是容器编排系统(CO)如k8s等扩展容器存储的一种实现方式,基于gRPC实现,是当前主流的存储扩展方式
2、插件组成
CSI 插件通过gRPC的方式调用,其内部组成可以分成Node服务和Controller服务两部分,如下文图所示:
- Node服务:负责k8s负载节点上的卷配置,每个节点都有一个Node提供服务
- Controller服务:负责将卷与具体节点进行配置,每个集群中只需要有一个Controller提供服务
此外,还有Identity gRPC服务,开发 CSI 插件时,需要为每个单独的服务实现Identity服务。CSI 插件的实现可以有几种形式:
- Node 和 Controller 服务集成到一个二进制文件中,并为这两者添加一个共同的Identity服务
- Node 和 Controller 服务分别实现,打包在两个不同的二进制文件中,此时需要为二者分别实现Identity服务
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: 此方法应返回运行此插件的节点的唯一IDNodeGetCapabilities: 与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、部署方案
- 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服务一起部署