1.K8s 的持久化存储支持
在支持持久化存储方面,K8s 提供了内嵌原生 Driver 的方式连接外部的常见存储系统例如 NFS、iSCSI、CephFS、RBD 等来满足不同业务的需求。但由于存储生态本身也在不断演进,使用 K8s 内嵌的方式来支持不断变化的存储系统在成本和时效上都会对 K8s 项目自身带来巨大的挑战。
所以和其他服务管理系统一样,K8s 逐渐的将存储系统的具体实现从主项目中分离出来,通过定义接口的方式允许第三方厂商自行接入存储服务。在这个道路上也经历了两个阶段:
- Flex Volume, 自 1.2 版本引入。
第三方存储服务提供商直接在 K8s Server 上部署符合 Flex Volume 规范的 Driver,利用 K8s 暴露出的 mount/unmount/attach/detach 等关键 API 实现存储系统的接入。这个模式主要的问题是,在这个形态下第三方厂商的 Driver 有权限接入物理节点的根文件系统,这会带来安全上的隐患。 - Container Storage Interface (CSI), 自 1.9 版本引入,目前已经进入 GA 阶段(1.13)。
CSI 定义了容器场景所需要的存储控制原语和完整的控制流程,并且在 K8s 的 CSI 实现中,所有的第三方 Driver 和 K8s 的其他服务扩展一样,以服务容器的形态的运行,不会直接影响到 K8s 的核心系统稳定性。
2.存储对象
CSI 定义的存储对象为持久化卷,称之为 Volume。包括两种类型:
- Mounted File Volume,Node 将会把 Volume 以指定的文件格式 Mount 到 Container 上,从 Container 的角度看到的是一个目录;
- Raw Block Volume, 直接将 Volume 以 Block Device(磁盘)的形态暴露给 Container,对于某些可以直接操作磁盘的服务,这个形态可以省去文件系统的开销获得更好的性能。
Raw Block Volume 目前还处于 Beta 阶段,所以下文的过程描述和 SMTX 的 CSI Driver 目前的实现方式均针对 Mounted File Volume。
3.基本术语
4.Plugin
RPC接口: CO通过RPC与插件交互, 每个SP必须提供两类插件:
- Controller Plugin,负责存储对象(Volume)的生命周期管理,在集群中仅需要有一个即可;
- Node Plugin,在必要时与使用 Volume 的容器所在的节点交互,提供诸如节点上的 Volume 挂载/卸载等动作支持,如有需要则在每个服务节点上均部署。
存储服务商可以根据自身需求实现不同的的 Plugin 组合。例如对于以 NFS 形式提供的存储服务,可以仅实现 Controller Plugin 实现资源的创建和访问权限控制,每个节点均可以通过标准的 NFS 方式获得服务,无需通过 Node Plugin 来实现挂载/卸载等操作。而以 iSCSI 形式提供的存储服务,就需要 Node Plugin 在指定节点上,通过挂载 LUN,格式化,挂载文件系统等一系列动作完成 iSCSI LUN 至容器可见的目录形式的转化。
5.Volume 生命周期
一个典型的 CSI Volume 生命周期如下图(来自 CSI SPEC)所示:
上面这个图是一个较为复杂的卷供应生命周期图,从这个图我们可以看出一个存储卷的供应分别调用了Controller Plugin
的CreateVolume
、ControllerPublishVolume
及Node Plugin
的NodeStageVolume
、NodePublishVolume
这4个gRPC接口,存储卷的销毁分别调用了Node Plugin
的NodeUnpublishVolume
、NodeUnstageVolume
及Controller
的ControllerUnpublishVolume
、DeleteVolume
这4个gRPC接口。
- Volume 被创建后进入 CREATED 状态,此时 Volume 仅在存储系统中存在,对于所有的 Node 或者 Container 都是不可感知的;
- 对 CREATED 状态的 Volume 进行 Controlller Publish 动作后在指定的 Node 上进入 NODE_READY 的状态,此时 Node 可以感知到 Volume,但是 Container 依然不可见;
- 在 Node 对 Volume 进行 Stage 操作,进入 VOL_READY 的状态,此时 Node 已经与 Volume 建立连接。Volume 在 Node 上以某种形式存在;
- 在 Node 上对 Volume 进行 Publish 操作,此时 Volume 将转化为 Node 上 Container 可见的形态被 Container 利用,进入正式服务的状态;
- 当 Container 的生命周期结束或其他不再需要 Volume 情况发生时,Node 执行 Unpublish Volume 动作,将 Volume 与 Container 之间的连接关系解除,回退至 VOL_READY 状态;
- Node Unstage 操作将会把 Volume 与 Node 的连接断开,回退至 NODE_READY 状态;
- Controller Unpublish 操作则会取消 Node 对 Volume 的访问权限;
- Delete 则从存储系统中销毁 Volume。
CSI 要求状态转化操作是幂等的,并在原则上保证 Volume 的状态转化是有序进行的。
根据存储使用方式和内部实现的不同,状态机可以略有区别,但对应操作必须是成对出现的。例如在不需要额外建立 Node 与 Volume 之间连接的 Stage/Unstage 阶段时,状态机就可以直接通过 Controller Publish/Unpublish 在 NODE_READY 与 PUBISHED 之间转化,而无需经过 VOL_READY 阶段。Plugin 向 CSI 注册时必须声明自身支持哪些语义。
6.RPC
CSI 要求 Plugin 支持的 RPC 包括:
· 身份服务:Node Plugin和Controller Plugin都必须实现这些RPC集。
· 控制器服务:Controller Plugin必须实现这些RPC集。
· 节点服务:Node Plugin必须实现这些RPC集。
(1)Identity Service:认证服务,Controller 和 Node Plugin 均需要支持
- GetPluginInfo, 获取 Plugin 基本信息
- GetPluginCapabilities,获取 Plugin 支持的能力
- Probe,探测 Plugin 的健康状态
(2)Controller Service:控制服务
- Volume CRUD,包括了扩容和容量探测等 Volume 状态检查与操作接口
- Controller Publish/Unpublish Volume ,Node 对 Volume 的访问权限管理
- Snapshot CRD,快照的创建和删除操作,目前 CSI 定义的 Snapshot 仅用于创建 Volume,未提供回滚的语义
(3)Node Service:节点服务
- Node Stage/Unstage/Publish/Unpublish/GetStats Volume,节点上 Volume 的连接状态管理
- Node Expand Volume, 节点上的 Volume 扩容操作,在 volume 逻辑大小扩容之后,可能还需要同步的扩容 Volume 之上的文件系统并让使用 Volume 的 Container 感知到,所以在 Node Plugin 上需要有对应的接口
- Node Get Capabilities/Info, Plugin 的基础属性与 Node 的属性查询
service Identity {
rpc GetPluginInfo(GetPluginInfoRequest)
returns (GetPluginInfoResponse) {}
rpc GetPluginCapabilities(GetPluginCapabilitiesRequest)
returns (GetPluginCapabilitiesResponse) {}
rpc Probe (ProbeRequest)
returns (ProbeResponse) {}
}
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) {}
...
}
service Node {
...
rpc NodePublishVolume (NodePublishVolumeRequest)
returns (NodePublishVolumeResponse) {}
rpc NodeUnpublishVolume (NodeUnpublishVolumeRequest)
returns (NodeUnpublishVolumeResponse) {}
rpc NodeExpandVolume(NodeExpandVolumeRequest)
returns (NodeExpandVolumeResponse) {}
rpc NodeGetCapabilities (NodeGetCapabilitiesRequest)
returns (NodeGetCapabilitiesResponse) {}
rpc NodeGetInfo (NodeGetInfoRequest)
returns (NodeGetInfoResponse) {}
...
}
7.部署形态
CSI 使用 Sidecar 的方式实现 CSI Plugin 与 K8s 核心逻辑的解耦。Sidecar 代表监听了 CSI 指定 API 的标准容器,它与 CSI Plugin 共同组成一个 Pod 对外提供服务,它们之间通过 Socket 连接。在这个模式下,Sidecar 成为 CSI Plugin 与 K8s 之间连接的中介和隔离带。理想状态下二者可以在不直接交互和影响的情况下共同工作,解决了安全问题。
CSI 定义了如下几种 Sidecar:
- external-provisioner:监听 Volume CRUD API,完成 Volume 的生命周期管理
- external-attacher:监听 Controller[Publish|Unpublish]Volume API,实现 Node 和 Volume 的可见性控制
- external-snapshotter:监听 Snapshot CRD API,完成 Snapshot 的生命周期管理
- node-driver-register:监听 Node 基本信息查询 API,注册 Node Plugin,每个节点 Node Plugin 均需要通过 driver-register 注册自身才可以与 K8s 之间建立连接获取 Node Volume 相关请求
- cluster-driver-register:用于向 K8s 注册 Plugin 整体支持的模式,包括是否跳过 Attach 阶段/是否在 Node Publish Volume 阶段时需要 K8s 提供 Pod 信息
- livenessprobe:心跳检测,用于探测 Plugin 的存活状态
8.适用场景
在容器化发展的早期阶段,Container 多用于承担轻量型的无状态服务,对数据存储的需求大多通过本地的临时共享文件,或者用网络访问的方式将数据置于远端的日志收集或者 DB 等外部存储上。这种模式业务和数据之间从程序管理的角度看是松耦合的,互相独立,没有严格的依赖。
但是另一方面,这个模式下数据本身无法成为服务的一部分,并不能通过 K8s 统一管理。并且需要为每个应用打开通往远端存储服务的网络通道,这在安全性上有时并不是一个好的选择。
而基于持久化卷,将数据服务提供方也放入 K8s Pod 中(例如挂载持久化卷作为磁盘,上面部署容器运行 DB)作为完整应用的一部分,数据即可以做到与应用无缝的统一管理,所有应用内部 Pod 间的业务数据请求均可以在 K8s 提供的虚拟网络中进行。而基于 K8s 本身的高可用特性和 CSI Driver 的灵活配置能力也可以获得不逊色于外部存储的可靠性与性能。
9.Kubernetes csi driver开发示例
type driver struct {
csiDriver *csicommon.CSIDriver
endpoint string
ids *csicommon.DefaultIdentityServer
cs *controllerServer
ns *nodeServer
}
首先我们需要定义一个driver结构体,基本包含了plugin启动的所需信息(除了以上信息还可以添加其他参数):
(1)csicommon.CSIDriver
k8s自定义代表插件的结构体, 初始化的时候需要指定插件的RPC功能和支持的读写模式.
func NewCSIDriver(nodeID string) *csicommon.CSIDriver {
csiDriver := csicommon.NewCSIDriver(driverName, version, nodeID)
csiDriver.AddControllerServiceCapabilities(
[]csi.ControllerServiceCapability_RPC_Type{
csi.ControllerServiceCapability_RPC_LIST_VOLUMES,
csi.ControllerServiceCapability_RPC_CREATE_DELETE_VOLUME,
csi.ControllerServiceCapability_RPC_PUBLISH_UNPUBLISH_VOLUME,
})
csiDriver.AddVolumeCapabilityAccessModes([]csi.VolumeCapability_AccessMode_Mode{csi.VolumeCapability_AccessMode_SINGLE_NODE_WRITER})
return csiDriver
}
(2)endpoint:
插件的监听地址,一般的,我们测试的时候可以用tcp方式进行,比如tcp://127.0.0.1:10000,最后在k8s中部署的时候一般使用unix方式:/csi/csi.sock
(3)csicommon.DefaultIdentityServer :
认证服务一般不需要特别实现,使用k8s公共部分的即可.
(4)controllerServer:
实现CSI中的controller服务的RPC功能,继承后可以选择性覆盖部分方法.
type controllerServer struct {
*csicommon.DefaultControllerServer
}
(5)nodeServer:
实现CSI中的node服务的RPC功能,继承后可以选择性覆盖部分方法.
type nodeServer struct {
*csicommon.DefaultNodeServer
}
(6)driver的Run方法:
该方法中调用csicommon的公共方法启动socket监听,RunControllerandNodePublishServer方法会同时启动controller和node.还可以单独启动controller和node,需要写两个入口main函数.
func (d *driver) Run(nodeID, endpoint string) {
d.endpoint = endpoint
d.cloudconfig = cloudConfig
csiDriver := NewCSIDriver(nodeID)
d.csiDriver = csiDriver
// Create GRPC servers
ns, err := NewNodeServer(d, nodeID, containerized)
if err != nil {
glog.Fatalln("failed to create node server, err:", err.Error())
}
glog.V(3).Infof("Running endpoint [%s]", d.endpoint)
csicommon.RunControllerandNodePublishServer(d.endpoint, d.csiDriver, NewControllerServer(d), ns)
}
其他主要函数功能实现如下(以下以我们iscsi产品实现描述):
1.Create Volume:Controller Plugin 收到创建请求之后会创建一个 iSCSI LUN ,如有必要则会自动再创建需要的 Target, iSCSI LUN 以及所属的 Target 均为逻辑对象,不与物理磁盘绑定。
2.Controller Publish Volume:目前 Kubernetes 使用 Open iSCSI 作为节点上的数据接入服务,Open iSCSI 在挂载 Target 时,会将所有 Target 内的 LUN 均挂载到主机上作为一个 Block Device(例如 /dev/sdx 这样的磁盘) 。
3.Node Stage Volume: Node Plugin 会将 LUN 通过 Open iSCSI 命令挂载至主机,呈现为一个磁盘;
4.Node Publish Volume: Node Plugin 对磁盘进行格式化(如果磁盘之前尚未被格式化,如已经格式化则为跳过对应步骤),将磁盘 Mount 到主机上提供给 Container 使用;
5.Node Unpublish Volume: Node Plugin 将磁盘上的文件系统 Unmount;
6.Node Stage Volume: Node Plugin 在主机上将断开磁盘的 iSCSI 链接;
7.Controller Unpublish Volume: Controller Plugin 向后端注销指定 Node 在 LUN 上的访问权限;
8.Delete Volume: Controller Plugin 请求删除对应的 LUN ,LUN 所占用的数据空间将会在存储系统中被回收。