kubernetes存储:local,openEBS,rook ceph

Local 存储(PV)

前面我们有通过 hostPath 或者 emptyDir 的方式来持久化我们的数据,但是显然我们还需要更加可靠的存储来保存应用的持久化数据,这样容器在重建后,依然可以使用之前的数据。但是存储资源和 CPU 资源以及内存资源有很大不同,为了屏蔽底层的技术实现细节,让用户更加方便的使用,Kubernetes 便引入了 PV 和 PVC 两个重要的资源对象来实现对存储的管理。
pod的hostPath,emptyDir,secret,configmap等等其实归根结底还是通过voulme进行挂载,容器的共享volume和挂载主机目录一样也是通过挂载volume来实现,说白了就是这些通过volume进行挂载的对象,一层一层剖析下去其实一般就是普通的主机目录

kubernetes的local存储主要就是两个hostPath和LocalPV,注意这里是指pv,区分开之前普通的hostpath的volume和现在的hostpath类型的PV

概念

PV 的全称是:PersistentVolume(持久化卷),是对底层共享存储的一种抽象,PV 由管理员进行创建和配置,它和具体的底层的共享存储技术的实现方式有关,比如 Ceph、GlusterFS、NFS、hostPath 等,都是通过插件机制完成与共享存储的对接。

PVC 的全称是:PersistentVolumeClaim(持久化卷声明),PVC 是用户存储的一种声明,PVC 和 Pod 比较类似,Pod 消耗的是节点,PVC 消耗的是 PV 资源,Pod 可以请求 CPU 和内存,而 PVC 可以请求特定的存储空间和访问模式。对于真正使用存储的用户不需要关心底层的存储实现细节,只需要直接使用 PVC 即可(实现底层存储技术的隔离解耦)。

但是通过 PVC 请求到一定的存储空间也很有可能不足以满足应用对于存储设备的各种需求,而且不同的应用程序对于存储性能的要求可能也不尽相同,比如读写速度、并发性能等,为了解决这一问题,Kubernetes 又为我们引入了一个新的资源对象:StorageClass,通过 StorageClass 的定义,管理员可以将存储资源定义为某种类型的资源,比如快速存储、慢速存储等,用户根据 StorageClass 的描述就可以非常直观的知道各种存储资源的具体特性了,这样就可以根据应用的特性去申请合适的存储资源了,此外 StorageClass 还可以为我们自动生成 PV,免去了每次手动创建的麻烦。

hostPath

我们上面提到了 PV 是对底层存储技术的一种抽象,PV 一般都是由管理员来创建和配置的,我们首先来创建一个 hostPath 类型的 PersistentVolume。Kubernetes 支持 hostPath 类型的 PersistentVolume 使用节点上的文件或目录来模拟附带网络的存储(终究只是模拟而不是实际的福袋网络地存储),但是需要注意的是在生产集群中,我们不会使用 hostPath,集群管理员会提供网络存储资源,比如 NFS 共享卷(局限较多功能一般,预期说是存储不如说是共享服务器)或 Ceph 存储卷,集群管理员还可以使用 StorageClasses 来设置动态提供存储。因为 Pod 并不是始终固定在某个节点上面的,所以要使用 hostPath 的话我们就需要将 Pod 固定在某个节点上,这样显然就大大降低了应用的容错性。(hostPath类型的volume要求和pod在同一台主机)

先总结一点,普通的hostPath类型的volume和hostPath类型的pv和容器中的都能挂载文件,但容器中的volume只能是目录

将的应用固定在节点 ydzs-node1 上面,首先在该节点上面创建一个 /data/k8s/test/hostpath 的目录,然后在该目录中创建一个 index.html 的文件:
$ echo 'Hello from Kubernetes hostpath storage' > /data/k8s/test/hostpath/index.html

然后接下来创建一个 hostPath 类型的 PV 资源对象:(pv-hostpath.yaml)


apiVersion: v1
kind: PersistentVolume
metadata:
  name: pv-hostpath
  labels:
    type: local
spec:
  storageClassName: manual   #可以用来绑定storageClass
  capacity:   #能力,主要是存储能力,指定了存储空间大小
    storage: 10Gi
  accessModes:
    - ReadWriteOnce
  hostPath:
    path: "/data/k8s/test/hostpath"   #这里的选择目录作为hostPath类型的volume

创建完成后查看 PersistentVolume 的信息,输出结果显示该 PersistentVolume 的状态(STATUS) 为 Available。 这意味着它还没有被绑定给 PersistentVolumeClaim:

$ kubectl get pv pv-hostpath
NAME          CAPACITY   ACCESS MODES   RECLAIM POLICY   STATUS      CLAIM   STORAGECLASS   REASON   AGE
pv-hostpath   10Gi       RWO            Retain           Available           manual             

配置文件中指定了该卷位于集群节点上的 /data/k8s/test/hostpath 目录,还指定了 10G 大小的空间和 ReadWriteOnce 的访问模式,这意味着该卷可以在单个节点上以读写方式挂载,另外还定义了名称为 manual 的 StorageClass,该名称用来将 PersistentVolumeClaim 请求绑定到该 PersistentVolum。

AccessModes(访问模式):用来对 PV 进行访问模式的设置,用于描述用户应用对存储资源的访问权限,访问权限包括下面几种方式:

ReadWriteOnce(RWO):读写权限,但是只能被单个节点挂载
ReadOnlyMany(ROX):只读权限,可以被多个节点挂载
ReadWriteMany(RWX):读写权限,可以被多个节点挂载

一些 PV 可能支持多种访问模式,你可以看pv的accessMode字段的值是个列表,但是在挂载的时候只能使用一种访问模式,多种访问模式是不会生效的。就是看你挂载时用的是哪个访问模式

常用的 Volume 插件支持的访问模式:

在这里插入图片描述
主要是cephFS,NFS,hostPath,hostPath仅支持ReadWriteOnly

其中有一项 RECLAIM POLICY 的配置,同样我们可以通过 PV 的 persistentVolumeReclaimPolicy(回收策略)属性来进行配置,目前 PV 支持的策略有三种:

Retain(保留):保留数据,需要管理员手工清理数据,就是绑定的pvc被删除后,pv仍存在,状态变成release,但依然存留着上一个申领的pvc的信息,这时候一般不会被申领成功
Recycle(回收)(弃用):清除 PV 中的数据,效果相当于执行 rm -rf /thevoluem/*,Recycle 策略会通过运行一个 busybox 容器来执行数据删除命令,默认定义的 busybox 镜像是:gcr.io/google_containers/busybox:latest,并且 imagePullPolicy: Always,如果需要调整配置,需要增加kube-controller-manager 启动参数:--pv-recycler-pod-template-filepath-hostpath 来进行配置。
Delete(删除):与 PV 相连的后端存储完成 volume 的删除操作,当然这常见于云服务商的存储服务,比如 ASW EBS。就是pvc删除后,pv这个api资源对象也会从kubernetes中移除,同时会从外部存储设备(我们这里是主机的目录)移除相关联的存储资产

一般来说还是设置为 Retain 这种策略保险一点。

关于 PV 的状态,实际上描述的是 PV 的生命周期的某个阶段,一个 PV 的生命周期中,可能会处于4种不同的阶段:
Available(可用):表示可用状态,还未被任何 PVC 绑定
Bound(已绑定):表示 PVC 已经被 PVC 绑定
Released(已释放):PVC 被删除,但是资源还未被集群重新声明,其实一般pvc删除后,pv会被成这个状态的,这时候的pv一般都不能用了
Failed(失败): 表示该 PV 的自动回收失败

现在我们创建完成了 PV,如果我们需要使用这个 PV 的话,就需要创建一个对应的 PVC 来和他进行绑定了

现在我们来创建一个 PersistentVolumeClaim,Pod 使用 PVC 来请求物理存储,我们这里创建的 PVC 请求至少 3G 容量的卷,该卷可以为一个节点提供读写访问,下面是 PVC 的配置文件:

apiVersion: v1
kind: PersistentVolumeClaim
metadata:
  name: pvc-hostpath
spec:
  storageClassName: manual
  accessModes:
  - ReadWriteOnce
  resources:   #资源定义
    requests:   #资源的要求大小
      storage: 3Gi

创建 PVC 之后,Kubernetes 就会去查找满足我们声明要求的 PV,比如 storageClassName、accessModes 以及容量这些是否满足要求,如果满足要求就会将 PV 和 PVC 绑定在一起。需要注意的是目前 PV 和 PVC 之间是一对一绑定的关系,也就是说一个 PV 只能被一个 PVC 绑定。

$ kubectl get pv -l type=local
NAME          CAPACITY   ACCESS MODES   RECLAIM POLICY   STATUS   CLAIM                  STORAGECLASS   REASON   AGE
pv-hostpath   10Gi       RWO            Retain           Bound    default/pvc-hostpath   manual                  81m

$ kubectl get pvc pvc-hostpath
NAME           STATUS   VOLUME        CAPACITY   ACCESS MODES   STORAGECLASS   AGE
pvc-hostpath   Bound    pv-hostpath   10Gi       RWO            manual         6m47s

输出结果表明该 PVC 绑定了到了上面我们创建的 pv-hostpath 这个 PV 上面了,我们这里虽然声明的3G的容量,但是由于 PV 里面是 10G,所以显然也是满足要求的。

PVC 准备好过后,接下来我们就可以来创建 Pod 了,该 Pod 使用上面我们声明的 PVC 作为存储卷

apiVersion: v1
kind: Pod
metadata:
  name: pv-hostpath-pod
spec:
  volumes:
  - name: pv-hostpath
    persistentVolumeClaim:   #volume类型时PVC
      claimName: pvc-hostpath
  nodeSelector:
    kubernetes.io/hostname: ydzs-node1   #这里需要注意的是,由于我们创建的 PV 真正的存储在节点 ydzs-node1 上面,所以我们这里必须把 Pod 固定在这个节点下面
  containers:
  - name: task-pv-container
    image: nginx
    ports:
    - containerPort: 80
    volumeMounts:
    - mountPath: "/usr/share/nginx/html"   #挂载的目录,这个目录会成为挂载卷的入口,就跟linux下的文件系统的挂在相类似,html目录下会有hostPath类型的volume的ndex.html
      name: pv-hostpath

另外可以注意到 Pod 的配置文件指定了 PersistentVolumeClaim,但没有指定 PersistentVolume,对 Pod 而言,PVC 就是一个存储卷

运行成功后,我们可以打开一个 shell 访问 Pod 中的容器:


$ kubectl exec -it pv-hostpath-pod -- /bin/bash

yum update
yum install -y curl
curl localhost
Hello from Kubernetes hostpath storage

我们可以看到输出结果是我们前面写到 hostPath 卷种的 index.html 文件中的内容,同样我们可以把 Pod 删除,然后再次重建再测试一次,可以发现内容还是我们在 hostPath 种设置的内容。

我们在持久化容器数据的时候使用 PV/PVC 有什么好处呢?比如我们这里之前直接在 Pod 下面也可以使用 hostPath 来持久化数据,为什么还要费劲去创建 PV、PVC 对象来引用呢?PVC 和 PV 的设计,其实跟“面向对象”的思想完全一致,**PVC 可以理解为持久化存储的“接口”,它提供了对某种持久化存储的描述,但不提供具体的实现;而这个持久化存储的实现部分则由 PV 负责完成。这样做的好处是,作为应用开发者,我们只需要跟 PVC 这个“接口”打交道,而不必关心具体的实现是 hostPath、NFS 还是 Ceph。**毕竟这些存储相关的知识太专业了,应该交给专业的人去做,这样对于我们的 Pod 来说就不用管具体的细节了,你只需要给我一个可用的 PVC 即可了,这样是不是就完全屏蔽了细节和解耦了啊,所以我们更应该使用 PV、PVC 这种方式。

Local PV

上面我们创建了后端是 hostPath 类型的 PV 资源对象,我们也提到了,使用 hostPath 有一个局限性就是,我们的 Pod 不能随便漂移,需要固定到一个节点上,因为一旦漂移到其他节点上去了宿主机上面就没有对应的数据了,所以我们在使用 hostPath 的时候都会搭配 nodeSelector 来进行使用。但是使用 hostPath 明显也有一些好处的,因为 PV 直接使用的是本地磁盘,尤其是 SSD 盘,它的读写性能相比于大多数远程存储来说,要好得多,所以对于一些对磁盘 IO 要求比较高的应用比如 etcd 就非常实用了。不过呢,相比于正常的 PV 来说,使用了 hostPath 的这些节点一旦宕机数据就可能丢失,所以这就要求使用 hostPath 的应用必须具备数据备份和恢复的能力,允许你把这些数据定时备份在其他位置。

所以在 hostPath 的基础上,Kubernetes 依靠 PV、PVC 实现了一个新的特性,这个特性的名字叫作:Local Persistent Volume,也就是我们说的 Local PV。

其实 Local PV 实现的功能就非常类似于 hostPath 加上 nodeAffinity,比如,一个 Pod 可以声明使用类型为 Local 的 PV,而这个 PV 其实就是一个 hostPath 类型的 Volume。如果这个 hostPath 对应的目录,已经在节点 A 上被事先创建好了,那么,我只需要再给这个 Pod 加上一个 nodeAffinity=nodeA,不就可以使用这个 Volume 了吗?理论上确实是可行的,但是事实上,**我们绝不应该把一个宿主机上的目录当作 PV 来使用,因为本地目录的存储行为是完全不可控,它所在的磁盘随时都可能被应用写满,甚至造成整个宿主机宕机。**所以,**一般来说 Local PV 对应的存储介质是一块额外挂载在宿主机的磁盘或者块设备,**我们可以认为就是“一个 PV 一块盘”。

另外一个 Local PV 和普通的 PV 有一个很大的不同在于 Local PV 可以保证 Pod 始终能够被正确地调度到它所请求的 Local PV 所在的节点上面,对于普通的 PV(hostPath) 来说,Kubernetes 都是先调度 Pod 到某个节点上,然后再持久化节点上的 Volume 目录,进而完成 Volume 目录与容器的绑定挂载,但是对于 Local PV 来说,节点上可供使用的磁盘必须是提前准备好的,因为它们在不同节点上的挂载情况可能完全不同,甚至有的节点可以没这种磁盘,所以,这时候,调度器就必须能够知道所有节点与 Local PV 对应的磁盘的关联关系,然后根据这个信息来调度 Pod,实际上就是在调度的时候考虑 Volume 的分布。

hostPath类型的pv,pod是先被调度到节点上,在持久化volume,进而完成volume和容器的绑定
Local PV类型的pv,pod是在调度的时候就考虑节点上的volume的情况在调度

接下来我们来测试下 Local PV 的使用,当然按照上面我们的分析我们应该给宿主机挂载并格式化一个可用的磁盘,我们这里就暂时将 ydzs-node1 节点上的 /data/k8s/localpv 这个目录看成是挂载的一个独立的磁盘。现在我们来声明一个 Local PV 类型的 PV

apiVersion: v1
kind: PersistentVolume
metadata:
  name: pv-local
spec:
  capacity:
    storage: 5Gi
  volumeMode: Filesystem
  accessModes:
  - ReadWriteOnce
  persistentVolumeReclaimPolicy: Delete   #pv的回收策略,一般用于云服务商提供的存储服务,我们这里用的是主机的目录,所以主机提供的存储才是pv的后端存储,要有主机来操作删除资源
  storageClassName: local-storage
  local:
    path: /data/k8s/localpv  # ydzs-node1节点上的目录
  nodeAffinity:
    required:   #pv的借点亲和性是required
      nodeSelectorTerms:
      - matchExpressions:
        - key: kubernetes.io/hostname
          operator: In
          values:
          - ydzs-node1

和前面我们定义的 PV 不同,我们这里定义了一个 local 字段,表明它是一个 Local PV,而 path 字段,指定的正是这个 PV 对应的本地磁盘的路径,即:/data/k8s/localpv,这也就意味着如果 Pod 要想使用这个 PV,那它就必须运行在 ydzs-node1 节点上。所以,在这个 PV 的定义里,添加了一个节点亲和性 nodeAffinity 字段指定 ydzs-node1 这个节点。这样,调度器在调度 Pod 的时候,就能够知道一个 PV 与节点的对应关系,从而做出正确的选择。

local PV是定义是就加上node Affinity

直接创建上面的资源对象:


$ kubectl apply -f pv-local.yaml 
persistentvolume/pv-local created
$ kubectl get pv
NAME      CAPACITY   ACCESS MODES   RECLAIM POLICY   STATUS  CLAIM      STORAGECLASS      REASON   AGE
pv-local  5Gi        RWO            Delete           Available          local-storage              24s
可以看到,这个 PV 创建后,进入了 Available(可用)状态。这个时候如果按照前面提到的,我们要使用这个 Local PV 的话就需要去创建一个 PVC 和他进行绑定

kind: PersistentVolumeClaim
apiVersion: v1
metadata:
  name: pvc-local
spec:
  accessModes:
  - ReadWriteOnce
  resources:
    requests:
      storage: 5Gi
  storageClassName: local-storage

同样要注意声明的这些属性需要和上面的 PV 对应,直接创建这个资源对象:


$ kubectl apply -f pvc-local.yaml 
persistentvolumeclaim/pvc-local created
$ kubectl get pvc
NAME           STATUS   VOLUME        CAPACITY   ACCESS MODES   STORAGECLASS    AGE
pvc-local      Bound    pv-local      5Gi        RWO            local-storage   38s

定义pvc默认就会区绑定符合的pv,通过storageClassName,也会判断pv的资源是否符合

可以看到现在 PVC 和 PV 已经处于 Bound 绑定状态了。但实际上这是不符合我们的需求的,比如现在我们的 Pod 声明使用这个 pvc-local,并且我们也明确规定,这个 Pod 只能运行在 ydzs-node2 这个节点上,如果按照上面我们这里的操作,这个 pvc-local 是不是就和我们这里的 pv-local 这个 Local PV 绑定在一起了,但是这个 PV 的存储券又在 ydzs-node1 这个节点上,显然就会出现冲突了,那么这个 Pod 的调度肯定就会失败了,所以我们在使用 Local PV 的时候,必须想办法延迟这个“绑定”操作。(在pod看来,pvc是存储设备,pvc应该和pod在同一个节点上,也就是说这里的Local PV应该和pod在同一个节点上

storageClassName指定延迟绑定动作

要怎么来实现这个延迟绑定呢?我们可以通过创建 StorageClass 来指定这个动作,在 StorageClass 种有一个 volumeBindingMode=WaitForFirstConsumer 的属性,就是告诉 Kubernetes 在发现这个 StorageClass 关联的 PVC 与 PV 可以绑定在一起,但不要现在就立刻执行绑定操作(即:设置 PVC 的 VolumeName 字段),而是要等到第一个声明使用该 PVC 的 Pod 出现在调度器之后,调度器再综合考虑所有的调度规则,当然也包括每个 PV 所在的节点位置,来统一决定,这个 Pod 声明的 PVC,到底应该跟哪个 PV 进行绑定。通过这个延迟绑定机制,原本实时发生的 PVC 和 PV 的绑定过程,就被延迟到了 Pod 第一次调度的时候在调度器中进行,从而保证了这个绑定结果不会影响 Pod 的正常调度。

所以我们需要创建对应的 StorageClass 对象:(local-storageclass.yaml)


apiVersion: storage.k8s.io/v1
kind: StorageClass
metadata:
  name: local-storage
provisioner: kubernetes.io/no-provisioner   #不需要storageClass动态生成pv,使用我们前面手动创建的pv
volumeBindingMode: WaitForFirstConsumer

这个 StorageClass 的名字,叫作 local-storage,也就是我们在 PV 中声明的,需要注意的是,在它的 provisioner 字段,我们指定的是 no-provisioner。这是因为我们这里是手动创建的 PV,所以不需要动态来生成 PV,另外这个 StorageClass 还定义了一个 volumeBindingMode=WaitForFirstConsumer 的属性,它是 Local PV 里一个非常重要的特性,即:延迟绑定。通过这个延迟绑定机制,原本实时发生的 PVC 和 PV 的绑定过程,就被延迟到了 Pod 第一次调度的时候在调度器中进行,从而保证了这个绑定结果不会影响 Pod 的正常调度。

现在我们来创建这个 StorageClass 资源对象:


$ kubectl apply -f local-storageclass.yaml 
storageclass.storage.k8s.io/local-storage created
现在我们重新删除上面声明的 PVC 对象,重新创建:


$ kubectl delete -f pvc-local.yaml 
persistentvolumeclaim "pvc-local" deleted
$ kubectl create -f pvc-local.yaml
persistentvolumeclaim/pvc-local created
$ kubectl get pvc
NAME           STATUS    VOLUME        CAPACITY   ACCESS MODES   STORAGECLASS    AGE
pvc-local      Pending                                           local-storage   3s
我们可以发现这个时候,集群中即使已经存在了一个可以与 PVC 匹配的 PV 了,但这个 PVC 依然处于 Pending 状态,也就是等待绑定的状态,这就是因为上面我们配置的是延迟绑定,需要在真正的 Pod 使用的时候才会来做绑定。
注意pvc删除后pv变成release状态,等待重新绑定,上面这个pv的回收策略是delete,pv可能会也被自动删除了,delete回收模式一般是在云服务商提供的存储服务中,留意一个坑,pv成了release状态,一般不能接收新的pvc的申领,即使该pvc跟上一个pvc相同一般都不会申领成功,最后pv的回收策略建议设置为retain

同样我们声明一个 Pod 来使用这里的 pvc-local 这个 PVC,资源对象如下所示:(pv-local-pod.yaml)


apiVersion: v1
kind: Pod
metadata:
  name: pv-local-pod
spec:
  volumes:
  - name: example-pv-local
    persistentVolumeClaim:
      claimName: pvc-local
  containers:
  - name: example-pv-local
    image: nginx
    ports:
    - containerPort: 80
    volumeMounts:
    - mountPath: /usr/share/nginx/html
      name: example-pv-local
直接创建这个 Pod:


$ kubectl apply -f pv-local-pod.yaml 
pod/pv-local-pod created
创建完成后我们这个时候去查看前面我们声明的 PVC,会立刻变成 Bound 状态,与前面定义的 PV 绑定在了一起:


$ kubectl get pvc
NAME           STATUS   VOLUME        CAPACITY   ACCESS MODES   STORAGECLASS    AGE
pvc-local      Bound    pv-local      5Gi        RWO            local-storage   4m59s

这时候,我们可以尝试在这个 Pod 的 Volume 目录里,创建一个测试文件,比如:


$ kubectl exec -it pv-local-pod /bin/sh
# cd /usr/share/nginx/html
# echo "Hello from Kubernetes local pv storage" > test.txt  
# 
然后,登录到 ydzs-node1 这台机器上,查看一下它的 /data/k8s/localpv 目录下的内容,你就可以看到刚刚创建的这个文件:


# 在ydzs-node1节点上
$ ls /data/k8s/localpv
test.txt
$ cat /data/k8s/localpv/test.txt 
Hello from Kubernetes local pv storage
如果重新创建这个 Pod 的话,就会发现,我们之前创建的测试文件,依然被保存在这个持久化 Volume 当中:


$ kubectl delete -f pv-local-pod.yaml  
$ kubectl apply -f pv-local-pod.yaml 
$ kubectl exec -it pv-local-pod /bin/sh
# ls /usr/share/nginx/html
test.txt
# cat /usr/share/nginx/html/test.txt
Hello from Kubernetes local pv storage
# 

说明基于本地存储的 Volume 是完全可以提供容器持久化存储功能的,对于 StatefulSet 这样的有状态的资源对象,也完全可以通过声明 Local 类型的 PV 和 PVC,来管理应用的存储状态。

pv的删除流程

需要注意的是,我们上面手动创建 PV 的方式,即静态的 PV 管理方式,在删除 PV 时需要按如下流程执行操作:

删除使用这个 PV 的 Pod
从宿主机移除本地磁盘(或者删除目录,比如这里就是)
删除 PVC
删除 PV
如果不按照这个流程的话,这个 PV 的删除就会失败。

总结:
hostPath pv就是最普通的pv,pod创建时要使用nodeSelector来制定pv所在的节点的,Local PV类似hostPath pv加nodeAffinity,定义Local PV是写明nodeAffinity让调度器知道pv和节点的关系,创建pod时就不用使用nodeSelector就可以调度到正确的节点上

用storageClass实现动态pv
但是实际情况下,管理员并不清楚用户需要什么样大小的存储卷,也没有办法在预先创建各种大小的PV
最好的效果是用户创建指定大小的pvc,则就自动创建同样大小的pv并关联用户的pvc
Kubernetes通过创建StorageClass来使用 Dynamic Provisioning 特性,storageclass需要有一个provisioner来决定使用什么样的存储插件来动态创建pvc,比如是glusterfs存储,cephfs存储等等
后续的存储主要就是写动态存储,与storageclass相关

记住一个概念storageclass有通过指定的pvc即可自动创建对应的pv的功能即可

OpenEBS存储

OpenEBS 是一种模拟了 AWS 的 EBS、阿里云的云盘等块存储实现基于容器的存储开源软件OpenEBS 是一种基于 CAS(Container Attached Storage) 理念的容器解决方案,其核心理念是存储和应用一样采用微服务架构,并通过 Kubernetes 来做资源编排。其架构实现上,每个卷的 Controller 都是一个单独的 Pod,且与应用 Pod 在同一个节点,卷的数据使用多个 Pod 进行管理。

在这里插入图片描述
OpenEBS 有很多组件,可以分为以下几类:

控制平面组件 - 管理 OpenEBS 卷容器,通常会用到容器编排软件的功能
数据平面组件 - 为应用程序提供数据存储,包含 Jiva 和 cStor 两个存储后端
节点磁盘管理器 - 发现、监控和管理连接到 Kubernetes 节点的媒体
与云原生工具的整合 - 与 Prometheus、Grafana、Fluentd 和 Jaeger 进行整合。

控制平面

OpenEBS 集群的控制平面通常被称为 Maya,控制平面负责供应卷、相关的卷操作,如快照、制作克隆、创建存储策略、执行存储策略、导出卷指标供 Prometheus/grafana 消费等

OpenEBS 控制平面 Maya 实现了创建超融合的 OpenEBS,并将其挂载到如 Kubernetes 调度引擎上,用来扩展特定的容器编排系统提供的存储功能;OpenEBS 的控制平面也是基于微服务的,通过不同的组件实现存储管理功能、监控、容器编排插件等功能。

OpenEBS 提供了一个动态供应器(供应pv),它是标准的 Kubernetes 外部存储插件(用来拓展kubernetes这个容器编排系统的存储功能,openEBS就是基于CAS的存储开源软件)。OpenEBS PV 供应器的主要任务是向应用 Pod 发起卷供应,并实现Kubernetes 的 PV 规范。

m-apiserver 暴露了存储 REST API,并承担了大部分的卷策略处理和管理。

控制平面和数据平面之间的连接采用 Kubernetes sidecar 模式。有如下几个场景,控制平面需要与数据平面进行通信。

对于 IOPS、吞吐量、延迟等卷统计 - 通过 volume-exporter sidecar实现
用于通过卷控制器 Pod 执行卷策略,以及通过卷复制 Pod 进行磁盘/池管理 - 通过卷管理 sidecar 实现。

OpenEBS PV Provisioner

该组件作为一个 Pod 运行,并做出供应决策。它的使用方式是开发者用所需的卷参数构建一个请求,选择合适的存储类,并在 YAML 规范上调用 kubelet(就是从m-apiserver拿到pv策略,进而调用kubelet生成需要的数据卷)。OpenEBS PV 动态供应器与maya-apiserver 交互,在适当的节点上为卷控制器 Pod 和卷复制 Pod 创建部署规范。可以使用 PVC 规范中的注解来控制容量 Pod(控制器/副本)的调度。

目前,OpenEBS 供应器只支持一种类型的绑定,即 iSCSI。

ISCSI:Internet Small Computer System Interface,internet小型计算机系统接口,是一种在TCP/IP上进行数据块传输的接口,实现在IP网络运行SCSI协议,使其能够在以太网中进行快速的数据存储备份操作,也就是基于网络的存储

Maya-apiserver

即m-apiserver
m-apiserver 作为一个 Pod 运行,主要是用来暴露 OpenEBS REST APIs

m-apiserver 还负责创建创建卷 Pod 所需的部署规范文件,在生成这些规范文件后,它调用 kube-apiserver 来相应地调度Pods。在 OpenEBS PV 供应器(OpenEBS PVProvisioner)的卷供应结束时,会创建一个Kubernetes 对象 PV,并挂载在应用 Pod 上,PV由控制器 Pod 托管,控制器 Pod(数据平面的Cstor和Jiva) 由一组位于不同节点的副本 Pod 支持,控制器 Pod和副本 Pod 是数据平面的一部分,。

m-apiserver 的另一个重要任务是卷策略管理。OpenEBS 提供了非常细化的规范来表达策略,m-apiserver 解释这些 YAML 规范,将其转换为可执行的组件,并通过卷管理 sidecar 来执行。

Maya Volume Exporter

Maya Volume Exporter 是每个存储控制器 Pod(cStor/Jiva)的 sidecar这些 sidecars 将控制平面与数据平面连接起来,以获取统计数据(将数据暴露给云原生工具比如prometheus是通过控制平面),比如:

volume 读/写延迟
读/写 IOPS
读/写块大小
容量统计
OpenEBS volume exporter 数据流

IOPS(Input/Output per second):美妙的读写次数或者输入输出量,衡量磁盘性能的主要指标之一

Volume 管理 Sidecars

Volume Management sidecar

Sidecars 还用于将控制器配置参数和卷策略传递给作为数据平面的卷控制器 Pod,以及将副本配置参数和副本数据保护参数传递给卷副本 Pod。

所以sidecar属于控制平面的,连接控制平面和数据平面,将卷控制器pod的数据比如对应的pv的读写IOPS,和磁盘容量等返回给控制平面,同时将控制平面的卷策略等发给卷控制器pod,由它来管理pv

节点磁盘管理器

Node Disk Manager (NDM)填补了使用 Kubernetes 管理有状态应用的持久性存储所需的工具链中的空白。容器时代的 DevOps 架构师必须以自动化的方式服务于应用和应用开发者的基础设施需求,以提供跨环境的弹性和一致性。这些要求意味着存储栈本身必须非常灵活,以便 Kubernetes 和云原生生态系统中的其他软件可以轻松使用这个存储栈。NDM 在 Kubernetes 的存储栈中起到了基础性的作用,它将不同的磁盘统一起来,并通过将它们识别为 Kubernetes 对象来提供部分池化的能力。同时, NDM 还可以发现、供应、监控和管理底层磁盘,这样Kubernetes PV 供应器(如 OpenEBS 和其他存储系统和Prometheus)可以管理磁盘子系统。

NDM填补kubernetes管理底层磁盘的工具链的空白,让kubernets的pv供应器或者其他的存储方案比如OpenEBS可以管理磁盘子系统
以daemonset方式运行在kubernetes中,用来做数据在节点磁盘的真正的持久化

数据平面

OpenEBS 持久化存储卷通过 Kubernetes 的 PV 来创建,使用 ISCSI 来实现(用于实现网络存储的协议),数据保存在节点上或者云存储中(ISCSI来进行数据的传输存储备份,所以OpenEBS这个存储软件部署在容器或者说kubernetes上,通过使用kubernetes的pv来创建OpenEBS持久化数据卷,使用ISCSI协议进行数据的世纪存储,是个网络存储设备或者说是存储方案)。OpenEBS 的卷完全独立于用户的应用的生命周期来管理,和 Kuberentes 中 PV 的思路一致。OpenEBS 卷为容器提供持久化存储,具有针对系统故障的弹性,更快地访问存储,快照和备份功能。同时还提供了监控使用情况和执行 QoS 策略的机制。

目前,OpenEBS 提供了两个可以轻松插入的存储引擎(卷控制器pod)。这两个引擎分别叫做 Jiva 和 cStor。这两个存储引擎都完全运行在Linux 用户空间(节点上,或者云服务器,对卷做真正的持久化)中,并且基于微服务架构(微服务简单来看就是将原本的单体应用进行拆分,这里不一定是说卷控制器使用微服务的方案来部署,而是说它用为服务类型的框架来开发)。

Jiva

Jiva 存储引擎是基于 Rancher 的 LongHorn 和 gotgt 开发的,采用 GO 语言编写,运行在用户空间。LongHorn 控制器将传入的 IO 同步复制到 LongHorn 复制器上。复制器考虑以 Linux 稀疏文件为基础,进行动态供应、快照、重建等存储功能。

cStor

cStor 数据引擎是用C语言编写的,具有高性能的 iSCSI 目标和Copy-On-Write 块系统,可提供数据完整性、数据弹性和时间点快照和克隆。cStor 具有池功能,可将节点上的磁盘以镜像式或 RAIDZ 模式聚合,以提供更大的容量和性能单位。

Local PV(普通情况下使用,网络性较小)

对于那些不需要存储级复制的应用,Local PV 可能是不错的选择,因为它能提供更高的性能。OpenEBS LocalPV 与 Kubernetes LocalPV 类似,只不过它是由 OpenEBS 控制平面动态调配的,就像其他常规 PV 一样。OpenEBS LocalPV 有两种类型–主机路径 LocalPV 或设备 LocalPV(和kubernetes的原生的Local PV一样,实际的存储可以是目录也可以是设备),主机路径 LocalPV 指的是主机上的一个子目录,设备 LocalPV 指的是节点上的一个被发现的磁盘(直接连接或网络连接)(网络连接磁盘,读写磁盘,ISCSI协议)。OpenEBS 引入了一个LocalPV 供应器,用于根据 PVC 和存储类规范中的一些标准选择匹配的磁盘或主机路径。

注意OpenEBS的pv其实也是通过kubernetes的pv这个api资源对象实现的,比较大的不同的相对于pv和pvc,OpenEBS的pv是动态分配的,跟storageclass使用某个provisioner时一样,其实高级点的存储方案都得实现动态分配pv这个功能

安装

由于 OpenEBS 通过 iSCSI 协议提供存储支持(所以要在真正存储数据的地方开启ISCSI功能),因此,需要在所有 Kubernetes 节点上(其实存储到云服务器也一样,云计算服务的存储能力后端对应的其实也是一台台实际的服务器)都安装 iSCSI 客户端(启动器)。

比如我们这里使用的是 CentOS 的系统,执行下面的命令安装启动 iSCSI 启动器:

# 安装 iscsi
$ yum install iscsi-initiator-utils -y

# 查看 InitiatorName 是否正常配置
$ cat /etc/iscsi/initiatorname.iscsi

# 启动查看状态
$ systemctl enable --now iscsid
$ systemctl start iscsid.service
$ systemctl status iscsid.service

iSCSI 客户端启动完成后就可以开始安装 OpenEBS 了。

直接使用下面的命令安装 OpenEBS 即可(将OpenEBS部署在kubernetes上):

$ kubectl apply -f https://openebs.github.io/charts/openebs-operator.yaml

该命令会将应用安装到名为 openebs 的命名空间中,安装成功后正常可以看到如下所示的 Pod:

$ kubectl get pods -n openebs                               
NAME                                           READY   STATUS    RESTARTS   AGE
maya-apiserver-5db4c7f9bc-fv9sc                1/1     Running   0          19h
openebs-admission-server-6c64d9ff64-sklvp      1/1     Running   0          19h
openebs-localpv-provisioner-784d8f9b56-9mphk   1/1     Running   1          19h
openebs-ndm-fdlpx                              1/1     Running   0          19h
openebs-ndm-jfxbj                              1/1     Running   0          19h
openebs-ndm-operator-6d5978d6fb-swp65          1/1     Running   0          19h
openebs-provisioner-7b99c87dbf-zpxqn           1/1     Running   1          19h
openebs-snapshot-operator-69b9f8cd8b-r6hrn     2/2     Running   1          19h
默认情况下 OpenEBS 还会安装一些内置的 StorageClass 对象:


$ kubectl get sc
NAME                        PROVISIONER                                                RECLAIMPOLICY   VOLUMEBINDINGMODE      ALLOWVOLUMEEXPANSION   AGE
openebs-device              openebs.io/local                                           Delete          WaitForFirstConsumer   false                  19h
openebs-hostpath            openebs.io/local                                           Delete          WaitForFirstConsumer   false                  19h
openebs-jiva-default        openebs.io/provisioner-iscsi                               Delete          Immediate              false                  19h
openebs-snapshot-promoter   volumesnapshot.external-storage.k8s.io/snapshot-promoter   Delete

测试(一般简单的使用就是用OpenEBS的local PV)

接下来我们创建一个 PVC 资源对象,Pods 使用这个 PVC 就可以从 OpenEBS 动态 Local PV Provisioner 中请求 Hostpath Local PV 了。(主要就是为了实现动态创建分配pv的功能)

直接使用上面自带的 openebs-hostpath 这个 StorageClass (所以说动态分配其实还是使用了kubernetes的原生的storageClass来实现,storageClass可以在定义是指定provisioner,这里的存储方案比如OpenEBS知识将storageClass封装起来直接使用)来创建 PVC:

# local-hostpath-pvc.yaml
apiVersion: v1
kind: PersistentVolumeClaim
metadata:
  name: local-hostpath-pvc
spec:
  storageClassName: openebs-hostpath
  accessModes:
    - ReadWriteOnce
  resources:
    requests:
      storage: 5Gi
直接创建这个 PVC 即可:


$ kubectl apply -f local-hostpath-pvc.yaml
$ kubectl get pvc local-hostpath-pvc
NAME                 STATUS    VOLUME   CAPACITY   ACCESS MODES   STORAGECLASS       AGE
local-hostpath-pvc   Pending                                      openebs-hostpath   12s

我们可以看到这个 PVC 的状态是 Pending,这是因为对应的 StorageClass 是延迟绑定模式,所以需要等到 Pod 消费这个 PVC 后才会去绑定,接下来我们去创建一个 Pod 来使用这个 PVC。

声明一个如下所示的 Pod 资源清单:

# local-hostpath-pod.yaml
apiVersion: v1
kind: Pod
metadata:
  name: hello-local-hostpath-pod
spec:
  volumes:
  - name: local-storage
    persistentVolumeClaim:
      claimName: local-hostpath-pvc
  containers:
  - name: hello-container
    image: busybox
    command:
       - sh
       - -c
       - 'while true; do echo "`date` [`hostname`] Hello from OpenEBS Local PV." >> /mnt/store/greet.txt; sleep $(($RANDOM % 5 + 300)); done'
    volumeMounts:
    - mountPath: /mnt/store
      name: local-storage
直接创建这个 Pod:


$ kubectl apply -f local-hostpath-pod.yaml
$ kubectl get pods hello-local-hostpath-pod          
NAME                       READY   STATUS    RESTARTS   AGE
hello-local-hostpath-pod   1/1     Running   0          2m7s
$ kubectl get pvc local-hostpath-pvc           
NAME                 STATUS   VOLUME                                     CAPACITY   ACCESS MODES   STORAGECLASS       AGE
local-hostpath-pvc   Bound    pvc-3f4a1a65-6cbc-42bf-a1f8-87ad238c0b88   5Gi        RWO            openebs-hostpath   5m41s
可以看到 Pod 运行成功后,PVC 也绑定上了一个自动生成的 PV,我们可以查看这个 PV 的详细信息:


$ kubectl get pv pvc-3f4a1a65-6cbc-42bf-a1f8-87ad238c0b88 -o yaml
apiVersion: v1
kind: PersistentVolume
metadata:
  annotations:
    pv.kubernetes.io/provisioned-by: openebs.io/local
  creationTimestamp: "2021-01-07T02:48:14Z"
  finalizers:
  - kubernetes.io/pv-protection
  labels:
    openebs.io/cas-type: local-hostpath
  ......
  name: pvc-3f4a1a65-6cbc-42bf-a1f8-87ad238c0b88
  resourceVersion: "21193802"
  selfLink: /api/v1/persistentvolumes/pvc-3f4a1a65-6cbc-42bf-a1f8-87ad238c0b88
  uid: f7cccdb3-d23a-4831-86c3-4363eb1a8dee
spec:
  accessModes:
  - ReadWriteOnce
  capacity:
    storage: 5Gi
  claimRef:
    apiVersion: v1
    kind: PersistentVolumeClaim
    name: local-hostpath-pvc
    namespace: default
    resourceVersion: "21193645"
    uid: 3f4a1a65-6cbc-42bf-a1f8-87ad238c0b88
  local:
    fsType: ""
    path: /var/openebs/local/pvc-3f4a1a65-6cbc-42bf-a1f8-87ad238c0b88
  nodeAffinity:
    required:
      nodeSelectorTerms:
      - matchExpressions:
        - key: kubernetes.io/hostname
          operator: In
          values:
          - node2
  persistentVolumeReclaimPolicy: Delete
  storageClassName: openebs-hostpath
  volumeMode: Filesystem
status:
  phase: Bound

动态创建的pv一般都会自动生成节点亲和性的属性,前面的Local PV是手动加节点亲和性,pv加上了节点亲和性技能让调度器了解pv和节点对应的关系,创建pod时就能根据该pod想要申请的pvc对应的满足的pv将pod调度到合理的节点上,毕竟不管是kubernetes原生的local pv还是OpenEBS的local pv都一样,需要pod与pv在同一节点(所以使用OpenEBS,创建了个pvc,创建使用该pvc的pod时,其实先创建了合适的pv,在将pod进行调度)

我们可以看到这个自动生成的 PV 和我们前面自己手动创建的 Local PV 基本上是一致的,和 node2 节点是亲和关系,本地数据目录位于 /var/openebs/local/pvc-3f4a1a65-6cbc-42bf-a1f8-87ad238c0b88 下面。

接着我们来验证下 volume 数据,前往 node2 节点查看下上面的数据目录中的数据:

[root@node2 ~]# ls /var/openebs/local/pvc-3f4a1a65-6cbc-42bf-a1f8-87ad238c0b88
greet.txt
[root@node2 ~]# cat /var/openebs/local/pvc-3f4a1a65-6cbc-42bf-a1f8-87ad238c0b88/greet.txt
Thu Jan  7 10:48:49 CST 2021 [hello-local-hostpath-pod] Hello from OpenEBS Local PV.
Thu Jan  7 10:53:50 CST 2021 [hello-local-hostpath-pod] Hello from OpenEBS Local PV.

可以看到 Pod 容器中的数据已经持久化到 Local PV 对应的目录中去了。但是需要注意的是 StorageClass 默认的数据回收策略是 Delete,所以如果将 PVC 删掉后数据会自动删除,我们可以 Velero 这样的工具来进行备份还原。

ceph

Ceph 是一个统一的分布式存储系统,提供较好的性能、可靠性和可扩展性。最早起源于 Sage 博士期间的工作,随后贡献给开源社区。

简介

高性能

1.抛弃了传统的集中式存储运输局寻址的方案,采用 CRUSH 算法,数据分布均衡,并行度高。
2.考虑了容灾域的隔离,能够实现各类负载的副本设置规则,例如跨机房、机架感知等。
3.能够支持上千个存储节点的规模,支持 TB 到 PB 级的数据。

高可用性

1,副本数可以灵活控制
2.支持故障域分离,数据强一致性
3.多种故障场景自动进行修复自愈
4.没有单点故障,自动管理

高可扩展性(分布式的特性)

1.去中心化
2.扩展灵活
3.随着节点增加而线性增长

特性丰富

支持三种存储接口:块存储、文件存储、对象存储
支持自定义接口,支持多种语言驱动

架构

支持三种接口
(存储接口:对象,块,文件

Object:有原生 API,而且也兼容 Swift 和 S3 的 API
Block:支持精简配置、快照、克隆
File:Posix 接口,支持快照

在这里插入图片描述

组件

Monitor:一个 Ceph 集群需要多个 Monitor 组成的小集群,它们通过 Paxos 同步数据,用来保存 OSD 的元数据。(监视器,监督员)

OSD:全称 Object Storage Device,对象存储设备,也就是负责响应客户端请求返回具体数据的进程,一个 Ceph 集群一般都有很多个 OSD。主要功能用于数据的存储,当直接使用硬盘作为存储目标时,一块硬盘称之为 OSD,当使用一个目录作为存储目标的时候,这个目录也被称为 OSD。(用于存储数据和相应客户端的请求返回数据,可以是一块硬盘或目录)

MDS:全称 Ceph Metadata Server,是 CephFS 服务依赖的元数据服务,对象存储和块设备存储不需要该服务。(文件存储接口可能会用到)

Object:Ceph 最底层的存储单元是 Object 对象一条数据、一个配置都是一个对象,每个 Object 包含 ID、元数据和原始数据。

Pool:Pool 是一个存储对象的逻辑分区(存储池),它通常规定了数据冗余的类型与副本数,默认为3副本。对于不同类型的存储,需要单独的 Pool,如 RBD。

PG:全称 Placement Grouops,是一个逻辑概念一个 OSD 包含多个 PG。引入 PG 这一层其实是为了更好的分配数据和定位数据。每个 Pool 内包含很多个 PG,它是一个对象的集合,服务端数据均衡和恢复的最小单位就是 PG。(就是将OSD更细分出PG,定位组)
在这里插入图片描述

pool 是 ceph 存储数据时的逻辑分区,它起到 namespace 的作用
每个 pool 包含一定数量(可配置)的 PG
PG里的对象被映射到不同的 Object 上
pool 是分布到整个集群的

OSD存储数据和返回数据,PG是OSD的中的逻辑概念,为了更细化管理数据,pool分布在整个ceph集群,每个pool包含多个PG,PG里面的内容其实就是底层存储单元Object

FileStore与BlueStore:FileStore 是老版本默认使用的后端存储引擎,如果使用 FileStore,建议使用 xfs 文件系统。BlueStore 是一个新的后端存储引擎,可以直接管理裸硬盘,抛弃了 ext4 与 xfs 等本地文件系统。可以直接对物理硬盘进行操作,同时效率也高出很多。

RADOS:全称 Reliable Autonomic Distributed Object Store(可靠的自动分配对象存储),是 Ceph 集群的精华,用于实现数据分配、Failover 等集群操作。

Librados:Librados 是 Rados 提供库(支持多种语言),因为 RADOS 是协议很难直接访问,因此上层的 RBD、RGW 和 CephFS 都是通过 librados 访问的,目前提供 PHP、Ruby、Java、Python、C 和 C++ 支持。

CRUSH:CRUSH 是 Ceph 使用的数据分布算法,类似一致性哈希,让数据分配到预期的地方。

RBD:全称 RADOS Block Device,是 Ceph 对外提供的块设备服务,如虚拟机硬盘,支持快照功能。

RGW:全称是 RADOS Gateway,是 Ceph 对外提供的对象存储服务,接口与 S3 和 Swift 兼容。

CephFS:全称 Ceph File System,是 Ceph 对外提供的文件系统服务。

其实也可见ceph内部的存储形式是对象存储

块存储(RBD)

典型设备

磁盘阵列,硬盘,主要是将裸磁盘空间映射给主机使用的。(linux的raid,lvm等)

优点
通过 Raid 与 LVM 等手段,对数据提供了保护。
多块廉价的硬盘组合起来,提高容量。
多块磁盘组合出来的逻辑盘,提升读写效率。(逻辑盘提升读写效率)

缺点
采用 SAN 架构组网时,光纤交换机,造价成本高。
主机之间无法共享数据。

使用场景
Docker 容器、虚拟机磁盘存储分配。
日志存储
文件存储

文件存储(CephFS)

典型设备 FTP、NFS 服务器,为了克服块存储文件无法共享的问题,所以有了文件存储,在服务器上架设 FTP 与 NFS 服务器,就是文件存储。

优点
造价低,随便一台机器就可以了
方便文件可以共享

缺点
读写速率低
传输速率慢

使用场景
日志存储
有目录结构的文件存储

对象存储(RGW)

典型设备
内置大容量硬盘的分布式服务器(swift, s3);多台服务器内置大容量硬盘,安装上对象存储管理软件,对外提供读写访问功能。

优点
具备块存储的读写高速。
具备文件存储的共享等特性

使用场景:(适合更新变动较少的数据)
图片存储
视频存储

部署

由于我们这里在 Kubernetes 集群中使用,也为了方便管理,我们这里使用 Rook 来部署 Ceph 集群,Rook 是一个开源的云原生存储编排工具,提供平台、框架和对各种存储解决方案的支持,以和云原生环境进行本地集成。(不只可以用来实现ceph分布式存储方案与云原生环境集成,还可以实现别的很多存储方案,就是将各种存储方案与云原生环境集成)

Rook 将存储软件转变成自我管理、自我扩展和自我修复的存储服务,通过自动化部署、启动、配置、供应、扩展、升级、迁移、灾难恢复、监控和资源管理来实现Rook 底层使用云原生容器管理、调度和编排平台提供的能力来提供这些功能,其实就是我们平常说的 Operator。Rook 利用扩展功能将其深度集成到云原生环境中,并为调度、生命周期管理、资源管理、安全性、监控等提供了无缝的体验。有关 Rook 当前支持的存储解决方案的状态的更多详细信息,可以参考 Rook 仓库 的项目介绍。

rook官网文档

在这里插入图片描述
可以看出来就是加上rook这个operator层,将存储集群比如这里是ceph集成到云原生环境中

Rook 包含多个组件:
Rook Operator:Rook 的核心组件,Rook Operator 是一个简单的容器,自动启动存储集群(ceph),并监控存储守护进程,来确保存储集群的健康。
Rook Agent:在每个存储节点上运行,并配置一个 FlexVolume 或者 CSI 插件,和 Kubernetes 的存储卷控制框架进行集成。Agent 处理所有的存储操作,例如挂接网络存储设备、在主机上加载存储卷以及格式化文件系统等。
Rook Discovers:检测挂接到存储节点上的存储设备。

Rook 还会用 Kubernetes Pod 的形式,部署 Ceph 的 MON、OSD 以及 MGR 守护进程(每个ceph集群包含多个monitor集群)。Rook Operator 让用户可以通过 CRD 来创建和管理存储集群。每种资源都定义了自己的 CRD:
RookCluster:提供了对存储机群的配置能力,用来提供块存储、对象存储以及共享文件系统。每个集群都有多个 Pool。
Pool:为块存储提供支持,Pool 也是给文件和对象存储提供内部支持。
Object Store:用 S3 兼容接口开放存储服务。
File System:为多个 Kubernetes Pod 提供共享存储。

环境

Rook Ceph 需要使用 RBD (RADOS Block Device)内核模块,我们可以通过运行 lsmod|grep rbd 来测试 Kubernetes 节点是否有该模块,如果没有,则需要更新下内核版本。

内核升级参考

开启命令
modprobe RBD

开启了内核模块还要考虑内接模块的开机自启
创建/etc/sysconfig/modules/*.modules,例如要加载的模块叫rbd,然后vim创建rbd.modules,内容为:modprobe rbd
(sysctl 文件名可以动态修改和参数)

为*.modules文件加可执行权限,我这里这就执行:chmod rbd.modules

另外需要在节点上安装 lvm2 软件包:

# Centos
sudo yum install -y lvm2

安装

我们这里部署的 release-1.5 版本的 Rook,部署清单文件地址:

https://github.com/rook/rook/tree/release-1.5/cluster/examples/kubernetes/ceph

当然你可以自主选择版本来下载(这里的github.com下载慢或下载不了,同样和raw.githubusercontent.com一样可以去ipaddress网站解析个ip出来写进/etc/hosts)(这里部署在kubernetes上,其实无非就是yaml文件,想办法搞到就行,你也可以直接浏览器打开网址复制文件下来)

从上面链接中下载 common.yaml 与 operator.yaml 两个资源清单文件:


# 会安装rbac相关资源对象
$ kubectl apply -f common.yaml
# 安装 CRD
$ kubectl apply -f crds.yaml
# 安装 rook operator
$ kubectl apply -f operator.yaml

在继续操作之前,验证 rook-ceph-operator 是否处于“Running”状态:


$ kubectl get pods -n rook-ceph                                   
NAME                                  READY   STATUS    RESTARTS   AGE
rook-ceph-operator-559b6fcf59-qrd2p   1/1     Running   5          58m

当 Rook Operator 处于 Running 状态,我们就可以创建 Ceph 集群了。为了使集群在重启后不受影响,请确保设置的 dataDirHostPath 属性值为有效得主机路径

cluster.yaml 是生产存储集群配置,需要至少三个节点(master节点默认有污点,加多个node或者去污点或者加个容忍)
cluster-test.yaml 是测试集群配置,只需要一个节点

创建如下的资源清单文件:(cluster.yaml可以再上述的rook的网址上下载)


apiVersion: ceph.rook.io/v1
kind: CephCluster
metadata:
  name: rook-ceph
  namespace: rook-ceph
spec:
  cephVersion:
    # ceph 镜像, 可以查看 https://hub.docker.com/r/ceph/ceph/tags
    image: ceph/ceph:v15.2.8
  dataDirHostPath: /var/lib/rook  # 用于存储rook的相关配置的主机目录
  mon:  # monitor 的数量(一般设置大于1的奇数)
    count: 3
  dashboard:  # 开启dashboard
    enabled: true
  storage:  # 整个集群的存储配置(可以单独为某个节点配置进行覆盖)
    useAllNodes: true
    useAllDevices: false
    # 重要: Directories 应该只在预生产环境中使用
    #directories:
    #- path: /data/rook

其中有几个比较重要的字段:

dataDirHostPath:宿主机上的目录,用于每个服务存储配置和数据。如果目录不存在,会自动创建该目录。由于此目录在主机上保留,因此在删除 Pod 后将保留该目录,另外不得使用以下路径及其任何子路径:/etc/ceph、/rook 或 /var/log/ceph。如果删除rook ceph集群并在同一主机上启动新集群,则 dataDirHostPath 必须删除使用的路径,否则,过时的配置将保留在先前的集群中,导致新的 mons 将无法启动。
useAllNodes:用于表示是否使用集群中的所有节点进行存储,如果在 nodes 字段下指定了节点,则必须将 useAllNodes 设置为 false。
useAllDevices:表示 OSD 是否自动使用节点上的所有设备,一般设置为 false,这样可控性较高
directories:这里使用目录来存储,一般来说应该使用一块裸盘来做存储,为了测试方便,使用一个目录也是可以的,当然生成环境不推荐使用目录。
(这个字段现在已经启用了)

上面的两个use都会有ceph集群的特性就是动态发现加进来的存储用的磁盘和ceph集群节点,可以拓展ceph集群

这里同时放上官网cluster.yaml完整的(你也可以直接用这个文件,参考上面的更改):
镜像默认是去k8s.gcr.io下载,下载不了可以去阿里下载在打标签

#################################################################################################################
# Define the settings for the rook-ceph cluster with common settings for a production cluster.
# All nodes with available raw devices will be used for the Ceph cluster. At least three nodes are required
# in this example. See the documentation for more details on storage settings available.

# For example, to create the cluster:
#   kubectl create -f crds.yaml -f common.yaml -f operator.yaml
#   kubectl create -f cluster.yaml
#################################################################################################################

apiVersion: ceph.rook.io/v1
kind: CephCluster
metadata:
  name: rook-ceph
  namespace: rook-ceph # namespace:cluster
spec:
  cephVersion:
    # The container image used to launch the Ceph daemon pods (mon, mgr, osd, mds, rgw).
    # v13 is mimic, v14 is nautilus, and v15 is octopus.
    # RECOMMENDATION: In production, use a specific version tag instead of the general v14 flag, which pulls the latest release and could result in different
    # versions running within the cluster. See tags available at https://hub.docker.com/r/ceph/ceph/tags/.
    # If you want to be more precise, you can always use a timestamp tag such ceph/ceph:v15.2.11-20200419
    # This tag might not contain a new Ceph version, just security fixes from the underlying operating system, which will reduce vulnerabilities
    image: ceph/ceph:v15.2.11
    # Whether to allow unsupported versions of Ceph. Currently `nautilus` and `octopus` are supported.
    # Future versions such as `pacific` would require this to be set to `true`.
    # Do not set to true in production.
    allowUnsupported: false
  # The path on the host where configuration files will be persisted. Must be specified.
  # Important: if you reinstall the cluster, make sure you delete this directory from each host or else the mons will fail to start on the new cluster.
  # In Minikube, the '/data' directory is configured to persist across reboots. Use "/data/rook" in Minikube environment.
  dataDirHostPath: /var/lib/rook
  # Whether or not upgrade should continue even if a check fails
  # This means Ceph's status could be degraded and we don't recommend upgrading but you might decide otherwise
  # Use at your OWN risk
  # To understand Rook's upgrade process of Ceph, read https://rook.io/docs/rook/master/ceph-upgrade.html#ceph-version-upgrades
  skipUpgradeChecks: false
  # Whether or not continue if PGs are not clean during an upgrade
  continueUpgradeAfterChecksEvenIfNotHealthy: false
  # WaitTimeoutForHealthyOSDInMinutes defines the time (in minutes) the operator would wait before an OSD can be stopped for upgrade or restart.
  # If the timeout exceeds and OSD is not ok to stop, then the operator would skip upgrade for the current OSD and proceed with the next one
  # if `continueUpgradeAfterChecksEvenIfNotHealthy` is `false`. If `continueUpgradeAfterChecksEvenIfNotHealthy` is `true`, then opertor would
  # continue with the upgrade of an OSD even if its not ok to stop after the timeout. This timeout won't be applied if `skipUpgradeChecks` is `true`.
  # The default wait timeout is 10 minutes.
  waitTimeoutForHealthyOSDInMinutes: 10
  mon:
    # Set the number of mons to be started. Must be an odd number, and is generally recommended to be 3.
    count: 3
    # The mons should be on unique nodes. For production, at least 3 nodes are recommended for this reason.
    # Mons should only be allowed on the same node for test environments where data loss is acceptable.
    allowMultiplePerNode: false
  mgr:
    modules:
    # Several modules should not need to be included in this list. The "dashboard" and "monitoring" modules
    # are already enabled by other settings in the cluster CR.
    - name: pg_autoscaler
      enabled: true
  # enable the ceph dashboard for viewing cluster status
  dashboard:
    enabled: true
    # serve the dashboard under a subpath (useful when you are accessing the dashboard via a reverse proxy)
    # urlPrefix: /ceph-dashboard
    # serve the dashboard at the given port.
    # port: 8443
    # serve the dashboard using SSL
    ssl: true
  # enable prometheus alerting for cluster
  monitoring:
    # requires Prometheus to be pre-installed
    enabled: false
    # namespace to deploy prometheusRule in. If empty, namespace of the cluster will be used.
    # Recommended:
    # If you have a single rook-ceph cluster, set the rulesNamespace to the same namespace as the cluster or keep it empty.
    # If you have multiple rook-ceph clusters in the same k8s cluster, choose the same namespace (ideally, namespace with prometheus
    # deployed) to set rulesNamespace for all the clusters. Otherwise, you will get duplicate alerts with multiple alert definitions.
    rulesNamespace: rook-ceph
  network:
    # enable host networking
    #provider: host
    # EXPERIMENTAL: enable the Multus network provider
    #provider: multus
    #selectors:
      # The selector keys are required to be `public` and `cluster`.
      # Based on the configuration, the operator will do the following:
      #   1. if only the `public` selector key is specified both public_network and cluster_network Ceph settings will listen on that interface
      #   2. if both `public` and `cluster` selector keys are specified the first one will point to 'public_network' flag and the second one to 'cluster_network'
      #
      # In order to work, each selector value must match a NetworkAttachmentDefinition object in Multus
      #
      #public: public-conf --> NetworkAttachmentDefinition object name in Multus
      #cluster: cluster-conf --> NetworkAttachmentDefinition object name in Multus
    # Provide internet protocol version. IPv6, IPv4 or empty string are valid options. Empty string would mean IPv4
    #ipFamily: "IPv6"
  # enable the crash collector for ceph daemon crash collection
  crashCollector:
    disable: false
  # enable log collector, daemons will log on files and rotate
  # logCollector:
  #   enabled: true
  #   periodicity: 24h # SUFFIX may be 'h' for hours or 'd' for days.
  # automate [data cleanup process](https://github.com/rook/rook/blob/master/Documentation/ceph-teardown.md#delete-the-data-on-hosts) in cluster destruction.
  cleanupPolicy:
    # Since cluster cleanup is destructive to data, confirmation is required.
    # To destroy all Rook data on hosts during uninstall, confirmation must be set to "yes-really-destroy-data".
    # This value should only be set when the cluster is about to be deleted. After the confirmation is set,
    # Rook will immediately stop configuring the cluster and only wait for the delete command.
    # If the empty string is set, Rook will not destroy any data on hosts during uninstall.
    confirmation: ""
    # sanitizeDisks represents settings for sanitizing OSD disks on cluster deletion
    sanitizeDisks:
      # method indicates if the entire disk should be sanitized or simply ceph's metadata
      # in both case, re-install is possible
      # possible choices are 'complete' or 'quick' (default)
      method: quick
      # dataSource indicate where to get random bytes from to write on the disk
      # possible choices are 'zero' (default) or 'random'
      # using random sources will consume entropy from the system and will take much more time then the zero source
      dataSource: zero
      # iteration overwrite N times instead of the default (1)
      # takes an integer value
      iteration: 1
    # allowUninstallWithVolumes defines how the uninstall should be performed
    # If set to true, cephCluster deletion does not wait for the PVs to be deleted.
    allowUninstallWithVolumes: false
  # To control where various services will be scheduled by kubernetes, use the placement configuration sections below.
  # The example under 'all' would have all services scheduled on kubernetes nodes labeled with 'role=storage-node' and
  # tolerate taints with a key of 'storage-node'.
#  placement:
#    all:
#      nodeAffinity:
#        requiredDuringSchedulingIgnoredDuringExecution:
#          nodeSelectorTerms:
#          - matchExpressions:
#            - key: role
#              operator: In
#              values:
#              - storage-node
#      podAffinity:
#      podAntiAffinity:
#      topologySpreadConstraints:
#      tolerations:
#      - key: storage-node
#        operator: Exists
# The above placement information can also be specified for mon, osd, and mgr components
#    mon:
# Monitor deployments may contain an anti-affinity rule for avoiding monitor
# collocation on the same node. This is a required rule when host network is used
# or when AllowMultiplePerNode is false. Otherwise this anti-affinity rule is a
# preferred rule with weight: 50.
#    osd:
#    mgr:
#    cleanup:
  annotations:
#    all:
#    mon:
#    osd:
#    cleanup:
#    prepareosd:
# If no mgr annotations are set, prometheus scrape annotations will be set by default.
#    mgr:
  labels:
#    all:
#    mon:
#    osd:
#    cleanup:
#    mgr:
#    prepareosd:
  resources:
# The requests and limits set here, allow the mgr pod to use half of one CPU core and 1 gigabyte of memory
#    mgr:
#      limits:
#        cpu: "500m"
#        memory: "1024Mi"
#      requests:
#        cpu: "500m"
#        memory: "1024Mi"
# The above example requests/limits can also be added to the mon and osd components
#    mon:
#    osd:
#    prepareosd:
#    crashcollector:
#    logcollector:
#    cleanup:
  # The option to automatically remove OSDs that are out and are safe to destroy.
  removeOSDsIfOutAndSafeToRemove: false
#  priorityClassNames:
#    all: rook-ceph-default-priority-class
#    mon: rook-ceph-mon-priority-class
#    osd: rook-ceph-osd-priority-class
#    mgr: rook-ceph-mgr-priority-class
  storage: # cluster level storage configuration and selection
    useAllNodes: true
    useAllDevices: true
    #deviceFilter:
    config:
      # crushRoot: "custom-root" # specify a non-default root label for the CRUSH map
      # metadataDevice: "md0" # specify a non-rotational storage so ceph-volume will use it as block db device of bluestore.
      # databaseSizeMB: "1024" # uncomment if the disks are smaller than 100 GB
      # journalSizeMB: "1024"  # uncomment if the disks are 20 GB or smaller
      # osdsPerDevice: "1" # this value can be overridden at the node or device level
      # encryptedDevice: "true" # the default value for this option is "false"
# Individual nodes and their config can be specified as well, but 'useAllNodes' above must be set to false. Then, only the named
# nodes below will be used as storage resources.  Each node's 'name' field should match their 'kubernetes.io/hostname' label.
#    nodes:
#    - name: "172.17.4.201"
#      devices: # specific devices to use for storage can be specified for each node
#      - name: "sdb"
#      - name: "nvme01" # multiple osds can be created on high performance devices
#        config:
#          osdsPerDevice: "5"
#      - name: "/dev/disk/by-id/ata-ST4000DM004-XXXX" # devices can be specified using full udev paths
#      config: # configuration can be specified at the node level which overrides the cluster level config
#        storeType: filestore
#    - name: "172.17.4.301"
#      deviceFilter: "^sd."
  # The section for configuring management of daemon disruptions during upgrade or fencing.
  disruptionManagement:
    # If true, the operator will create and manage PodDisruptionBudgets for OSD, Mon, RGW, and MDS daemons. OSD PDBs are managed dynamically
    # via the strategy outlined in the [design](https://github.com/rook/rook/blob/master/design/ceph/ceph-managed-disruptionbudgets.md). The operator will
    # block eviction of OSDs by default and unblock them safely when drains are detected.
    managePodBudgets: false
    # A duration in minutes that determines how long an entire failureDomain like `region/zone/host` will be held in `noout` (in addition to the
    # default DOWN/OUT interval) when it is draining. This is only relevant when  `managePodBudgets` is `true`. The default value is `30` minutes.
    osdMaintenanceTimeout: 30
    # A duration in minutes that the operator will wait for the placement groups to become healthy (active+clean) after a drain was completed and OSDs came back up.
    # Operator will continue with the next drain if the timeout exceeds. It only works if `managePodBudgets` is `true`.
    # No values or 0 means that the operator will wait until the placement groups are healthy before unblocking the next drain.
    pgHealthCheckTimeout: 0
    # If true, the operator will create and manage MachineDisruptionBudgets to ensure OSDs are only fenced when the cluster is healthy.
    # Only available on OpenShift.
    manageMachineDisruptionBudgets: false
    # Namespace in which to watch for the MachineDisruptionBudgets.
    machineDisruptionBudgetNamespace: openshift-machine-api

  # healthChecks
  # Valid values for daemons are 'mon', 'osd', 'status'
  healthCheck:
    daemonHealth:
      mon:
        disabled: false
        interval: 45s
      osd:
        disabled: false
        interval: 60s
      status:
        disabled: false
        interval: 60s
    # Change pod liveness probe, it works for all mon,mgr,osd daemons
    livenessProbe:
      mon:
        disabled: false
      mgr:
        disabled: false
      osd:
        disabled: false

我们在 node1、node2、node3 这3个节点上都一个大小为 100G 的 /dev/vdb2 设备,所以我们是手动指定 storage 的配置:

[root@node1 ~]# fdisk -l

Disk /dev/vda: 21.5 GB, 21474836480 bytes, 41943040 sectors
Units = sectors of 1 * 512 = 512 bytes
Sector size (logical/physical): 512 bytes / 512 bytes
I/O size (minimum/optimal): 512 bytes / 512 bytes
Disk label type: dos
Disk identifier: 0x00053d5b

   Device Boot      Start         End      Blocks   Id  System
/dev/vda1   *        2048     1026047      512000   83  Linux
/dev/vda2         1026048     5220351     2097152   82  Linux swap / Solaris
/dev/vda3         5220352    41943039    18361344   83  Linux

Disk /dev/vdb: 322.1 GB, 322122547200 bytes, 629145600 sectors
Units = sectors of 1 * 512 = 512 bytes
Sector size (logical/physical): 512 bytes / 512 bytes
I/O size (minimum/optimal): 512 bytes / 512 bytes
Disk label type: dos
Disk identifier: 0xf3f54157

   Device Boot      Start         End      Blocks   Id  System
/dev/vdb1            2048   419430399   209714176   83  Linux
/dev/vdb2       419430400   629145599   104857600   83  Linux

Disk /dev/sda: 10.7 GB, 10737418240 bytes, 20971520 sectors
Units = sectors of 1 * 512 = 512 bytes
Sector size (logical/physical): 512 bytes / 4096 bytes
I/O size (minimum/optimal): 4096 bytes / 4096 bytes

直接apply,Rook Operator 就会根据我们的描述信息去自动创建 Ceph 集群了。

验证

要验证集群是否处于正常状态,我们可以使用 Rook 工具箱 来运行 ceph status 命令查看。

Rook 工具箱是一个用于调试和测试 Rook 的常用工具容器,该工具基于 CentOS 镜像,所以可以使用 yum 来轻松安装更多的工具包。我们这里用 Deployment 控制器来部署 Rook 工具箱,部署的资源清单文件如下所示:(toolbox.yaml)

apiVersion: apps/v1
kind: Deployment
metadata:
  name: rook-ceph-tools
  namespace: rook-ceph
  labels:
    app: rook-ceph-tools
spec:
  replicas: 1
  selector:
    matchLabels:
      app: rook-ceph-tools
  template:
    metadata:
      labels:
        app: rook-ceph-tools
    spec:
      dnsPolicy: ClusterFirstWithHostNet
      containers:
      - name: rook-ceph-tools
        image: rook/ceph:v1.2.1
        command: ["/tini"]
        args: ["-g", "--", "/usr/local/bin/toolbox.sh"]
        imagePullPolicy: IfNotPresent
        env:
          - name: ROOK_ADMIN_SECRET
            valueFrom:
              secretKeyRef:
                name: rook-ceph-mon
                key: admin-secret
        securityContext:
          privileged: true
        volumeMounts:
          - mountPath: /dev
            name: dev
          - mountPath: /sys/bus
            name: sysbus
          - mountPath: /lib/modules
            name: libmodules
          - name: mon-endpoint-volume
            mountPath: /etc/rook
      # 如果设置 hostNetwork: false,  "rbd map" 命令会被 hang 住, 参考 https://github.com/rook/rook/issues/2021
      hostNetwork: true
      volumes:
        - name: dev
          hostPath:
            path: /dev
        - name: sysbus
          hostPath:
            path: /sys/bus
        - name: libmodules
          hostPath:
            path: /lib/modules
        - name: mon-endpoint-volume
          configMap:
            name: rook-ceph-mon-endpoints
            items:
            - key: data
              path: mon-endpoints

直接apply

一旦 toolbox 的 Pod 运行成功后,我们就可以使用下面的命令进入到工具箱内部进行操作:

$ kubectl -n rook-ceph exec -it $(kubectl -n rook-ceph get pod -l "app=rook-ceph-tools" -o jsonpath='{.items[0].metadata.name}') bash

工具箱中的所有可用工具命令均已准备就绪,可满足您的故障排除需求。例如:


ceph status
ceph osd status
ceph df
rados df

比如现在我们要查看集群的状态,需要满足下面的条件才认为是健康的:

所有 mons 应该达到法定数量
mgr 应该是激活状态
至少有一个 OSD 处于激活状态
如果不是 HEALTH_OK 状态,则应该查看告警或者错误信息

$ ceph status
ceph status
  cluster:
    id:     dae083e6-8487-447b-b6ae-9eb321818439
    health: HEALTH_OK

  services:
    mon: 3 daemons, quorum a,b,c (age 15m)
    mgr: a(active, since 2m)
    osd: 31 osds: 2 up (since 6m), 2 in (since 6m)

  data:
    pools:   0 pools, 0 pgs
    objects: 0 objects, 0 B
    usage:   79 GiB used, 314 GiB / 393 GiB avail
    pgs:

Dashboard

Ceph 有一个 Dashboard 工具,我们可以在上面查看集群的状态,包括总体运行状态,mgr、osd 和其他 Ceph 进程的状态,查看池和 PG 状态,以及显示守护进程的日志等等。

我们可以在上面的 cluster CRD 对象中开启 dashboard,设置dashboard.enable=true即可,这样 Rook Operator 就会启用 ceph-mgr dashboard 模块,并将创建一个 Kubernetes Service 来暴露该服务,将启用端口 7000 进行 https 访问,如果 Ceph 集群部署成功了,我们可以使用下面的命令来查看 Dashboard 的 Service:

$ kubectl get svc -n rook-ceph
NAME                         TYPE        CLUSTER-IP       EXTERNAL-IP   PORT(S)          AGE
rook-ceph-mgr              ClusterIP   10.99.87.1       <none>        9283/TCP            3m6s
rook-ceph-mgr-dashboard    ClusterIP   10.111.195.180   <none>        7000/TCP            3m29s

这里的 rook-ceph-mgr 服务用于报告 Prometheus metrics 指标数据的,而后面的的 rook-ceph-mgr-dashboard 服务就是我们的 Dashboard 服务,如果在集群内部我们可以通过 DNS 名称 http://rook-ceph-mgr-dashboard.rook-ceph:7000或者 CluterIP http://10.111.195.180:7000 来进行访问,但是如果要在集群外部进行访问的话,我们就需要通过 Ingress 或者 NodePort 类型的 Service 来暴露了,为了方便测试我们这里创建一个新的 NodePort 类型的服务来访问 Dashboard,资源清单如下所示:(dashboard-external.yaml)

apiVersion: v1
kind: Service
metadata:
  name: rook-ceph-mgr-dashboard-external
  namespace: rook-ceph
  labels:
    app: rook-ceph-mgr
    rook_cluster: rook-ceph
spec:
  ports:
  - name: dashboard
    port: 7000
    protocol: TCP
    targetPort: 7000
  selector:
    app: rook-ceph-mgr
    rook_cluster: rook-ceph
  type: NodePort

同样直接创建即可:

$ kubectl apply -f dashboard-external.yaml

创建完成后我们可以查看到新创建的 rook-ceph-mgr-dashboard-external 这个 Service 服务:

$ kubectl get svc -n rook-ceph 
NAME                                    TYPE        CLUSTER-IP       EXTERNAL-IP   PORT(S)          AGE
rook-ceph-mgr                           ClusterIP   10.96.49.29     <none>        9283/TCP            23m
rook-ceph-mgr-dashboard                 ClusterIP   10.109.8.98     <none>        7000/TCP            23m
rook-ceph-mgr-dashboard-external   NodePort    10.109.53.223    <none>        7000:31361/TCP      14s

现在我们需要通过 http://:31361 就可以访问到 Dashboard 了。
在这里插入图片描述
但是在访问的时候需要我们登录才能够访问,Rook 创建了一个默认的用户 admin,并在运行 Rook 的命名空间中生成了一个名为 rook-ceph-dashboard-admin-password 的 Secret,要获取密码,可以运行以下命令:

$ kubectl -n rook-ceph get secret rook-ceph-dashboard-password -o jsonpath="{['data']['password']}" | base64 --decode && echo
xxxx(登录密码)

用上面获得的密码和用户名 admin 就可以登录 Dashboard 了,在 Dashboard 上面可以查看到整个集群的状态:
在这里插入图片描述

使用

现在我们的 Ceph 集群搭建成功了,我们就可以来使用存储了。首先我们需要创建存储池,可以用 CRD 来定义 Pool。Rook 提供了两种机制来维持 OSD(又细化分出PG):

副本:缺省选项,每个对象都会根据 spec.replicated.size 在多个磁盘上进行复制。建议非生产环境至少 2 个副本,生产环境至少 3 个。
Erasure Code:是一种较为节约的方式。EC 把数据拆分 n 段(spec.erasureCoded.dataChunks),再加入 k 个代码段(spec.erasureCoded.codingChunks),用分布的方式把 n+k 段数据保存在磁盘上。这种情况下 Ceph 能够隔离 k 个 OSD 的损失。(erasure删除)

erasure code是分布式存储常用的方式,将原本的数据拆分成n份,加入k个数据(每份数据里面存储着easure编码),然后将这n+k份数据保存在分布吃存储系统上,特性是能够通过n+k份数据中的任意n份数据还原出原本的数据,可以看出它最多可以容许k个数据存储的损失,这样就可以将n+k份数据存放在不同的存储设备上(OSD),失效的数据只要小于等于k份,我们都能用剩下1数据块还原出原始数据

这里是RBD类型的存储池(不同类型的PG需要定义不同类型的存储池)(存储池属性更改直接该这个配置文件即可)

我们这里使用副本的方式,创建如下所示的 RBD 类型的存储池:(pool.yaml)


apiVersion: ceph.rook.io/v1
kind: CephBlockPool
metadata:
  name: k8s-test-pool   # operator会监听并创建一个pool,执行完后界面上也能看到对应的pool
  namespace: rook-ceph
spec:
  failureDomain: host  # 数据块的故障域: 值为host时,每个数据块将放置在不同的主机上(放在任意一个osd上即可);值为osd时,每个数据块将放置在不同的osd上(故障域,就是用来容灾的,防故障),这里分辨清楚host其实就是ceph集群的节点,每个host上可以有多个osd,host的容灾能力会强些,他可以容许某台主机整个的故障
  replicated:
    size: 3   # 池中数据的副本数,1就是不保存任何副本,这里就是保留到3个节点上共4个osd,也就是做了3个副本来备份数据

直接apply即可

存储池创建完成后我们在 Dashboard 上面的确可以看到新增了一个 pool,但是会发现集群健康状态变成了 WARN,我们可以查看到有如下日志出现(跟是用其他东西一样,有错误就差查志):

Health check update: too few PGs per OSD (6 < min 30) (TOO_FEW_PGS)

*这是因为每个 osd 上的 pg 数量小于最小的数目30个。pgs 为8,因为是3副本的配置,所以当有4个 osd 的时候,每个 osd 上均分了8/4 3=6个pgs,也就是出现了如上的错误小于最小配置30个,集群这种状态如果进行数据的存储和操作,集群会卡死,无法响应io,同时会导致大面积的 osd down。

我们可以进入 toolbox 的容器中查看上面存储的 pg 数量:
$ ceph osd pool get k8s-test-pool pg_num
pg_num: 8

我们可以通过增加 pg_num 来解决这个问题:


$ ceph osd pool set k8s-test-pool pg_num 64
set pool 1 pg_num to 64
$ ceph -s
  cluster:
    id:     7851387c-5d18-489a-8c04-b699fb9764c0
    health: HEALTH_OK

  services:
    mon: 3 daemons, quorum a,b,c (age 33m)
    mgr: a(active, since 32m)
    osd: 4 osds: 4 up (since 32m), 4 in (since 32m)

  data:
    pools:   1 pools, 64 pgs
    objects: 0 objects, 0 B
    usage:   182 GiB used, 605 GiB / 787 GiB avail
    pgs:     64 active+clean

这个时候我们再查看就可以看到现在就是健康状态了。不过需要注意的是我们这里的 pool 上没有数据,所以修改 pg 影响并不大,但是如果是生产环境重新修改 pg 数,会对生产环境产生较大影响。因为 pg 数变了,就会导致整个集群的数据重新均衡和迁移,数据越大响应 io 的时间会越长。所以,最好在一开始就设置好 pg 数

定义个storageClass使用ceph的块存储

现在我们来创建一个 StorageClass 来进行动态存储配置,如下所示我们定义一个 Ceph 的块存储的 StorageClass:(storageclass.yaml)


apiVersion: storage.k8s.io/v1
kind: StorageClass
metadata:
  name: rook-ceph-block
provisioner: rook-ceph.rbd.csi.ceph.com
parameters:
  # clusterID 是 rook 集群运行的命名空间
  clusterID: rook-ceph

  # 指定存储池
  pool: k8s-test-pool

  # RBD image (实际的存储介质) 格式. 默认为 "2".   format格式,块存储
  imageFormat: "2"

  # RBD image 特性. CSI RBD 现在只支持 `layering` .   feature特性   layer层,分层的
  imageFeatures: layering

  # Ceph 管理员认证信息,这些都是在 clusterID 命名空间下面自动生成的
  csi.storage.k8s.io/provisioner-secret-name: rook-csi-rbd-provisioner
  csi.storage.k8s.io/provisioner-secret-namespace: rook-ceph
  csi.storage.k8s.io/node-stage-secret-name: rook-csi-rbd-node
  csi.storage.k8s.io/node-stage-secret-namespace: rook-ceph
  # 指定 volume 的文件系统格式,如果不指定, csi-provisioner 会默认设置为 `ext4`
  csi.storage.k8s.io/fstype: ext4
# uncomment the following to use rbd-nbd as mounter on supported nodes
# **IMPORTANT**: If you are using rbd-nbd as the mounter, during upgrade you will be hit a ceph-csi
# issue that causes the mount to be disconnected. You will need to follow special upgrade steps
# to restart your application pods. Therefore, this option is not recommended.
#mounter: rbd-nbd
reclaimPolicy: Delete

直接apply

$ kubectl apply -f storageclass.yaml 
storageclass.storage.k8s.io/rook-ceph-block created
$ kubectl get storageclass
NAME              PROVISIONER                    AGE
rook-ceph-block   rook-ceph.rbd.csi.ceph.com     35s

然后创建一个 PVC 来使用上面的 StorageClass 对象:(pvc.yaml)

apiVersion: v1
kind: PersistentVolumeClaim
metadata:
  name: mysql-pv-claim
  labels:
    app: wordpress
spec:
  storageClassName: rook-ceph-block
  accessModes:
  - ReadWriteOnce
  resources:
    requests:
      storage: 20Gi

同样直接创建上面的 PVC 资源对象:

$ kubectl apply -f pvc.yaml 
persistentvolumeclaim/mysql-pv-claim created
$ kubectl get pvc -l app=wordpress
NAME             STATUS   VOLUME                                     CAPACITY   ACCESS MODES   STORAGECLASS      AGE
mysql-pv-claim   Bound    pvc-1eab82e3-d214-4d8e-8fcc-ed379c24e0e3   20Gi       RWO            rook-ceph-block   32m

创建完成后我们可以看到我们的 PVC 对象已经是 Bound 状态了,自动创建了对应的 PV(这里的storageClass使用provisioner是ceph,会动态分配pv,并且这里没有设置延迟绑定),然后我们就可以直接使用这个 PVC 对象来做数据持久化操作了。

这个时候可能集群还会出现如下的健康提示:

$ ceph health detail
HEALTH_WARN application not enabled on 1 pool(s)
POOL_APP_NOT_ENABLED application not enabled on 1 pool(s)
    application not enabled on pool 'k8s-test-pool'
    use 'ceph osd pool application enable <pool-name> <app-name>', where <app-name> is 'cephfs', 'rbd', 'rgw', or freeform for custom applications.
$ ceph osd pool application enable k8s-test-pool k8srbd
enabled application 'k8srbd' on pool 'k8s-test-pool'

根据提示启用一个 application 即可。

在上面的 cluster/examples/kubernetes 官方仓库目录下,官方给了个 wordpress(博客系统) 的例子,可以直接运行测试即可:

$ kubectl apply -f mysql.yaml
$ kubectl apply -f wordpress.yaml  

官方的这个示例里面的 wordpress 用的 Loadbalancer 类型,我们可以改成 NodePort:


$ kubectl get pvc -l app=wordpress
NAME             STATUS   VOLUME                                     CAPACITY   ACCESS MODES   STORAGECLASS      AGE
mysql-pv-claim   Bound    pvc-1eab82e3-d214-4d8e-8fcc-ed379c24e0e3   20Gi       RWO            rook-ceph-block   12h
wp-pv-claim      Bound    pvc-237932ed-5ca7-468c-bd16-220ebb2a1ce3   20Gi       RWO            rook-ceph-block   25s
$ kubectl get pods -l app=wordpress               
NAME                              READY   STATUS    RESTARTS   AGE
wordpress-5b886cf59b-4xwn8        1/1     Running   0          24m
wordpress-mysql-b9ddd6d4c-qhjd4   1/1     Running   0          24m
$ kubectl get svc -l app=wordpress
NAME              TYPE        CLUSTER-IP       EXTERNAL-IP   PORT(S)        AGE
wordpress         NodePort    10.106.253.225   <none>        80:30307/TCP   80s
wordpress-mysql   ClusterIP   None             <none>        3306/TCP       87s

当应用都处于 Running 状态后,我们可以通过 http://<任意节点IP>:30307 去访问 wordpress 应用:

在这里插入图片描述
比如我们在第一篇文章中更改下内容,然后我们将应用 Pod 全部删除重建:

$ kubectl delete pod wordpress-mysql-b9ddd6d4c-qhjd4 wordpress-5b886cf59b-4xwn8
pod "wordpress-mysql-b9ddd6d4c-qhjd4" deleted
pod "wordpress-5b886cf59b-4xwn8" deleted
$ kubectl get pods -l app=wordpress                                            
NAME                              READY   STATUS    RESTARTS   AGE
wordpress-5b886cf59b-kwxk4        1/1     Running   0          2m52s
wordpress-mysql-b9ddd6d4c-kkcr7   1/1     Running   0          2m52s

当 Pod 重建完成后再次访问 wordpress 应用的主页我们可以发现之前我们添加的数据仍然存在,这就证明我们的数据持久化是正确的。

不同类型存储的的使用案例

使用任意类型的ceph存储,首先创建相应的storageclass,再用pvc绑定storageClass,最后pod使用pvc即可

官网地址https://github.com/rook/rook/tree/release-1.5/cluster/examples/kubernetes/ceph/csi
你可以看不同rook版本的

RBD

rbd-storageclass.yaml

apiVersion: ceph.rook.io/v1
kind: CephBlockPool
metadata:
  name: replicapool
  namespace: rook-ceph
spec:
  failureDomain: host
  replicated:
    size: 3
    # Disallow setting pool with replica 1, this could lead to data loss without recovery.
    # Make sure you're *ABSOLUTELY CERTAIN* that is what you want
    requireSafeReplicaSize: true
    # gives a hint (%) to Ceph in terms of expected consumption of the total cluster capacity of a given pool
    # for more info: https://docs.ceph.com/docs/master/rados/operations/placement-groups/#specifying-expected-pool-size
    #targetSizeRatio: .5
---
apiVersion: storage.k8s.io/v1
kind: StorageClass
metadata:
   name: rook-ceph-block
# Change "rook-ceph" provisioner prefix to match the operator namespace if needed
provisioner: rook-ceph.rbd.csi.ceph.com
parameters:
    # clusterID is the namespace where the rook cluster is running
    # If you change this namespace, also change the namespace below where the secret namespaces are defined
    clusterID: rook-ceph # namespace:cluster

    # If you want to use erasure coded pool with RBD, you need to create
    # two pools. one erasure coded and one replicated.
    # You need to specify the replicated pool here in the `pool` parameter, it is
    # used for the metadata of the images.
    # The erasure coded pool must be set as the `dataPool` parameter below.
    #dataPool: ec-data-pool
    pool: replicapool

    # (optional) mapOptions is a comma-separated list of map options.
    # For krbd options refer
    # https://docs.ceph.com/docs/master/man/8/rbd/#kernel-rbd-krbd-options
    # For nbd options refer
    # https://docs.ceph.com/docs/master/man/8/rbd-nbd/#options
    # mapOptions: lock_on_read,queue_depth=1024

    # (optional) unmapOptions is a comma-separated list of unmap options.
    # For krbd options refer
    # https://docs.ceph.com/docs/master/man/8/rbd/#kernel-rbd-krbd-options
    # For nbd options refer
    # https://docs.ceph.com/docs/master/man/8/rbd-nbd/#options
    # unmapOptions: force

    # RBD image format. Defaults to "2".
    imageFormat: "2"

    # RBD image features. Available for imageFormat: "2". CSI RBD currently supports only `layering` feature.
    imageFeatures: layering

    # The secrets contain Ceph admin credentials. These are generated automatically by the operator
    # in the same namespace as the cluster.
    csi.storage.k8s.io/provisioner-secret-name: rook-csi-rbd-provisioner
    csi.storage.k8s.io/provisioner-secret-namespace: rook-ceph # namespace:cluster
    csi.storage.k8s.io/controller-expand-secret-name: rook-csi-rbd-provisioner
    csi.storage.k8s.io/controller-expand-secret-namespace: rook-ceph # namespace:cluster
    csi.storage.k8s.io/node-stage-secret-name: rook-csi-rbd-node
    csi.storage.k8s.io/node-stage-secret-namespace: rook-ceph # namespace:cluster
    # Specify the filesystem type of the volume. If not specified, csi-provisioner
    # will set default as `ext4`. Note that `xfs` is not recommended due to potential deadlock
    # in hyperconverged settings where the volume is mounted on the same node as the osds.
    csi.storage.k8s.io/fstype: ext4
# uncomment the following to use rbd-nbd as mounter on supported nodes
# **IMPORTANT**: If you are using rbd-nbd as the mounter, during upgrade you will be hit a ceph-csi
# issue that causes the mount to be disconnected. You will need to follow special upgrade steps
# to restart your application pods. Therefore, this option is not recommended.
#mounter: rbd-nbd
allowVolumeExpansion: true
reclaimPolicy: Delete

cephFS

https://github.com/rook/rook/blob/release-1.5/cluster/examples/kubernetes/ceph/filesystem.yaml
cephFS类型的存储服务可能需要元数据服务,这里部署元数据存储池(可选)
filesystem.yaml:

#################################################################################################################
# Create a filesystem with settings with replication enabled for a production environment.
# A minimum of 3 OSDs on different nodes are required in this example.
#  kubectl create -f filesystem.yaml
#################################################################################################################

apiVersion: ceph.rook.io/v1
kind: CephFilesystem
metadata:
  name: myfs
  namespace: rook-ceph # namespace:cluster
spec:
  # The metadata pool spec. Must use replication.
  metadataPool:
    replicated:
      size: 3
      requireSafeReplicaSize: true
    parameters:
      # Inline compression mode for the data pool
      # Further reference: https://docs.ceph.com/docs/nautilus/rados/configuration/bluestore-config-ref/#inline-compression
      compression_mode: none
        # gives a hint (%) to Ceph in terms of expected consumption of the total cluster capacity of a given pool
      # for more info: https://docs.ceph.com/docs/master/rados/operations/placement-groups/#specifying-expected-pool-size
      #target_size_ratio: ".5"
  # The list of data pool specs. Can use replication or erasure coding.
  dataPools:
    - failureDomain: host
      replicated:
        size: 3
        # Disallow setting pool with replica 1, this could lead to data loss without recovery.
        # Make sure you're *ABSOLUTELY CERTAIN* that is what you want
        requireSafeReplicaSize: true
      parameters:
        # Inline compression mode for the data pool
        # Further reference: https://docs.ceph.com/docs/nautilus/rados/configuration/bluestore-config-ref/#inline-compression
        compression_mode: none
          # gives a hint (%) to Ceph in terms of expected consumption of the total cluster capacity of a given pool
        # for more info: https://docs.ceph.com/docs/master/rados/operations/placement-groups/#specifying-expected-pool-size
        #target_size_ratio: ".5"
  # Whether to preserve filesystem after CephFilesystem CRD deletion
  preserveFilesystemOnDelete: true
  # The metadata service (mds) configuration
  metadataServer:
    # The number of active MDS instances
    activeCount: 1
    # Whether each active MDS instance will have an active standby with a warm metadata cache for faster failover.
    # If false, standbys will be available, but will not have a warm cache.
    activeStandby: true
    # The affinity rules to apply to the mds deployment
    placement:
    #  nodeAffinity:
    #    requiredDuringSchedulingIgnoredDuringExecution:
    #      nodeSelectorTerms:
    #      - matchExpressions:
    #        - key: role
    #          operator: In
    #          values:
    #          - mds-node
    #  topologySpreadConstraints:
    #  tolerations:
    #  - key: mds-node
    #    operator: Exists
    #  podAffinity:
       podAntiAffinity:
          requiredDuringSchedulingIgnoredDuringExecution:
          - labelSelector:
              matchExpressions:
              - key: app
                operator: In
                values:
                - rook-ceph-mds
            # topologyKey: kubernetes.io/hostname will place MDS across different hosts
            topologyKey: kubernetes.io/hostname
          preferredDuringSchedulingIgnoredDuringExecution:
          - weight: 100
            podAffinityTerm:
              labelSelector:
                matchExpressions:
                - key: app
                  operator: In
                  values:
                  - rook-ceph-mds
              # topologyKey: */zone can be used to spread MDS across different AZ
              # Use <topologyKey: failure-domain.beta.kubernetes.io/zone> in k8s cluster if your cluster is v1.16 or lower
              # Use <topologyKey: topology.kubernetes.io/zone>  in k8s cluster is v1.17 or upper
              topologyKey: topology.kubernetes.io/zone
    # A key/value list of annotations
    annotations:
    #  key: value
    # A key/value list of labels
    labels:
    #  key: value
    resources:
    # The requests and limits set here, allow the filesystem MDS Pod(s) to use half of one CPU core and 1 gigabyte of memory
    #  limits:
    #    cpu: "500m"
    #    memory: "1024Mi"
    #  requests:
    #    cpu: "500m"
    #    memory: "1024Mi"
    # priorityClassName: my-priority-class


ceph-storageclass.yaml:

apiVersion: storage.k8s.io/v1
kind: StorageClass
metadata:
  name: rook-cephfs
provisioner: rook-ceph.cephfs.csi.ceph.com # driver:namespace:operator
parameters:
  # clusterID is the namespace where operator is deployed.
  clusterID: rook-ceph # namespace:cluster

  # CephFS filesystem name into which the volume shall be created
  fsName: myfs

  # Ceph pool into which the volume shall be created
  # Required for provisionVolume: "true"
  pool: myfs-data0

  # The secrets contain Ceph admin credentials. These are generated automatically by the operator
  # in the same namespace as the cluster.
  csi.storage.k8s.io/provisioner-secret-name: rook-csi-cephfs-provisioner
  csi.storage.k8s.io/provisioner-secret-namespace: rook-ceph # namespace:cluster
  csi.storage.k8s.io/controller-expand-secret-name: rook-csi-cephfs-provisioner
  csi.storage.k8s.io/controller-expand-secret-namespace: rook-ceph # namespace:cluster
  csi.storage.k8s.io/node-stage-secret-name: rook-csi-cephfs-node
  csi.storage.k8s.io/node-stage-secret-namespace: rook-ceph # namespace:cluster

  # (optional) The driver can use either ceph-fuse (fuse) or ceph kernel client (kernel)
  # If omitted, default volume mounter will be used - this is determined by probing for ceph-fuse
  # or by setting the default mounter explicitly via --volumemounter command-line argument.
  # mounter: kernel
reclaimPolicy: Delete
allowVolumeExpansion: true
mountOptions:
  # uncomment the following line for debugging
  #- debug

查看元数据服务MDS
kubectl get pod -l app=rook-ceph-mds -n rook-ceph

查看storageclass
kubectl get storageclass
kubectl get stotrageclasses.storage.k8s.io

创建pvc绑定storageClass即可
注意rbd只支持RWX,cephFS还可以支持RWM(文件系统类型的存储比如nfs也是,一般都支持多个共享挂载

rook-ceph开启S3 API接口

创建过程:
https://github.com/rook/rook/tree/release-1.5/cluster/examples/kubernetes/ceph下的object.yaml文件
创建对象存储,Rook操作员将创建启动服务所需的所有池和其他资源。这可能需要三四分钟才能完成
kubectl create -f object.yaml 
kubectl -n rook-ceph get pod -l app=rook-ceph-rgw
 
创建存储桶,客户端可以在其中读取和写入对象。可以通过定义存储类来创建存储桶,类似于块存储和文件存储所使用的模式。首先,定义允许对象客户端创建存储桶的存储类。存储类定义对象存储系统,存储桶保留策略以及管理员所需的其他属性
kubectl create -f storageclass-bucket-delete.yaml 
 
创建申请声明。基于此存储类,对象客户端现在可以通过创建对象存储桶声明(OBC)来请求存储桶。创建OBC后,Rook-Ceph存储桶配置程序将创建一个新存储桶。请注意,OBC引用了上面创建的存储类。
kubectl create -f object-bucket-claim-delete.yaml 
 
创建集群外部访问
Rook设置了对象存储,因此Pod可以访问群集内部。如果您的应用程序在集群外部运行,则需要通过来设置外部服务NodePort。
首先,请注意将RGW公开到群集内部的服务。我们将保留该服务不变,并为外部访问创建一个新服务。
创建之前修改rgw-external.yaml,配置一个固定的nodeport端口例如38000
kubectl create -f rgw-external.yaml 
 
创建一个用户 
如果您需要创建一组独立的用户凭据来访问S3端点,请创建一个CephObjectStoreUser。该用户将使用S3 API连接到集群中的RGW服务。用户将独立于您可能在本文档前面的说明中创建的任何对象存储桶声明。
 
kubectl create -f object-user.yaml 
 
获取用户的AccessKey和SecretKey
这里建议先print 打印出来,之后再echo "xxxx"  | base64 --decode
kubectl -n rook-ceph get secret rook-ceph-object-user-my-store-my-user -o yaml | grep AccessKey | awk '{print $2}' | base64 --decode 
kubectl -n rook-ceph get secret rook-ceph-object-user-my-store-my-user -o yaml | grep SecretKey | awk '{print $2}' | base64 --decode
 
最终提供访问的地址及认证信息
 
例如:
access_key = 'BMXG3WP8JA9D1GSD2AJJ'
secret_key = 'vl32x2t0sBxy0BEgcY9Iz442HK2HobPTNw4T99yK'
host = '192.168.10.237:38000'


创建RGW完成
附加上相关的官方完整文件

object.yaml:

#################################################################################################################
# Create an object store with settings for replication in a production environment. A minimum of 3 hosts with
# OSDs are required in this example.
#  kubectl create -f object.yaml
#################################################################################################################

apiVersion: ceph.rook.io/v1
kind: CephObjectStore
metadata:
  name: my-store
  namespace: rook-ceph # namespace:cluster
spec:
  # The pool spec used to create the metadata pools. Must use replication.
  metadataPool:
    failureDomain: host
    replicated:
      size: 3
      # Disallow setting pool with replica 1, this could lead to data loss without recovery.
      # Make sure you're *ABSOLUTELY CERTAIN* that is what you want
      requireSafeReplicaSize: true
    parameters:
      # Inline compression mode for the data pool
      # Further reference: https://docs.ceph.com/docs/nautilus/rados/configuration/bluestore-config-ref/#inline-compression
      compression_mode: none
      # gives a hint (%) to Ceph in terms of expected consumption of the total cluster capacity of a given pool
      # for more info: https://docs.ceph.com/docs/master/rados/operations/placement-groups/#specifying-expected-pool-size
      #target_size_ratio: ".5"
  # The pool spec used to create the data pool. Can use replication or erasure coding.
  dataPool:
    failureDomain: host
    replicated:
      size: 3
      # Disallow setting pool with replica 1, this could lead to data loss without recovery.
      # Make sure you're *ABSOLUTELY CERTAIN* that is what you want
      requireSafeReplicaSize: true
    parameters:
      # Inline compression mode for the data pool
      # Further reference: https://docs.ceph.com/docs/nautilus/rados/configuration/bluestore-config-ref/#inline-compression
      compression_mode: none
      # gives a hint (%) to Ceph in terms of expected consumption of the total cluster capacity of a given pool
      # for more info: https://docs.ceph.com/docs/master/rados/operations/placement-groups/#specifying-expected-pool-size
      #target_size_ratio: ".5"
  # Whether to preserve metadata and data pools on object store deletion
  preservePoolsOnDelete: false
  # The gateway service configuration
  gateway:
    # type of the gateway (s3)
    type: s3
    # A reference to the secret in the rook namespace where the ssl certificate is stored
    sslCertificateRef:
    # The port that RGW pods will listen on (http)
    port: 80
    # The port that RGW pods will listen on (https). An ssl certificate is required.
    # securePort: 443
    # The number of pods in the rgw deployment
    instances: 1
    # The affinity rules to apply to the rgw deployment or daemonset.
    placement:
    #  nodeAffinity:
    #    requiredDuringSchedulingIgnoredDuringExecution:
    #      nodeSelectorTerms:
    #      - matchExpressions:
    #        - key: role
    #          operator: In
    #          values:
    #          - rgw-node
    #  topologySpreadConstraints:
    #  tolerations:
    #  - key: rgw-node
    #    operator: Exists
    #  podAffinity:
    #  podAntiAffinity:
    # A key/value list of annotations
    annotations:
    #  key: value
    # A key/value list of labels
    labels:
    #  key: value
    resources:
    # The requests and limits set here, allow the object store gateway Pod(s) to use half of one CPU core and 1 gigabyte of memory
    #  limits:
    #    cpu: "500m"
    #    memory: "1024Mi"
    #  requests:
    #    cpu: "500m"
    #    memory: "1024Mi"
    # priorityClassName: my-priority-class
  #zone:
    #name: zone-a
  # service endpoint healthcheck
  healthCheck:
    bucket:
      disabled: false
      interval: 60s
    # Configure the pod liveness probe for the rgw daemon
    livenessProbe:
      disabled: false

storageclass-bucket-delete.yaml:

apiVersion: storage.k8s.io/v1
kind: StorageClass
metadata:
   name: rook-ceph-delete-bucket
provisioner: rook-ceph.ceph.rook.io/bucket # driver:namespace:cluster
# set the reclaim policy to delete the bucket and all objects
# when its OBC is deleted.
reclaimPolicy: Delete
parameters:
  objectStoreName: my-store
  objectStoreNamespace: rook-ceph # namespace:cluster
  region: us-east-1
  # To accommodate brownfield cases reference the existing bucket name here instead
  # of in the ObjectBucketClaim (OBC). In this case the provisioner will grant
  # access to the bucket by creating a new user, attaching it to the bucket, and
  # providing the credentials via a Secret in the namespace of the requesting OBC.
  #bucketName:

object-bucket-claim-delete.yaml:

apiVersion: objectbucket.io/v1alpha1
kind: ObjectBucketClaim
metadata:
  name: ceph-delete-bucket
spec:
  # To create a new bucket specify either `bucketName` or 
  # `generateBucketName` here. Both cannot be used. To access
  # an existing bucket the bucket name needs to be defined in
  # the StorageClass referenced here, and both `bucketName` and
  # `generateBucketName` must be omitted in the OBC.
  #bucketName: 
  generateBucketName: ceph-bkt
  storageClassName: rook-ceph-delete-bucket
  additionalConfig:
    # To set for quota for OBC
    #maxObjects: "1000"
    #maxSize: "2G"

rgw-external.yaml:

apiVersion: v1
kind: Service
metadata:
  name: rook-ceph-rgw-my-store-external
  namespace: rook-ceph # namespace:cluster
  labels:
    app: rook-ceph-rgw
    rook_cluster: rook-ceph # namespace:cluster
    rook_object_store: my-store
spec:
  ports:
  - name: rgw
    port: 80 # service port mentioned in object store crd
    protocol: TCP
    targetPort: 8080
  selector:
    app: rook-ceph-rgw
    rook_cluster: rook-ceph # namespace:cluster
    rook_object_store: my-store
  sessionAffinity: None
  type: NodePort

object-user.yaml:

#################################################################################################################
# Create an object store user for access to the s3 endpoint.
#  kubectl create -f object-user.yaml
#################################################################################################################

apiVersion: ceph.rook.io/v1
kind: CephObjectStoreUser
metadata:
  name: my-user
  namespace: rook-ceph # namespace:cluster
spec:
  store: my-store
  displayName: "my display name"

ceph基础命令

进入rook-ceph命令行工具pod
 
kubectl  exec -it rook-ceph-tools-6bdcd78654-tvl5j  bash -n rook-ceph
 
检查Ceph集群状态
ceph -s
 
如果需要实时观察Ceph集群状态变化,可使用如下命令
ceph -w 
 
检查集群容量使用情况
ceph df
 
查看集群OSD配置
ceph osd df
 
查看OSD在集群布局中的设计分布
ceph osd tree
 
列式pool列表
ceph osd lspools

ceph 历史报错回收

使用ceph -s查看集群状态,发现一直有如下报错,且数量一直在增加

经查当前系统运行状态正常,判断这里显示的应该是历史故障,处理方式如下:

查看历史crash
ceph crash ls-new
 
根据ls出来的id查看详细信息
ceph crash info <crash-id>
 
将历史crash信息进行归档,即不再显示
ceph crash archive <crash-id>
 
归档所有信息
ceph crash archive-all

故障修复参考

1
rook-ceph-crashcollector-k8s-master3-offline-217-d9ff847442ng7p  一直处于init状态,输入命令查看pod启动状态 kubectl  describe  pod rook-ceph-crashcollector-k8s-master3-offline-217-d9ff847442ng7p -n rook-ceph报错信息如下:
MountVolume.SetUp failed for volume "rook-ceph-crash-collector-keyring" : secret "rook-ceph-crash-collector-keyring" not foun
修复过程:
删除集群 
 kubectl  delete -f cluster.yaml
之后会一直卡在删除的阶段。
kubectl  edit customresourcedefinitions.apiextensions.k8s.io cephclusters.ceph.rook.io 
删除文件中状态两行
 
每个节点都需要执行
rm -rf /var/lib/rook/*
rm -rf /var/lib/kubelet/plugins/rook-ceph.*
rm -rf /var/lib/kubelet/plugins_registry/rook-ceph.*
 
重新安装,再次查看
kubectl create -f common.yaml
 
kubectl create -f operator.yaml
 
kubectl create -f cluster.yaml

2.现象:rook-ceph部分容器为一直创建的状态
 
排查过程:查看pod状态后发现缺少rook-ceph-csi-config文件,github相关资料https://github.com/rook/rook/issues/6162。大概原因是由于服务器重启或者是服务器发生抖动,导致pod飘逸重建。重建失败
 
解决方法:
 
kubectl delete -f rook/cluster/examples/kubernetes/ceph/operator.yaml 
 
kubectl apply -f rook/cluster/examples/kubernetes/ceph/common.yaml
 
kubectl apply -f rook/cluster/examples/kubernetes/ceph/operator.yaml
 
3.清理ceph集群
 
每个节点都需要执行
rm -rf /var/lib/rook/*
rm -rf /var/lib/kubelet/plugins/rook-ceph.*
rm -rf /var/lib/kubelet/plugins_registry/rook-ceph.*
https://rook.io/docs/rook/v1.4/ceph-teardown.html

rook-ceph升级

步骤很简单只需要导入两个镜像,之后重新apply operator.yaml即可,rook会自动检查并升级重启
 
docker load -i cephcsi.tar
docker load -i rook-ceph.tar
kubectl apply -f operator.yaml

存储原理

前面的章节中我们介绍了在 Kubernetes 中的持久化存储的使用,了解了 PV、PVC 以及 StorageClass 的使用方法,从本地存储到 Ceph 共享存储都有学习,到这里我们其实已经可以完成应用各种场景的数据持久化了,但是难免在实际的使用过程中会遇到各种各样的问题,要解决这些问题最好的方式就是来了解下 Kubernetes 中存储的实现原理。

Kubernetes 默认情况下就提供了主流的存储卷接入方案,我们可以执行命令 kubectl explain pod.spec.volumes 查看到支持的各种存储卷,另外也提供了插件机制,允许其他类型的存储服务接入到 Kubernetes 系统中来,在 Kubernetes 中就对应 In-Tree 和 Out-Of-Tree 两种方式,In-Tree 就是在 Kubernetes 源码内部实现的,和 Kubernetes 一起发布、管理的,但是更新迭代慢、灵活性比较差,Out-Of-Tree 是独立于 Kubernetes 的,目前主要有 CSI 和 FlexVolume 两种机制,开发者可以根据自己的存储类型实现不同的存储插件接入到 Kubernetes 中去,其中 CSI 是现在也是以后主流的方式,所以当然我们的重点也会是 CSI 的使用介绍。
(flex屈伸)

NFS

NFS服务器搭建配置笔记

NAS存储,NFS服务器(笔记个人用)

NFS案例

我们这里为了演示方便,先使用相对简单的 NFS 这种存储资源,接下来我们在节点 10.151.30.11 上来安装 NFS 服务,数据目录:/data/k8s/

关闭防火墙


$ systemctl stop firewalld.service
$ systemctl disable firewalld.service
安装配置 nfs


$ yum -y install nfs-utils rpcbind
共享目录设置权限:


$ mkdir -p /data/k8s/
$ chmod 755 /data/k8s/
配置 nfs,nfs 的默认配置文件在 /etc/exports 文件下,在该文件中添加下面的配置信息:


$ vi /etc/exports
/data/k8s  *(rw,sync,no_root_squash)


配置说明:

/data/k8s:是共享的数据目录
*:表示任何人都有权限连接,当然也可以是一个网段,一个 IP,也可以是域名
rw:读写的权限
sync:表示文件同时写入硬盘和内存
no_root_squash:当登录 NFS 主机使用共享目录的使用者是 root 时,其权限将被转换成为匿名使用者,通常它的 UID 与 GID,都会变成 nobody 身份

启动服务 nfs 需要向 rpc 注册,rpc 一旦重启了,注册的文件都会丢失,向他注册的服务都需要重启 注意启动顺序,先启动 rpcbind


$ systemctl start rpcbind.service
$ systemctl enable rpcbind
$ systemctl status rpcbind
● rpcbind.service - RPC bind service
   Loaded: loaded (/usr/lib/systemd/system/rpcbind.service; disabled; vendor preset: enabled)
   Active: active (running) since Tue 2018-07-10 20:57:29 CST; 1min 54s ago
  Process: 17696 ExecStart=/sbin/rpcbind -w $RPCBIND_ARGS (code=exited, status=0/SUCCESS)
 Main PID: 17697 (rpcbind)
    Tasks: 1
   Memory: 1.1M
   CGroup: /system.slice/rpcbind.service
           └─17697 /sbin/rpcbind -w

Jul 10 20:57:29 master systemd[1]: Starting RPC bind service...
Jul 10 20:57:29 master systemd[1]: Started RPC bind service.
看到上面的 Started 证明启动成功了。

然后启动 nfs 服务:


$ systemctl start nfs.service
$ systemctl enable nfs
$ systemctl status nfs
● nfs-server.service - NFS server and services
   Loaded: loaded (/usr/lib/systemd/system/nfs-server.service; enabled; vendor preset: disabled)
  Drop-In: /run/systemd/generator/nfs-server.service.d
           └─order-with-mounts.conf
   Active: active (exited) since Tue 2018-07-10 21:35:37 CST; 14s ago
 Main PID: 32067 (code=exited, status=0/SUCCESS)
   CGroup: /system.slice/nfs-server.service

Jul 10 21:35:37 master systemd[1]: Starting NFS server and services...
Jul 10 21:35:37 master systemd[1]: Started NFS server and services.
同样看到 Started 则证明 NFS Server 启动成功了。

另外我们还可以通过下面的命令确认下:


$ rpcinfo -p|grep nfs
    100003    3   tcp   2049  nfs
    100003    4   tcp   2049  nfs
    100227    3   tcp   2049  nfs_acl
    100003    3   udp   2049  nfs
    100003    4   udp   2049  nfs
    100227    3   udp   2049  nfs_acl
查看具体目录挂载权限:


$ cat /var/lib/nfs/etab
/data/k8s    *(rw,sync,wdelay,hide,nocrossmnt,secure,no_root_squash,no_all_squash,no_subtree_check,secure_locks,acl,no_pnfs,anonuid=65534,anongid=65534,sec=sys,secure,no_root_squash,no_all_squash)
到这里我们就把 nfs server 给安装成功了,然后就是前往节点安装 nfs 的客户端来验证,安装 nfs 当前也需要先关闭防火墙:


$ systemctl stop firewalld.service
$ systemctl disable firewalld.service
然后安装 nfs


$ yum -y install nfs-utils rpcbind
安装完成后,和上面的方法一样,先启动 rpc、然后启动 nfs:


$ systemctl start rpcbind.service 
$ systemctl enable rpcbind.service 
$ systemctl start nfs.service    
$ systemctl enable nfs.service
挂载数据目录 客户端启动完成后,我们在客户端来挂载下 nfs 测试下,首先检查下 nfs 是否有共享目录:


$ showmount -e 10.151.30.11
Export list for 10.151.30.11:
/data/k8s *
然后我们在客户端上新建目录:


$ mkdir -p /root/course/kubeadm/data
将 nfs 共享目录挂载到上面的目录:


$ mount -t nfs 10.151.30.11:/data/k8s /root/course/kubeadm/data
挂载成功后,在客户端上面的目录中新建一个文件,然后我们观察下 nfs 服务端的共享目录下面是否也会出现该文件:


$ touch /root/course/kubeadm/data/test.txt
然后在 nfs 服务端查看:


$ ls -ls /data/k8s/
total 4
4 -rw-r--r--. 1 root root 4 Jul 10 21:50 test.txt
如果上面出现了 test.txt 的文件,那么证明我们的 nfs 挂载成功了。

nfs很简单,个人看来,说nfs是存储系统不够直接明确,应该直接把它看成一个共享的文件设备,毕竟名称就是netqork file system

存储架构

前面我们了解到了 PV、PVC、StorgeClass 的使用,但是他们是如何和我们的 Pod 关联起来使用的呢?这就需要从 Volume 的处理流程和原理说起了。

我们创建了一个 nfs 类型的 PV 资源对象

apiVersion: v1
kind: PersistentVolume
metadata:
  name: nfs-pv
spec:
  storageClassName: manual
  capacity: 
    storage: 1Gi
  accessModes:
  - ReadWriteOnce
  persistentVolumeReclaimPolicy: Retain
  nfs:   #hostPath,local,nfs
    path: /data/k8s  # 指定nfs的挂载点
    server: 10.151.30.11  # 指定nfs服务地址
---
apiVersion: v1
kind: PersistentVolumeClaim
metadata:
  name: nfs-pvc
spec:
  storageClassName: manual
  accessModes:
  - ReadWriteOnce
  resources:
    requests:
      storage: 1Gi

我们知道用户真正使用的是 PVC,而要使用 PVC 的前提就是必须要先和某个符合条件的 PV 进行一一绑定,比如存储容器、访问模式,以及 PV 和 PVC 的 storageClassName 字段必须一样,这样才能够进行绑定,当 PVC 和 PV 绑定成功后就可以直接使用这个 PVC 对象了:(pod.yaml)

apiVersion: v1
kind: Pod
metadata:
  name: test-volumes
spec:
  volumes:
  - name: nfs
    persistentVolumeClaim:
      claimName: nfs-pvc
  containers:
  - name: web
    image: nginx
    ports:
    - name: web
      containerPort: 80
    volumeMounts:
    - name: nfs
      subPath: test-volumes
      mountPath: "/usr/share/nginx/html"

我们只是在 volumes 中指定了我们上面创建的 PVC 对象,当这个 Pod 被创建之后, kubelet 就会把这个 PVC 对应的这个 NFS 类型的 Volume(PV)挂载到这个 Pod 容器中的目录中去。前面我们也提到了这样的话对于普通用户来说完全就不用关心后面的具体存储在 NFS 还是 Ceph 或者其他了,只需要直接使用 PVC 就可以了,因为真正的存储是需要很多相关的专业知识的,这样就完全职责分离解耦了。

普通用户直接使用 PVC 没有问题,但是也会出现一个问题,那就是当普通用户创建一个 PVC 对象的时候,这个时候系统里面并没有合适的 PV 来和它进行绑定,因为 PV 大多数情况下是管理员给我们创建的,这个时候启动 Pod 肯定就会失败了,如果现在管理员如果去创建一个对应的 PV 的话,PVC 和 PV 当然就可以绑定了,然后 Pod 也会自动的启动成功,这是因为在 Kubernetes 中有一个专门处理持久化存储的控制器 Volume Controller,这个控制器下面有很多个控制循环,其中一个就是用于 PV 和 PVC 绑定的 PersistentVolumeController

PersistentVolumeController 会不断地循环去查看每一个 PVC,是不是已经处于 Bound(已绑定)状态。如果不是,那它就会遍历所有的、可用的 PV,并尝试将其与未绑定的 PVC 进行绑定,这样,Kubernetes 就可以保证用户提交的每一个 PVC,只要有合适的 PV 出现,它就能够很快进入绑定状态。而所谓将一个 PV 与 PVC 进行“绑定”,其实就是将这个 PV 对象的名字,填在了 PVC 对象的 spec.volumeName 字段上

PV 和 PVC 绑定上了,那么又是如何将容器里面的数据进行持久化的呢,前面我们学习过 Docker 的 Volume 挂载,其实就是将一个宿主机上的目录和一个容器里的目录绑定挂载在了一起,具有持久化功能当然就是指的宿主机上面的这个目录了,当容器被删除或者在其他节点上重建出来以后,这个目录里面的内容依然存在,所以一般情况下实现持久化是需要一个远程存储的,比如 NFS、Ceph 或者云厂商提供的磁盘等等。所以接下来需要做的就是持久化宿主机目录这个过程。

当 Pod 被调度到一个节点上后,节点上的 kubelet 组件就会为这个 Pod 创建它的 Volume 目录,默认情况下 kubelet 为 Volume 创建的目录在 kubelet 工作目录下面:

/var/lib/kubelet/pods/<Pod的ID>/volumes/kubernetes.io~<Volume类型>/<Volume名字>

比如上面我们创建的 Pod 对应的 Volume 目录完整路径为:

/var/lib/kubelet/pods/d4fcdb11-baf7-43d9-8d7d-3ede24118e08/volumes/kubernetes.io~nfs/nfs-pv

要获取 Pod 的唯一标识 uid,可通过命令 kubectl get pod pod名 -o jsonpath={.metadata.uid} 获取。

然后就需要根据我们的 Volume 类型来决定需要做什么操作了

比如上节课我们用的 Ceph RBD,那么 kubelet 就需要先将 Ceph 提供的 RBD 挂载到 Pod 所在的宿主机上面,这个阶段在 Kubernetes 中被称为 Attach 阶段。Attach 阶段完成后,为了能够使用这个块设备,kubelet 还要进行第二个操作,即:格式化这个块设备,然后将它挂载到宿主机指定的挂载点上。这个挂载点,也就是上面我们提到的 Volume 的宿主机的目录。将块设备格式化并挂载到 Volume 宿主机目录的操作,在 Kubernetes 中被称为 Mount 阶段。(就像我们之前在linux主机下加多块盘,或者你分区出来的分区,但这之后的盘或者分区不能进行数据存储,要格式化成文件系统,然后将该文件系统挂载到主机的某个目录下)

上节课我们使用 Ceph RBD 持久化的 Wordpress 的 MySQL 数据,我们可以查看对应的 Volume 信息:

$ kubectl get pods -o wide -l app=wordpress
NAME                              READY   STATUS    RESTARTS   AGE   IP             NODE         NOMINATED NODE   READINESS GATES
wordpress-5b886cf59b-dv2zt        1/1     Running   0          20d   10.244.1.158   ydzs-node1   <none>           <none>
wordpress-mysql-b9ddd6d4c-pjhbt   1/1     Running   0          20d   10.244.4.70    ydzs-node4   <none>           <none>

我们可以看到 MySQL 运行在 node4 节点上,然后可以在该节点上查看 Volume 信息,Pod 对应的 uid 可以通过如下命令获取:
$ kubectl get pod wordpress-mysql-b9ddd6d4c-pjhbt -o jsonpath={
    
    .metadata.uid}
3f84af87-9f58-4c69-9e38-5ef234498133
$ ls /var/lib/kubelet/pods/3f84af87-9f58-4c69-9e38-5ef234498133/volumes/kubernetes.io~csi/pvc-c8861c23-c03d-47aa-96f6-73c4d4093109/
mount  vol_data.json

然后通过如下命令可以查看 Volume 的持久化信息:
(volume类型时csi,开发者使用这个csi的插件机制将存储插件加入kubernetes中)
$ findmnt /var/lib/kubelet/pods/3f84af87-9f58-4c69-9e38-5ef234498133/volumes/kubernetes.io~csi/pvc-c8861c23-c03d-47aa-96f6-73c4d4093109/mount
TARGET                                                                                            SOURCE    FSTYPE OPTIONS
/var/lib/kubelet/pods/3f84af87-9f58-4c69-9e38-5ef234498133/volumes/kubernetes.io~csi/pvc-c8861c23-c03d-47aa-96f6-73c4d4093109/mount    /dev/rbd0 ext4   rw,relatime,

这里我们就经过了 Attach 和 Mount 两个阶段完成了 Volume 的持久化

对于上面我们使用的 NFS 就更加简单了, 因为 NFS 存储并没有一个设备需要挂载到宿主机上面,所以这个时候 kubelet 就会直接进入第二个 Mount 阶段,相当于直接在宿主机上面执行如下的命令(nfs本身就是个文件系统,不需要再次格式化可以直接使用,所以直接挂在即可):

$ mount -t nfs 10.151.30.11:/data/k8s /var/lib/kubelet/pods/d4fcdb11-baf7-43d9-8d7d-3ede24118e08/volumes/kubernetes.io~nfs/nfs-pv

同样可以在测试的 Pod 所在节点查看 Volume 的挂载信息:

$ findmnt /var/lib/kubelet/pods/d4fcdb11-baf7-43d9-8d7d-3ede24118e08/volumes/kubernetes.io~nfs/nfs-pv
TARGET                                                                               SOURCE                 FSTYPE OPTIONS
/var/lib/kubelet/pods/d4fcdb11-baf7-43d9-8d7d-3ede24118e08/volumes/kubernetes.io~nfs/nfs-pv
                                                                                     10.151.30.11:/data/k8s nfs4   rw,relatime,

我们可以看到这个 pod的Volume 被挂载到了 NFS(10.151.30.11:/data/k8s)下面,以后我们在这个目录里写入的所有文件,都会被保存在远程 NFS 服务器上。(其实也可以看成nfs服务器的/data/k8s目录或者说文件系统挂载到容器中的volume对应的目录下(这个volume又会被挂载到容器中的某个目录下)

我们可以看到这个 Volume 被挂载到了 NFS(10.151.30.11:/data/k8s)下面,以后我们在这个目录里写入的所有文件,都会被保存在远程 NFS 服务器上。

这样在经过了上面的两个阶段过后,我们就得到了一个持久化的宿主机上面的 Volume 目录了,接下来 kubelet 只需要把这个 Volume 目录挂载到容器中对应的目录即可,这样就可以为 Pod 里的容器挂载这个持久化的 Volume 了,这一步其实也就相当于执行了如下所示的命令:

$ docker run -v /var/lib/kubelet/pods/<Pod的ID>/volumes/kubernetes.io~<Volume类型>/<Volume名字>:/<容器内的目标目录> 我的镜像 ...

在这里插入图片描述

PV Controller:负责 PV/PVC 的绑定,并根据需求进行数据卷的 Provision/Delete 操作
AD Controller:负责存储设备的 Attach/Detach 操作,将设备挂载到目标节点
Volume Manager:管理卷的 Mount/Unmount 操作、卷设备的格式化等操作
Volume Plugin:扩展各种存储类型的卷管理能力,实现第三方存储的各种操作能力和 Kubernetes 存储系统结合

我们上面使用的 NFS 就属于 In-Tree 这种方式,而上节课使用的 Ceph RBD 就是 Out-Of-Tree 的方式,而且是使用的是 CSI 插件

两种out-of-tree得存储插件机制

FlexVolume(非主流)

FlexVolume 提供了一种扩展 Kubernetes 存储插件的方式,用户可以自定义自己的存储插件。要使用 FlexVolume 需要在每个节点上安装存储插件二进制文件,该二进制需要实现 FlexVolume 的相关接口,默认存储插件的存放路径为/usr/libexec/kubernetes/kubelet-plugins/volume/exec/<vendor~driver>/,VolumePlugins 组件会不断 watch 这个目录来实现插件的添加、删除等功能。

其中 vendor~driver 的名字需要和 Pod 中flexVolume.driver 的字段名字匹配,例如:

/usr/libexec/kubernetes/kubelet-plugins/volume/exec/foo~cifs/cifs

对应的 Pod 中的 flexVolume.driver 属性为:foo/cifs。

在我们实现自定义存储插件的时候,需要实现 FlexVolume 的部分接口,因为要看实际需求,并不一定所有接口都需要实现。比如对于类似于 NFS 这样的存储就没必要实现 attach/detach 这些接口了,因为不需要,只需要实现 init/mount/umount 3个接口即可。

init: init - kubelet/kube-controller-manager 初始化存储插件时调用,插件需要返回是否需要要 attach 和 detach 操作
attach: attach - 将存储卷挂载到 Node 节点上
detach: detach - 将存储卷从 Node 上卸载
waitforattach: waitforattach - 等待 attach 操作成功(超时时间为 10 分钟)
isattached: isattached - 检查存储卷是否已经挂载
mountdevice: mountdevice - 将设备挂载到指定目录中以便后续 bind mount 使用
unmountdevice: unmountdevice - 将设备取消挂载
mount: mount - 将存储卷挂载到指定目录中
unmount: unmount - 将存储卷取消挂载

实现上面的这些接口需要返回如下所示的 JSON 格式的数据:

{
    
    
    "status": "<Success/Failure/Not supported>",
    "message": "<Reason for success/failure>",
    "device": "<Path to the device attached. This field is valid only for attach & waitforattach call-outs>"
    "volumeName": "<Cluster wide unique name of the volume. Valid only for getvolumename call-out>"
    "attached": <True/False (Return true if volume is attached on the node. Valid only for isattached call-out)>
    "capabilities": <Only included as part of the Init response>
    {
    
    
        "attach": <True/False (Return true if the driver implements attach and detach)>
    }
}

比如我们来实现一个 NFS 的 FlexVolume 插件,最简单的方式就是写一个脚本,然后实现 init、mount、unmount 3个命令即可,然后按照上面的 JSON 格式返回数据,最后把这个脚本放在节点的 FlexVolume 插件目录下面即可。

下面就是官方给出的一个 NFS 的 FlexVolume 插件示例,可以从 https://github.com/kubernetes/examples/blob/master/staging/volumes/flexvolume/nfs 获取脚本:

#!/bin/bash
# 注意:
#  - 在使用插件之前需要先安装 jq。
usage() {
    
    
    err "Invalid usage. Usage: "
    err "\t$0 init"
    err "\t$0 mount <mount dir> <json params>"
    err "\t$0 unmount <mount dir>"
    exit 1
}

err() {
    
    
    echo -ne $* 1>&2
}

log() {
    
    
    echo -ne $* >&1
}

ismounted() {
    
    
    MOUNT=`findmnt -n ${
     
     MNTPATH} 2>/dev/null | cut -d' ' -f1`
    if [ "${MOUNT}" == "${MNTPATH}" ]; then
        echo "1"
    else
        echo "0"
    fi
}

domount() {
    
    
    MNTPATH=$1

    NFS_SERVER=$(echo $2 | jq -r '.server')
    SHARE=$(echo $2 | jq -r '.share')

    if [ $(ismounted) -eq 1 ] ; then
        log '{"status": "Success"}'
        exit 0
    fi

    mkdir -p ${MNTPATH} &> /dev/null

    mount -t nfs ${NFS_SERVER}:/${SHARE} ${MNTPATH} &> /dev/null
    if [ $? -ne 0 ]; then
        err "{ \"status\": \"Failure\", \"message\": \"Failed to mount ${NFS_SERVER}:${SHARE} at ${MNTPATH}\"}"
        exit 1
    fi
    log '{"status": "Success"}'
    exit 0
}

unmount() {
    
    
    MNTPATH=$1
    if [ $(ismounted) -eq 0 ] ; then
        log '{"status": "Success"}'
        exit 0
    fi

    umount ${MNTPATH} &> /dev/null
    if [ $? -ne 0 ]; then
        err "{ \"status\": \"Failed\", \"message\": \"Failed to unmount volume at ${MNTPATH}\"}"
        exit 1
    fi

    log '{"status": "Success"}'
    exit 0
}

op=$1

if ! command -v jq >/dev/null 2>&1; then
    err "{ \"status\": \"Failure\", \"message\": \"'jq' binary not found. Please install jq package before using this driver\"}"
    exit 1
fi

if [ "$op" = "init" ]; then
    log '{"status": "Success", "capabilities": {"attach": false}}'
    exit 0
fi

if [ $# -lt 2 ]; then
    usage
fi

shift

case "$op" in
    mount)
        domount $*
        ;;
    unmount)
        unmount $*
        ;;
    *)
        log '{"status": "Not supported"}'
        exit 0
esac

exit 1

将上面脚本命名成 nfs,放置到 node1 节点对应的插件下面: /usr/libexec/kubernetes/kubelet-plugins/volume/exec/ydzs~nfs/nfs,并设置权限为 700:

$ chmod 700 /usr/libexec/kubernetes/kubelet-plugins/volume/exec/ydzs~nfs/nfs
# 安装 jq 工具
$ yum -y install https://dl.fedoraproject.org/pub/epel/epel-release-latest-7.noarch.rpm
$ yum install jq -y

这个时候我们部署一个应用到 node1 节点上,并用 flexVolume 来持久化容器中的数据(当然也可以通过定义 flexvolume 类型的 PV、PVC 来使用),如下所示:(test-flexvolume.yaml)

apiVersion: v1
kind: Pod
metadata:
  name: test-flexvolume
spec:
  nodeSelector:
    kubernetes.io/hostname: ydzs-node1
  volumes:
  - name: test
    flexVolume:
      driver: "ydzs/nfs"  # 定义插件类型,根据这个参数在对应的目录下面找到插件的可执行文件
      fsType: "nfs"  # 定义存储卷文件系统类型
      options:  # 定义所有与存储相关的一些具体参数
        server: "10.151.30.11"
        share: "data/k8s"
  containers:
  - name: web
    image: nginx
    ports:
    - containerPort: 80
    volumeMounts:
    - name: test
      subPath: testflexvolume
      mountPath: /usr/share/nginx/html

其中 flexVolume.driver 就是插件目录 ydzs~nfs 对应的 ydzs/nfs 名称,flexVolume.options 中根据上面的 nfs 脚本可以得知里面配置的是 NFS 的 Server 地址和挂载目录路径,直接创建上面的资源对象:

$ kubectl apply -f test-flexvolume.yaml
$ kubectl get pods 
NAME                                      READY   STATUS    RESTARTS   AGE
test-flexvolume                           1/1     Running   0          13h
......
$ kubectl exec -it test-flexvolume mount |grep test
10.151.30.11:/data/k8s/testflexvolume on /usr/share/nginx/html type nfs4 (rw,relatime,vers=4.1,rsize=524288,wsize=524288,namlen=255,hard,proto=tcp,timeo=600,retrans=2,sec=sys,clientaddr=10.151.30.22,local_lock=none,addr=10.151.30.11)
$ mount |grep test
10.151.30.11:/data/k8s on /var/lib/kubelet/pods/a376832a-7638-4faf-b1a0-404956e8e60a/volumes/ydzs~nfs/test type nfs4 (rw,relatime,vers=4.1,rsize=524288,wsize=524288,namlen=255,hard,proto=tcp,timeo=600,retrans=2,sec=sys,clientaddr=10.151.30.22,local_lock=none,addr=10.151.30.11)
10.151.30.11:/data/k8s/testflexvolume on /var/lib/kubelet/pods/a376832a-7638-4faf-b1a0-404956e8e60a/volume-subpaths/test/web/0 type nfs4 (rw,relatime,vers=4.1,rsize=524288,wsize=524288,namlen=255,hard,proto=tcp,timeo=600,retrans=2,sec=sys,clientaddr=10.151.30.22,local_lock=none,addr=10.151.30.11)

同样我们可以查看到 Pod 的本地持久化目录是被 mount 到了 NFS 上面,证明上面我们的 FlexVolume 插件是正常的。

调用

当我们要去真正的 mount NFS 的时候,就是通过 kubelet 调用VolumePlugin,然后直接执行命令/usr/libexec/kubernetes/kubelet-plugins/volume/exec/ydzs~nfs/nfsmount 来完成的,就相当于平时我们在宿主机上面手动挂载 NFS的方式一样的,所以存储插件 nfs 是一个可执行的二进制文件或者 shell 脚本都是可以的。

CSI(主流)

既然已经有了 FlexVolume 插件了,为什么还需要 CSI 插件呢?上面我们使用 FlexVolume 插件的时候可以看出 FlexVolume 插件实际上相当于就是一个普通的 shell 命令,类似于平时我们在 Linux 下面执行的 ls 命令一样,只是返回的信息是 JSON 格式的数据,并不是我们通常认为的一个常驻内存的进程,而 CSI 是一个更加完善、编码更加方便友好的一种存储插件扩展方式。
在这里插入图片描述
CSI 是由来自 Kubernetes、Mesos、 Cloud Foundry 等社区成员联合制定的一个行业标准接口规范,旨在将任意存储系统暴露给容器化应用程序。CSI 规范定义了存储提供商实现 CSI 兼容插件的最小操作集合和部署建议,CSI 规范的主要焦点是声明插件必须实现的接口。
在这里插入图片描述
Kubernetes CSI 存储体系主要由两部分组成:

Kubernetes 外部组件:包含 Driver registrar、External provisioner、External attacher 三部分,这三个组件是从 Kubernetes 原本的 in-tree 存储体系中剥离出来的存储管理功能,实际上是 Kubernetes 中的一种外部 controller ,它们 watch kubernetes 的 API 资源对象,根据 watch 到的状态来调用下面提到的第二部分的 CSI 插件来实现存储的管理和操作。这部分是 Kubernetes 团队维护的,插件开发者完全不必关心其实现细节。

Driver registra:用于将插件注册到 kubelet 的 sidecar 容器,并将驱动程序自定义的 NodeId 添加到节点Annotations 上,通过与 CSI 上面的 Identity 服务进行通信调用 CSI 的 GetNodeId 方法来完成该操作。
External provisioner:用于 watch Kubernetes 的 PVC 对象并调用 CSI 的CreateVolume 和 DeleteVolume 操作。
External attacher:用于 Attach/Detach阶段,通过 watch Kubernetes 的 VolumeAttachment 对象并调用 CSI 的ControllerPublish 和 ControllerUnpublish 操作来完成对应的 Volume 的Attach/Detach。而 Volume 的 Mount/Unmount 阶段并不属于外部组件,当真正需要执行 Mount操作的时候,kubelet 会去直接调用下面的 CSI Node 服务来完成 Volume 的 Mount/UnMount 操作。

CSI 存储插件: 这部分正是开发者需要实现的 CSI 插件部分,都是通过 gRPC 实现的服务,一般会用一个二进制文件对外提供服务,主要包含三部分:CSI Identity、CSI Controller、CSI Node。

CSI Identity — 主要用于负责对外暴露这个插件本身的信息,确保插件的健康状态。

service Identity {
    
    
    // 返回插件的名称和版本
    rpc GetPluginInfo(GetPluginInfoRequest)
        returns (GetPluginInfoResponse) {
    
    }
    // 返回这个插件的包含的功能,比如非块存储类型的 CSI 插件不需要实现 Attach 功能,GetPluginCapabilities 就可以在返回中标注这个 CSI 插件不包含 Attach 功能
    rpc GetPluginCapabilities(GetPluginCapabilitiesRequest)
        returns (GetPluginCapabilitiesResponse) {
    
    }
    // 插件插件是否正在运行
    rpc Probe (ProbeRequest)
        returns (ProbeResponse) {
    
    }
}

CSI Controller - 主要实现 Volume 管理流程当中的 Provision 和 Attach 阶段,Provision 阶段是指创建和删除 Volume 的流程,而 Attach 阶段是指把存储卷附着在某个节点或脱离某个节点的流程,另外只有块存储类型的 CSI 插件才需要 Attach 功能。

service Controller {
    
    
    // 创建存储卷,包括云端存储介质以及PV对象
    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) {
    
    }

    // 返回所有可用的 volumes
    rpc ListVolumes (ListVolumesRequest)
        returns (ListVolumesResponse) {
    
    }

    // 可用存储池的总容量
    rpc GetCapacity (GetCapacityRequest)
        returns (GetCapacityResponse) {
    
    }

    // 例如. 插件可能未实现 GetCapacity、Snapshotting
    rpc ControllerGetCapabilities (ControllerGetCapabilitiesRequest)
        returns (ControllerGetCapabilitiesResponse) {
    
    }

    // 创建快照
    rpc CreateSnapshot (CreateSnapshotRequest)
        returns (CreateSnapshotResponse) {
    
    }

    // 删除指定的快照
    rpc DeleteSnapshot (DeleteSnapshotRequest)
        returns (DeleteSnapshotResponse) {
    
    }

    // 获取所有的快照
    rpc ListSnapshots (ListSnapshotsRequest)
        returns (ListSnapshotsResponse) {
    
    }
}

CSI Node — 负责控制 Kubernetes 节点上的 Volume 操作。其中 Volume 的挂载被分成了 NodeStageVolume 和 NodePublishVolume 两个阶段。NodeStageVolume 接口主要是针对块存储类型的 CSI 插件而提供的,块设备在 “Attach” 阶段被附着在 Node 上后,需要挂载至 Pod 对应目录上,但因为块设备在 linux 上只能 mount 一次,而在 kubernetes volume 的使用场景中,一个 volume 可能被挂载进同一个 Node 上的多个 Pod 实例中,所以这里提供了 NodeStageVolume 这个接口,使用这个接口把块设备格式化后先挂载至 Node 上的一个临时全局目录,然后再调用 NodePublishVolume 使用 linux 中的 bind mount 技术把这个全局目录挂载进 Pod 中对应的目录上。

service Node {
    
    
    // 在节点上初始化存储卷(格式化),并执行挂载到Global目录
    rpc NodeStageVolume (NodeStageVolumeRequest)
        returns (NodeStageVolumeResponse) {
    
    }

    // umount 存储卷在节点上的 Global 目录
    rpc NodeUnstageVolume (NodeUnstageVolumeRequest)
        returns (NodeUnstageVolumeResponse) {
    
    }

    // 在节点上将存储卷的 Global 目录挂载到 Pod 的实际挂载目录
    rpc NodePublishVolume (NodePublishVolumeRequest)
        returns (NodePublishVolumeResponse) {
    
    }

    // unmount 存储卷在节点上的 Pod 挂载目录
    rpc NodeUnpublishVolume (NodeUnpublishVolumeRequest)
        returns (NodeUnpublishVolumeResponse) {
    
    }

    // 获取节点上Volume挂载文件系统统计信息(总空间、可用空间等)
    rpc NodeGetVolumeStats (NodeGetVolumeStatsRequest)
        returns (NodeGetVolumeStatsResponse) {
    
    }

    // 获取节点的唯一 ID
    rpc NodeGetId (NodeGetIdRequest)
        returns (NodeGetIdResponse) {
    
    
        option deprecated = true;
    }

    // 返回节点插件的能力
    rpc NodeGetCapabilities (NodeGetCapabilitiesRequest)
        returns (NodeGetCapabilitiesResponse) {
    
    }

    // 获取节点的一些信息
    rpc NodeGetInfo (NodeGetInfoRequest)
        returns (NodeGetInfoResponse) {
    
    }
}

只需要实现上面的接口就可以实现一个 CSI 插件了。虽然 Kubernetes 并未规定 CSI 插件的打包安装,但是提供了以下建议来简化我们在 Kubernetes 上容器化 CSI Volume 驱动程序的部署方案
在这里插入图片描述
按照上图的推荐方案,CSI Controller 部分以 StatefulSet 或者 Deployment 方式部署,CSI Node 部分以 DaemonSet 方式部署。因为这两部分实现在同一个 CSI 插件程序中,因此只需要把这个 CSI 插件与 External Components 以容器方式部署在同一个 Pod中,把这个 CSI 插件与 Driver registrar 以容器方式部署在 DaemonSet 的 Pod 中,即可完成 CSI 的部署。

前面我们使用的 Rook 部署的 Ceph 集群就是实现了 CSI 插件的:

$ kubectl get pods -n rook-ceph |grep plugin
csi-cephfsplugin-2s9d5                                 3/3     Running     0          21d
csi-cephfsplugin-fgp4v                                 3/3     Running     0          17d
csi-cephfsplugin-fv5nx                                 3/3     Running     0          21d
csi-cephfsplugin-mn8q4                                 3/3     Running     0          17d
csi-cephfsplugin-nf6h8                                 3/3     Running     0          21d
csi-cephfsplugin-provisioner-56c8b7ddf4-68h6d          4/4     Running     0          21d
csi-cephfsplugin-provisioner-56c8b7ddf4-rq4t6          4/4     Running     0          21d
csi-cephfsplugin-xwnl4                                 3/3     Running     0          21d
csi-rbdplugin-7r88w                                    3/3     Running     0          21d
csi-rbdplugin-95g5j                                    3/3     Running     0          21d
csi-rbdplugin-bnzpr                                    3/3     Running     0          21d
csi-rbdplugin-dvftb                                    3/3     Running     0          21d
csi-rbdplugin-jzmj2                                    3/3     Running     0          17d
csi-rbdplugin-provisioner-6ff4dd4b94-bvtss             5/5     Running     0          21d
csi-rbdplugin-provisioner-6ff4dd4b94-lfn68             5/5     Running     0          21d
csi-rbdplugin-trxb4                                    3/3     Running     0          17d

这里其实是实现了 RBD 和 CephFS 两种 CSI,用 DaemonSet 在每个节点上运行了一个包含 Driver registra 容器的 Pod,当然和节点相关的操作比如 Mount/Unmount 也是在这个 Pod 里面执行的,其他的比如 Provision、Attach 都是在另外的 csi-rbdplugin-provisioner-xxx Pod 中执行的。

猜你喜欢

转载自blog.csdn.net/weixin_45843419/article/details/124603184