背景

为什么k8s需要RBD呢?以前用到的volume,有 configmap 、empty dir、hostpath,configmap通常是用来向容器注入配置的,而empty dir、hostpath 也能够用来存储数据,但他们都有一个致命的问题:如果容器被删除了,数据也就跟着丢失了。这对一些无状态应用来说没什么,因为他们的数据可能早就进数据库了,但对一些有状态的应用来说(比如说mysql),就是个问题了。

Ceph RBD、Cephfs,都是解决这个问题的办法。

Ceph RBD是个什么东西呢?

我们知道,存储可以分为三类:对象存储(例如aws s3、aliyun oss)、文件存储(例如nfs、nas)、块存储。而Ceph RBD(RADOS Block Devices)即为块存储的一种,下文的Cephfs则是文件存储的一种。

下面简要走一下ceph RBD的使用。

ceph集群

因为公司有专门团队做Ceph,我只管用就行,跳过Ceph集群的安装过程。对于k8s来说,需要的Ceph信息有这些:

IP地址、端口号 管理员用户名 管理员keyring

node上准备ceph

接下来会用一些ceph的命令行,用来根ceph集群交互。

yum/apt install ceph-common ceph-fs-common -y

ceph命令行需要2个配置文件,具体配置问ceph团队就行了。

  • /etc/ceph/ceph.conf
  • /etc/ceph/ceph.client.admin.keyring

RBD是这样用的:用户在Ceph上创建Pool(逻辑隔离),然后在Pool中创建image(实际存储介质),之后再将image挂载到本地服务器的某个目录上。

记住下面几条命令就行了。

# rbd list    #列出默认pool下的image
# rbd list -p k8s   #列出pool k8s下的image
# rbd create foo -s 1024  #在默认pool中创建名为foo的image,大小为1024MB
# rbd map foo #将ceph集群的image映射到本地的块设备
/dev/rbd0
# ls -l /dev/rbd0   #是b类型
brw-rw---- 1 root disk 252, 0 May 22 20:57 /dev/rbd0
$ rbd showmapped    #查看已经map的rbd image
id pool image snap device    
0  rbd  foo   -    /dev/rbd0
# mount /dev/rbd1 /mnt/bar/   #此时去mount会失败,因为image还没有格式化文件系统
mount: /dev/rbd1 is write-protected, mounting read-only
mount: wrong fs type, bad option, bad superblock on /dev/rbd1,
       missing codepage or helper program, or other error

       In some cases useful info is found in syslog - try
       dmesg | tail or so.
# mkfs.ext4 /dev/rbd0   #格式化为ext4
...
Writing superblocks and filesystem accounting information: done
# mount /dev/rbd0 /mnt/foo/   #重新挂载
# df -h |grep foo             #ok
/dev/rbd0       976M  2.6M  907M   1% /mnt/foo

如果你有自己挂载过硬盘的经验,对RBD的操作应该是很快就能熟悉了。

上面都是在默认pool中做的,我们在k8s里,当然要用自己的pool了。

# ceph osd lspools # 看看已经有那些pool
# ceph osd pool create k8s 128 #创建pg_num 为128的名为k8s的pool
# rados df

有了自己专属的 pool以后,把上面创建image的过程重新走一下吧。

# rbd create foobar -s 1024 -p k8s #在k8s pool中创建名为foobar的image,大小为1024MB

不要挂载!不要挂载!不要挂载!

RBD用作volume

前面已经创建好了image foobar,给volume用简单直接粗暴,挂就行了。

apiVersion: v1
kind: Pod
metadata:
  name: rbd
spec:
  containers:
    - image: gcr.io/nginx
      name: rbd-rw
      volumeMounts:
      - name: rbdpd
        mountPath: /mnt/rbd
  volumes:
    - name: rbdpd
      rbd:
        monitors:
        - '1.2.3.4:6789'
        pool: k8s
        image: foobar
        fsType: ext4
        readOnly: false
        user: admin
        keyring: /etc/ceph/ceph.client.admin.keyring 

Pod启动后,可以看到文件系统由k8s做好并挂载到了容器里。我们将/etc/hosts文件拷贝到/mnt/rbd/目录去。

# kubectl exec rbd -- df -h|grep rbd
/dev/rbd6       976M  2.6M  907M   1% /mnt/rbd
# kubectl exec rbd -- cp /etc/hosts /mnt/rbd/
# kubectl exec rbd -- cat /mnt/rbd/hosts
# Kubernetes-managed hosts file.
127.0.0.1       localhost
::1     localhost ip6-localhost ip6-loopback
fe00::0 ip6-localnet
fe00::0 ip6-mcastprefix
fe00::1 ip6-allnodes
fe00::2 ip6-allrouters
10.244.3.249    rbd

然后将Pod删除、重新挂载foobar image。

前面Pod要求各node上都要有keyring文件,很不方便也不安全。新的Pod我使用推荐的做法:secret(虽然也安全不到哪里)

先创建一个secret。

apiVersion: v1
kind: Secret
metadata:
  name: ceph-secret
type: "kubernetes.io/rbd"  
data:
  key: QVFCXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX9PQ==

在新的Pod里Ref这个secret。

apiVersion: v1
kind: Pod
metadata:
  name: rbd3
spec:
  containers:
    - image: gcr.io/nginx
      name: rbd-rw
      volumeMounts:
      - name: rbdpd
        mountPath: /mnt/rbd
  volumes:
    - name: rbdpd
      rbd:
        monitors: 
        - '1.2.3.4:6789'
        pool: k8s
        image: foobar
        fsType: ext4
        readOnly: false
        user: admin
        secretRef:
          name: ceph-secret

再来看看前面写到image上的文件还在不在。

kubectl exec rbd3 -- cat /mnt/rbd/hosts
# Kubernetes-managed hosts file.
127.0.0.1       localhost
::1     localhost ip6-localhost ip6-loopback
fe00::0 ip6-localnet
fe00::0 ip6-mcastprefix
fe00::1 ip6-allnodes
fe00::2 ip6-allrouters
10.244.3.249    rbd

你会发现,哎,文件还在哎!对,这就是RBD的效果。但注意,Volume已经不存在了,虽然数据保住了,但这其实不是我们希望的持久化存储的方式。

RBD用作PV/PVC

volume是一种比较初级的使用方式,中级的方式是使用PV/PVC。

简单说说PV/PVC。集群管理员会创建多个PV(Persist Volume,持久化卷,不区分namespace),而普通用户会创建PVC(PV Claim,PV声明),k8s会从满足PVC的要求中选择一个PV来与该PVC绑定,之后再将PV对应的image挂载到容器中。

来看个例子。

先在k8s pool里创建一个名为pv的 image。

rbd image create pv -s 1024 -p k8s

再创建一个PV,使用上面创建的image pv。

apiVersion: v1
kind: PersistentVolume
metadata:
  name: ceph-rbd-pv
spec:
  capacity:
    storage: 1Gi
  accessModes:
    - ReadWriteOnce
  rbd:
    monitors:
      - '1.2.3.4:6789'
    pool: k8s
    image: pv
    user: admin
    secretRef:
      name: ceph-secret
    fsType: ext4
    readOnly: false
  persistentVolumeReclaimPolicy: Recycle

看下现在pv的状态,还是 Available。

kubectl get pv|grep rbd
ceph-rbd-pv                                1Gi        RWO            Recycle          Available

创建一个PVC,要求一块1G的存储。

apiVersion: v1
kind: PersistentVolumeClaim
metadata:
  name: ceph-rbd-pv-claim
spec:
  accessModes:
    - ReadWriteOnce
  resources:
    requests:
      storage: 1Gi

因为上面已经创建满足要求的PV了,可以看到pvc和pv的状态都已经是Bound了。

# kubectl get pvc|grep rbd
ceph-rbd-pv-claim   Bound     pvc-bdaf8358-5dc6-11e8-a37d-ecf4bbdeea94   1Gi        RWO            fast           10s
# kubectl get pv|grep ceph-rbd-pv
ceph-rbd-pv     1Gi    RWO    Recycle    Bound     12m

RBD用作storage class

为什么前面说PV/PVC是一个“中级”解决方案呢?

很简单啊,管理员不可能没事就等着给人创建RBD image、PV,而一次创建大量PV等着人用又显得很不云计算。

storage class的出现,就是为了解决这个问题。

简单来说,storage创建需要的材料,只需要访问ceph RBD的IP/Port、用户名、keyring、pool,不需要提前创建image;当用户创建一个PVC时,k8s查找是否有符合PVC请求的storage class类型,如果有,则依次:

  1. 到ceph集群上创建image
  2. 创建一个PV,名字为pvc-xx-xxx-xxx,大小pvc请求的storage。
  3. 将上面的PV与PVC绑定,格式化后挂到容器中

是不是很简单?管理员只要创建好storage class就行了,后面的事情用户自己就可以搞定了。当然了,为了防止流氓用户,设置一下Resource Quota还是很有必要的。

下面走一个。

先创建一个SC。跟PV一样,SC也是集群范围的(RBD认为是fast的)。

kind: StorageClass
apiVersion: storage.k8s.io/v1
metadata:
  name: fast
provisioner: kubernetes.io/rbd
parameters:
  monitors: 172.25.60.3:6789
  adminId: admin
  adminSecretName: ceph-secret
  adminSecretNamespace: resource-quota
  pool: k8s
  userId: admin
  userSecretName: ceph-secret
  fsType: ext4
  imageFormat: "2"
  imageFeatures: "layering"

之后创建应用的时候,需要同时创建 pvc+pod,二者通过claimName关联。pvc中需要指定其storageClassName为上面创建的sc的name(即fast)。

kind: PersistentVolumeClaim
apiVersion: v1
metadata:
  name: rbd-pvc-pod-pvc
spec:
  accessModes:
    - ReadWriteOnce
  volumeMode: Filesystem
  resources:
    requests:
      storage: 8Gi
  storageClassName: fast

RBD只支持 ReadWriteOnce 和 ReadOnlyAll,不支持ReadWriteAll。注意这两者的区别点是,不同nodes之间是否可以同时挂载。同一个node上,即使是ReadWriteOnce,也可以同时挂载到2个容器上的。

apiVersion: v1
kind: Pod
metadata:
  labels:
    test: rbd-pvc-pod
  name: ceph-rbd-sc-pod2
spec:
  containers:
  - name: ceph-rbd-sc-nginx
    image: gcr.io/nginx
    volumeMounts:
    - name: ceph-rbd-vol1
      mountPath: /mnt/ceph-rbd-pvc/nginx
      readOnly: false
  volumes:
  - name: ceph-rbd-vol1
    persistentVolumeClaim:
      claimName: rbd-pvc-pod-pvc

其实我觉得PVC的创建都可以直接省掉,可以新增一个类型,直接Pod请求就得了,类似下面StatefulSets里的volumeClaimTemplates。

如果是多副本的应用怎么办呢?可以用StatefulSet。

apiVersion: apps/v1
kind: StatefulSet
metadata:
  name: nginx
spec:
  selector:
    matchLabels:
      app: nginx
  serviceName: "nginx"
  replicas: 3
  template:
    metadata:
      labels:
        app: nginx
    spec:
      terminationGracePeriodSeconds: 10
      containers:
      - name: nginx
        image: gcr.io/nginx
        volumeMounts:
        - name: www
          mountPath: /usr/share/nginx/html
  volumeClaimTemplates:
  - metadata:
      name: www
    spec:
      accessModes: [ "ReadWriteOnce" ]
      storageClassName: "fast"
      resources:
        requests:
          storage: 8Gi

此时去看PVC,可以看到创建了3个PVC。

# kubectl get pvc|grep www
www-nginx-0         Bound     pvc-05f10f64-58df-11e8-8bd4-ecf4bbdeea94   8Gi        RWO            fast           6d
www-nginx-1         Bound     pvc-0e1768f3-58df-11e8-8bd4-ecf4bbdeea94   8Gi        RWO            fast           6d
www-nginx-2         Bound     pvc-17347e8e-58df-11e8-8bd4-ecf4bbdeea94   8Gi        RWO            fast           6d
# kubectl get pv|grep www
pvc-05f10f64-58df-11e8-8bd4-ecf4bbdeea94   8Gi        RWO            Delete           Bound       dex/www-nginx-0                     fast                     6d
pvc-0e1768f3-58df-11e8-8bd4-ecf4bbdeea94   8Gi        RWO            Delete           Bound       dex/www-nginx-1                     fast                     6d
pvc-17347e8e-58df-11e8-8bd4-ecf4bbdeea94   8Gi        RWO            Delete           Bound       dex/www-nginx-2                     fast                     6d

但注意不要用Deployment。因为,如果Deployment的副本数是1,那么还是可以用的,跟Pod一致;但如果副本数 >1 ,此时创建deployment后会发现,只启动了1个Pod,其他Pod都在ContainerCreating状态。过一段时间describe pod可以看到,等volume等很久都没等到。

 Unable to mount volumes for pod "nginx-deployment-55bd845d95-vjs58_dex(73be7605-5dcf-11e8-a37d-ecf4bbdeea94)": timeout expired waiting for volumes to attach or mount for pod "default"/"nginx-deployment-55bd845d95-vjs58". list of unmounted volumes=[nginx-rbd-v1]. list of unattached volumes=[nginx-rbd-v1 default-token-62cfx]

总结

本文介绍了kubernetes使用RBD的必要性,一些简单的RBD命令行及RBD手工操作方式,以及RBD在Volume、PV、StorageClass三种方式下的使用。

Ref