这是Kubernetes存储系列的第二篇文章。上一篇花了一些篇幅解释了Kubernetes中的一些存储概念,最终带出了关键的一点-----动态供应。在编排系统中,存储资源也应该像Pod一样,可以动态规划的,这种方式又称之为动态供应(dynamic provision)。为了实现这个目标,k8s社区提出了好几种方案,今天在此进行探索一番。

动态供应的不同时期不同方案

有以下方法实现卷插件:

In-tree Volume Plugin Out-of-tree Provisioner Out-of-tree CSI Driver 最初地,Kubernetes内部代码中实现了一些存储插件,用于支持一些主流网络存储,叫作In-tree Volume Plugin。

第二种 Out-of-tree Provisioner:如果官方的插件不能满足要求,存储供应商可以根据需要去定制或者优化存储插件并集成到Kubernetes系统。

第三种是容器存储接口CSI (Container Storage Interface),是Kubernetes对外开放的存储接口,实现这个接口即可集成到Kubernetes系统中。CSI特性在刚过去的12月正式GA,同时社区也宣布未来将不再对In tree/Out of tree继续开发,并将已有功能全部迁移到CSI上,所以对于存储供应商和使用者来说,第三种CSI是更推荐的解决方案。

In-tree Volume Plugin

这是最初的一种方案,Kubernetes最初处理存储的方式比较粗暴,就是需要什么存储需求就直接添加一个卷插件,比如说最开始的,只是提供简单的hostpath存储卷(宿主机上的某个共享目录),那就添加一个hostpath volume plugin并添加相应的实现代码。然后开始用户有其他需求,比如这次就想要使用ceph的块接口,需要支持rbd接口的动态插件,就又开发了rbd plugin。这个口子一开,肯定还有更多的存储插件加到kubernetes 源码中,到后面就包括Google自家的gcePersistentDisk,AWS的云存储,以及其他很多厂商的提供存储服务的代码开始逐步怼到kubernetes源码中。确实就是这么粗暴的,截止如今,还是有很多这种代码嵌入到Kubernetes的源码中,我认为这是非常可怕的事情😨。 这种把存储服务集成到kubernetes系统的中插件又称为In-tree Volume Plugin,他们是k8s系统的一部分,会随着kuberntes版本发布而更新,维护等都和Kubernetes紧密联系

比如下面例子: volume_plugins 可以看到这里有一大堆不同类型的volume plugin,需要新增加一个存储供应商就往这里怼点代码,简直就是大力出奇迹。 以ceph和cinder为例,他们会根据自己的存储特性,后端结果等有着不同的实现。但是并没有统一的接口。此时可以说是面向对象开发而不是面向接口。 ceph_fs vs cinder

这种实现虽然不够优雅,但是至少某种程度上解决了Kubernetes编排系统中持久化存储的难点。 很多的存储从而变得可以动态供应。比如说我只需要创建一个rbd的storagecalss以及一个pvc,此时借助这些内置的存储插件,就可以让系统自动创建想要的rbd类型的PV,并且和这个PVC进行绑定。再也不用人工创建以及回收等繁复的操作了。(但是并不所有的volume plugin都实现的动态供应比如原生的nfs就不能动态创建)。

Out-of-tree Provisioner

显然上面这种方式有很多的值得吐槽的地方,比如是和Kubernetes核心代码紧密的联系在一起,要是存储商的后端接口有变动或者其他原因需要更新volume plugin的时候还得等到kubernetes下一次版本发布。这些intree volume plugin甚至有可能会影响到Kubernetes本身,这是个问题💩。 这个时候就逐步衍生了另外一种方案,如果官方的插件不能满足要求,存储供应商可以根据需要去定制或者优化存储插件并集成到Kubernetes系统,这种外置的插件并不需要和以前一样成为内嵌在Kubernetes的核心代码中,这些插件称之为external provisioner >What is an 'external provisioner'? An external provisioner is a dynamic PV provisioner whose code lives out-of-tree/external to Kubernetes. Unlike in-tree dynamic provisioners that run as part of the Kubernetes controller manager, external ones can be deployed & updated independently.

它们可以独立部署(比如以deployment的形式运作在集群中),这是因为kubernetes作出了适配,可在需要使用到它们功能的时候ubernetes可以根据名字找到这些外置的插件,并要求这些external provisioner来实现供应PV的操作。

举个例子: 前面我提到过,kubernetes原生不知道nfs动态供应功能,但是现在我们可以创建一个专用的external provisoner来实现nfs 的动态供应。 先创建一个PVC,并在PVC中指定使用storageclassA来动态创建PV,

kind: PersistentVolumeClaim
apiVersion: v1
metadata:
  name: test-claim
  annotations:
    volume.beta.kubernetes.io/storage-class: "nfssc"
spec:
  accessModes:
    - ReadWriteMany
  resources:
    requests:
      storage: 1Mi

如果使用external provisioner此时可以看到这个storageclassA的定义是这样的:

apiVersion: storage.k8s.io/v1
kind: StorageClass
metadata:
  name: nfssc
provisioner: xsky.nfs/v1 # or choose another name, must match deployment's env PROVISIONER_NAME'
parameters:
  archiveOnDelete: "false"

⚠️注意这里:我使用了一个自定义的provisoner叫xsky.nfs/v1,这当然不是Kuberntes自带的插件。这是我们额外部署的external provisions。

这个external provisoner是这样定义的(其实就是一个特殊的deployment)源码请看后面说明:

apiVersion: v1
kind: ServiceAccount
metadata:
  name: nfs-client-provisioner
---
kind: Deployment
apiVersion: extensions/v1beta1
metadata:
  name: nfs-client-provisioner
spec:
  replicas: 1
  strategy:
    type: Recreate
  template:
    metadata:
      labels:
        app: nfs-client-provisioner
    spec:
      serviceAccountName: nfs-client-provisioner
      containers:
        - name: nfs-client-provisioner
          image: quay.io/external_storage/nfs-client-provisioner:latest
          volumeMounts:
            - name: nfs-client-root
              mountPath: /persistentvolumes
          env:
            - name: PROVISIONER_NAME
              value: xsky.nfs/v1
            - name: NFS_SERVER
              value: 10.252.90.104
            - name: NFS_PATH
              value: /sdsfs/filesys/

具体的使用external provisioner来实现nfs动态供应的详细例子可以戳这里

尽管这样还是有个问题。毕竟这只是个provisioner,顾名思义,它是负责供应PV的(创建和删除PV)。面对日益复杂的Kubernetes上的持久化存储需求,这就有点抓襟见肘,比如快照,动态扩容等高级功能就等还是没办法解决🤷‍♂️

CSI容器存储接口

Kubernetes和CNCF决定把容器存储进行抽象,通过标准接口的形式把存储部分移到容器编排系统外部去。CSI的设计目的是定义一个行业标准,该标准将使存储供应商能够自己实现,维护和部署他们的存储插件。这些存储插件会以Sidecar Container形式运行在Kubernetes上并为容器平台提供稳定的存储服务。

CSI: 是容器存储接口,是针对存储抽象出的一类接口,由存储厂商实现具体的接口操作。https://github.com/container-storage-interface/spec/blob/master/csi.proto

可以看到这里的protobuf的定义,其实就是一类RPC服务以及基本的类型,简单来说就是通过重新定义和统一了存储接口,任意的存储厂商只要遵循这套接口,实现想要的操作并能对Kubernetes系统提供gRPC调用,那就可以集成到Kubernetes系统中。具体接口怎么实现那就是存储厂商的事情了,反正他们已经给定了一个protobuf的定义了。

既然是csi是容器存储接口,能否用其他语言来实现?当然是可以,csi是一套抽象接口,使用protobuf协议定义,protobuf是类型json或者xml一样的序列化数据结果,由于它可跨平台,因此可以生成不同语言的库,比如Java,Go,C++,C#,Python等等,只要针对其中定义的接口实现,最后在Kubernetes编排系统里部署起来作为gRPC的服务端即可。

最后一种容器存储接口CSI (Container Storage Interface),是Kubernetes对外开放的存储接口,实现这个接口即可集成到Kubernetes系统中。CSI特性在18年的12月正式GA,同时社区也宣布未来将不再对In tree/Out of tree继续开发,并将已有功能全部迁移到CSI上,所以对于存储供应商和使用者来说,第三种CSI是更推荐的解决方案。

细节解析

尽管说到这个份上了,实际可能还是不是很清晰🤯🤯🤯,理论归理论,文字总是枯燥的

还是用图片来解释把:

In tree volume plugin 解析图:

in_tree

⚠️注意: 尽管这个示意图把中间的volume plugin抽离出来,只是为了阐述他们的作为连接k8s编排(包括master和kubelt直接)和后端存储的一个桥梁身份。实际上volume plugin是属于controller manager组件下的。

如图所示,In-tree Volume Plugin是Kubernetes自带的,属于Kubernetes的一部分,由Kubernetes一起发布和维护,所有存储插件代码都集成在Kubernetes中。第三方存储供应商难以集成。

  1. 首先在Kubernetes启动的时候,master侧会有三个核心组件,kubectl-sheduler,etcd和controller-masnter

scheduler负责资源对象的调度,比如把一个pod调度到某个节点nodeA

etcd会负责把集群内的资源对象持久化,比如把pod,pvc,node,service等存到etcd

controller-manager包含非常多模块,比如pod manager,volume manager等等,是集群的大脑🧠。他们负责监控集群内的组件,并判断是否需要操作。

比如创建一个PVC资源后,pv controller就会检测到,并在适合的适合调用某个volume plugin来创建PV。

in tree volume plugin属于volume manager下的,因此他们会被加载进来,然后在需要的时候会被调用。

2)k8s集群内的负载节点Node会在启动的适合启动关键进程kubelet。

kubelet会在node商执行一些初始化以及收拾烂摊子的工作,比如某个pod被调度进来它就要开始检查Pod依赖的卷是否准备好,如果没有就要负责挂载操作

然后才能吧pod容器启动。

这里先不细说每个组件的源码,后面会再分析。只需要知道每个组件负责什么,整个数据流搞清楚就够了。

Out-of-tree Provisioner 解析图:

前面有讲到,其实Out-of-tree Provisioner 是基于In tree Volume plugin来实现的。

只是让Kubernetes作适配,兼容外置的插件,有很多组件的使用和控制都还是共用的。

详细数据流如下:

external_provisioner

⚠️:

a. 上图volume plugin组件被分离处理,实际上代码还是在controller manager里面,只是因为它处理的PV既可以在master也可以在kubelet侧,因此单独拿出来

b. 尽管使用了external provisioner,实际volume plugin也还参与工作,比如把创建卷请求路由到指定的external provisioner上

分析:

  1. 当这里用 kubectl create xxx 的时候会在集群创建一些资源(pv,pvc,pod...)。

  2. k8s各方组件会对资源进行监控和调度,比如scheduler检测到Pod资源创建了,就开始把Pod分配到某个Node上

  3. Pod被分配到Node上还不能马上启动,因为它所依赖的PV卷还没准备好,此时还要等待Kubelet准备好环境

kubelet上的volume manager需要等待PV的创建,直到PV可用了,它才可以把PV存储挂载到宿主机上,再从宿主机映射到容器内

另一条路上(controller manager),准确点说应该是PV controller,它也在监听集群中刚创建的PVC,并发现这个新的PVC还没绑定,

于是就决定先创建一个PV。

  1. Controller会查找pvc.yam里指定的StoageClass对象(如果没有指定的话则使用默认的SC),此时确定了需要使用的Provisioner。

一个外置的external-provisoner就会被调用,负责创建PV,这个external-provisoner会和相应的后端存储(比如nfs server端)通过某种方式通信(比如RPC或者RESTful接口)

并在后端创建了一种存储资源,成功后就给k8s这边吐出一个相应的PV。此时完成provision动作。

  1. Kubelet检测到这个PV已经准备完毕处于可用状态则开始把PV挂载在宿主机NodeA上, 即attach。

  2. 当远端存储已经在宿主机上完成挂载和初始化(可能会有格式化), Kubelet 会将宿主机上的mountpoint 映射到容器内指定位置,比如使用docker的-v参数

  3. Kubelet 启动Pod,并开始使用这个PV

CSI(Container storage interface)

直接上解析图:

csi

⚠️注意:

上图中实际上绿色和橙色两部分都会在Kubernetes中运行,最终形态都是Pod。

采用容器存储接口方案,总览图是这样的。

大体上分为4部分:

  1. master侧,这里其实还是和之前一样,有controller manager在检测组件并路由请求,如果是CSI类型的PV,它就不用找in tree volume plugin,或者external provision了

但是它还是会进行状态判断,已经更新状态等操作

2)此时还需要额外创建3个辅助容器(gRPC客户端):

他们其实也是从以前的kubernetes代码里抽离处理的,只是这里是专门地用来处理CSI 卷的处理,分别是

Driver-registrar: 使用 Kubelet注册 CSI 驱动程序的 sidecar 容器,并将 NodeId (通过 GetNodeID 调用检索到 CSI endpoint)添加到 Kubernetes Node API 对象的 annotation 里面。

External-provisioner: 监听 Kubernetes PersistentVolumeClaim 对象的 sidecar 容器,并触发对 CSI 端点的 CreateVolume 和DeleteVolume 操作;

External-attacher: 可监听 Kubernetes VolumeAttachment 对象并触发 ControllerPublish 和 ControllerUnPublish 操作的 sidecar 容器,负责attache/detache卷到node节点上;

3) 右侧橘黄色表示存储厂商实现的存储插件驱动,分别有三个服务(gRPC服务端):

他们是实现了csi标志(proto文件里定义的接口)的gRPC服务端

CSI identify: 标志插件服务,并维持插件健康状态;

CSI Controller: 创建/删除,attaching/detaching,快照,扩容等;

CSI Node: attach/mount、umount/detach;

4)真正的后端存储(厂商,AWS, Google, Tencent ...) CSI driver真正操作的后端存储平台,比如csi driver通过腾讯的接口调用创建块存储卷,如果成功则对k8s侧返回一个PV对象

删除操作同理。

动态供应方式总结

通过对比Kubernetes的In-tree Volume Plugin,以及Out-of-tree Provisioner和CSI三种方式,在对接比较常见的存储时,可以使用不需要改动的In-tree方案,因为开箱即用,但是缺点也非常明显,只支持有限的存储类型,可拓展性较差甚至有版本限制,另外官方宣布以后新特性将不再添加到其中。相比之下,使用Out-of-tree Provisioner或者CSI则可以实现和Kubernetes的核心组件解耦,并能支持更多的存储类型和高级特性,因而也是推荐使用的一种供应方式。由于后者对编排系统而言是非侵入式插件部署,因而更受存储供应商的青睐。目前CSI方案是kubernetes社区最为推荐的方式。因此后面还会专门针对CSI进行讲解。