ここは今からストレージです。

Rook-Ceph OSD on PVC(前半)

f:id:ututaq:20191125022735p:plain

この記事はRookだらけの Advent Calendar 2019 6日目の記事です。

Rook 1.1でできるようになった OSD on PVC の説明をします。

OSD on PVC ???

OSD on PVCというのは、ceph-osdをPVC から作ろうということです。

CephはブロックデバイスからOSDを作成するため、言うまでもなくceph-osdにはブロックデバイスが必要です。
osdのブロックデバイスはrook-ceph-osd Podが稼働するworker nodeから取ってくるので、事前に何らかの方法でworker nodeにブロックデバイスをattachしておかなくてはなりません。 しかしこのworker nodeにブロックデバイスをattachする作業は、Kubernetesが乗るプラットフォーム側の操作です。Kubernetesで何かしらやりようはあると思いますが、基本的にはKubernetes外の管理操作だと思います。
せっかくRookでKubernetes内で完結できるストレージシステムが作れるというのに、根本にある保存媒体はKubernetes外でmanageされる。それってなんかイケてないですよね。

そういうわけで、 「既存のPV/PVCの仕組みでceph-osdのPodにブロックデバイスを直でattachしようや、そしたら何もないところから自動でOSDが作れるやろ」 を実現するのがOSD on PVCです。
この考えの元にあるのがKubernetes 1.11からでてきたRaw Block Volumeのfeatureです。(1.14でbetaながらデフォルトでenableになっているfeature) この技術を使ってPVをブロックデバイスとして見せることができるようになりました。

https://kubernetes.io/docs/concepts/storage/volumes/#csi-raw-block-volume-support

これってまるでCloud(Container) Native Storageのためにあるようなfeature。ええやん、ステキやん。

実際にやってみる

OSDはベースのPVC-baseのブロックデバイスにするとし、MONの構成ファイルをどうするかという選択肢もあります。
これまでMONの構成ファイルはworker nodeの/var/lib/rookなどyamlで記載されたディレクトリに置かれていたのですが、これもPVC-baseのファイルPVに置くというパターンが選べます。せっかくだからこっちでやってみましょう。

前回までと同じように3master+3workerのクラスタです。

[utubo@tutsunom ceph]$ kubectl get node --sort-by=".metadata.creationTimestamp" --show-labels
NAME                            STATUS   ROLES    AGE   VERSION   LABELS
ip-172-20-91-64.ec2.internal    Ready    master   25h   v1.15.5   beta.kubernetes.io/arch=amd64,beta.kubernetes.io/instance-type=t2.medium,beta.kubernetes.io/os=linux,failure-domain.beta.kubernetes.io/region=us-east-1,failure-domain.beta.kubernetes.io/zone=us-east-1b,kops.k8s.io/instancegroup=master-us-east-1b,kubernetes.io/arch=amd64,kubernetes.io/hostname=ip-172-20-74-29.ec2.internal,kubernetes.io/os=linux,kubernetes.io/role=master,node-role.kubernetes.io/master=
ip-172-20-53-29.ec2.internal    Ready    master   25h   v1.15.5   beta.kubernetes.io/arch=amd64,beta.kubernetes.io/instance-type=t2.medium,beta.kubernetes.io/os=linux,failure-domain.beta.kubernetes.io/region=us-east-1,failure-domain.beta.kubernetes.io/zone=us-east-1a,kops.k8s.io/instancegroup=master-us-east-1a,kubernetes.io/arch=amd64,kubernetes.io/hostname=ip-172-20-63-169.ec2.internal,kubernetes.io/os=linux,kubernetes.io/role=master,node-role.kubernetes.io/master=
ip-172-20-113-40.ec2.internal   Ready    master   25h   v1.15.5   beta.kubernetes.io/arch=amd64,beta.kubernetes.io/instance-type=t2.medium,beta.kubernetes.io/os=linux,failure-domain.beta.kubernetes.io/region=us-east-1,failure-domain.beta.kubernetes.io/zone=us-east-1c,kops.k8s.io/instancegroup=master-us-east-1c,kubernetes.io/arch=amd64,kubernetes.io/hostname=ip-172-20-121-240.ec2.internal,kubernetes.io/os=linux,kubernetes.io/role=master,node-role.kubernetes.io/master=
ip-172-20-93-28.ec2.internal    Ready    node     25h   v1.15.5   beta.kubernetes.io/arch=amd64,beta.kubernetes.io/instance-type=t2.large,beta.kubernetes.io/os=linux,failure-domain.beta.kubernetes.io/region=us-east-1,failure-domain.beta.kubernetes.io/zone=us-east-1a,kops.k8s.io/instancegroup=nodes,kubernetes.io/arch=amd64,kubernetes.io/hostname=ip-172-20-52-182.ec2.internal,kubernetes.io/os=linux,kubernetes.io/role=node,node-role.kubernetes.io/node=
ip-172-20-42-193.ec2.internal   Ready    node     25h   v1.15.5   beta.kubernetes.io/arch=amd64,beta.kubernetes.io/instance-type=t2.large,beta.kubernetes.io/os=linux,failure-domain.beta.kubernetes.io/region=us-east-1,failure-domain.beta.kubernetes.io/zone=us-east-1c,kops.k8s.io/instancegroup=nodes,kubernetes.io/arch=amd64,kubernetes.io/hostname=ip-172-20-118-92.ec2.internal,kubernetes.io/os=linux,kubernetes.io/role=node,node-role.kubernetes.io/node=
ip-172-20-102-19.ec2.internal   Ready    node     25h   v1.15.5   beta.kubernetes.io/arch=amd64,beta.kubernetes.io/instance-type=t2.large,beta.kubernetes.io/os=linux,failure-domain.beta.kubernetes.io/region=us-east-1,failure-domain.beta.kubernetes.io/zone=us-east-1b,kops.k8s.io/instancegroup=nodes,kubernetes.io/arch=amd64,kubernetes.io/hostname=ip-172-20-74-57.ec2.internal,kubernetes.io/os=linux,kubernetes.io/role=node,node-role.kubernetes.io/node=

まず、ベースの元になるPV/PVCのStorageClassをチェックしましょう。
ベースのStorageClassがvolumeBindingMode: WaitForFirstConsumerで定義されていることが大事です。volumeBindingMode: Immediateだと動きません。ダメです。ここちょっとハマりました。

[utubo@tutsunom ceph]$ cat my-sc-gp2.yaml 
kind: StorageClass
apiVersion: storage.k8s.io/v1
metadata:
  name: my-gp2
  annotations:
    storageclass.kubernetes.io/is-default-class: "true"
provisioner: kubernetes.io/aws-ebs
volumeBindingMode: WaitForFirstConsumer
[utubo@tutsunom ceph]$ kubectl create -f my-sc-gp2.yaml 

それではgithubにあるサンプルのcluster-on-pvc.yamlから、StorageClassと(gp2 --> my-gp2)とosdの数(count:3 --> 9)だけ変更して、Cephクラスタを作ってみます。(長いのでコメント行は省略)
yamlの中のmon:storage:(=osd)にあるvolumeClaimTemplateがPVC-baseの証。

[utubo@tutsunom ceph]$ cat my-cluster-on-pvc.yaml 
apiVersion: ceph.rook.io/v1
kind: CephCluster
metadata:
  name: rook-ceph
  namespace: rook-ceph
spec:
  dataDirHostPath: /var/lib/rook
  mon:
    count: 3
    allowMultiplePerNode: false
    volumeClaimTemplate:
      spec:
        storageClassName: my-gp2
        resources:
          requests:
            storage: 10Gi
  cephVersion:
    image: ceph/ceph:v14.2.4-20190917
    allowUnsupported: false
  dashboard:
    enabled: true
    ssl: true
  network:
    hostNetwork: false
  storage:
    topologyAware: true
    storageClassDeviceSets:
    - name: set1
      count: 9
      portable: true
      placement:
        podAntiAffinity:
          preferredDuringSchedulingIgnoredDuringExecution:
          - weight: 100
            podAffinityTerm:
              labelSelector:
                matchExpressions:
                - key: app
                  operator: In
                  values:
                  - rook-ceph-osd
                - key: app
                  operator: In
                  values:
                  - rook-ceph-osd-prepare
              topologyKey: kubernetes.io/hostname
      resources:
      volumeClaimTemplates:
      - metadata:
          name: data
        spec:
          resources:
            requests:
              storage: 10Gi
          storageClassName: my-gp2
          volumeMode: Block
          accessModes:
            - ReadWriteOnce
  disruptionManagement:
    managePodBudgets: false
    osdMaintenanceTimeout: 30
    manageMachineDisruptionBudgets: false
    machineDisruptionBudgetNamespace: openshift-machine-api
[utubo@tutsunom ceph]$
[utubo@tutsunom ceph]$ kubectl create -f common.yaml
[utubo@tutsunom ceph]$ kubectl create -f operator.yaml
[utubo@tutsunom ceph]$ kubectl create -f my-cluster-on-pvc.yaml
[utubo@tutsunom ceph]$
[utubo@tutsunom ceph]$ kubectl -n rook-ceph get pod
NAME                                            READY   STATUS      RESTARTS   AGE
csi-cephfsplugin-jbs7g                          3/3     Running     0          4m53s
csi-cephfsplugin-lvtg9                          3/3     Running     0          4m53s
csi-cephfsplugin-provisioner-974b566d9-b24zs    4/4     Running     0          4m53s
csi-cephfsplugin-provisioner-974b566d9-wgx4l    4/4     Running     0          4m53s
csi-cephfsplugin-rpg6v                          3/3     Running     0          4m53s
csi-rbdplugin-4pvjd                             3/3     Running     0          4m53s
csi-rbdplugin-hw59q                             3/3     Running     0          4m53s
csi-rbdplugin-mrq2g                             3/3     Running     0          4m53s
csi-rbdplugin-provisioner-579c546f5-k4x78       5/5     Running     0          4m53s
csi-rbdplugin-provisioner-579c546f5-tlb8s       5/5     Running     0          4m53s
rook-ceph-mgr-a-8656964c64-d6rxd                1/1     Running     0          2m50s
rook-ceph-mon-a-5964fd74f7-fpqhl                1/1     Running     0          4m3s
rook-ceph-mon-b-858958c794-mwfzz                1/1     Running     0          3m36s
rook-ceph-mon-c-576b4d7687-94ts9                1/1     Running     0          3m14s
rook-ceph-operator-fb8b96548-f5dl5              1/1     Running     0          5m1s
rook-ceph-osd-0-66db6b5bbc-6v82x                1/1     Running     0          59s
rook-ceph-osd-1-7b76f55b96-bqknl                1/1     Running     0          49s
rook-ceph-osd-2-57f696464b-jlzjr                1/1     Running     0          74s
rook-ceph-osd-3-7b975f57c7-6snsr                1/1     Running     0          78s
rook-ceph-osd-4-5d485c8997-8fq7r                1/1     Running     0          75s
rook-ceph-osd-5-6f4f87db77-b52z6                1/1     Running     0          43s
rook-ceph-osd-6-6999896cdf-7d4mv                1/1     Running     0          26s
rook-ceph-osd-7-c66d49fcb-5lrwr                 1/1     Running     0          29s
rook-ceph-osd-8-6bdf79c57c-prnzq                1/1     Running     0          28s
rook-ceph-osd-prepare-set1-0-data-bhmrj-rjbnk   0/1     Completed   0          2m22s
rook-ceph-osd-prepare-set1-1-data-dcr5r-gcdl8   0/1     Completed   0          2m21s
rook-ceph-osd-prepare-set1-2-data-klbr2-bnlqv   0/1     Completed   0          2m21s
rook-ceph-osd-prepare-set1-3-data-7b2pk-7n6wl   0/1     Completed   0          2m21s
rook-ceph-osd-prepare-set1-4-data-249gz-b2z79   0/1     Completed   0          2m20s
rook-ceph-osd-prepare-set1-5-data-znf49-l7xbz   0/1     Completed   0          2m19s
rook-ceph-osd-prepare-set1-6-data-57bft-qtnwt   0/1     Completed   0          2m19s
rook-ceph-osd-prepare-set1-7-data-sm244-v546c   0/1     Completed   0          2m18s
rook-ceph-osd-prepare-set1-8-data-cd5hb-zt2gk   0/1     Completed   0          2m18s
rook-discover-5jzcr                             1/1     Running     0          5m
rook-discover-glvgr                             1/1     Running     0          5m
rook-discover-jh9pr                             1/1     Running     0          5m

おっ、イケるやん。worker nodeにあらかじめブロックデバイスをつけることなくCephクラスタが作れました。

[utubo@tutsunom ceph]$ kubectl -n rook-ceph get pod -l app=rook-ceph-mon -o wide
NAME                               READY   STATUS    RESTARTS   AGE     IP             NODE                            NOMINATED NODE   READINESS GATES
rook-ceph-mon-a-5964fd74f7-fpqhl   1/1     Running   0          4m56s   100.96.6.128   ip-172-20-93-28.ec2.internal    <none>           <none>
rook-ceph-mon-b-858958c794-mwfzz   1/1     Running   0          4m29s   100.96.7.198   ip-172-20-42-193.ec2.internal   <none>           <none>
rook-ceph-mon-c-576b4d7687-94ts9   1/1     Running   0          4m7s    100.96.8.117   ip-172-20-102-19.ec2.internal   <none>           <none>

MONはしっかり3ノードに分かれています。OSDは、

[utubo@tutsunom ceph]$ kubectl -n rook-ceph get pod -l app=rook-ceph-osd -o wide
NAME                               READY   STATUS    RESTARTS   AGE     IP             NODE                            NOMINATED NODE   READINESS GATES
rook-ceph-osd-0-66db6b5bbc-6v82x   1/1     Running   0          119s    100.96.6.136   ip-172-20-93-28.ec2.internal    <none>           <none>
rook-ceph-osd-1-7b76f55b96-bqknl   1/1     Running   0          109s    100.96.6.137   ip-172-20-93-28.ec2.internal    <none>           <none>
rook-ceph-osd-2-57f696464b-jlzjr   1/1     Running   0          2m14s   100.96.7.204   ip-172-20-42-193.ec2.internal   <none>           <none>
rook-ceph-osd-3-7b975f57c7-6snsr   1/1     Running   0          2m18s   100.96.7.202   ip-172-20-42-193.ec2.internal   <none>           <none>
rook-ceph-osd-4-5d485c8997-8fq7r   1/1     Running   0          2m15s   100.96.7.203   ip-172-20-42-193.ec2.internal   <none>           <none>
rook-ceph-osd-5-6f4f87db77-b52z6   1/1     Running   0          103s    100.96.6.138   ip-172-20-93-28.ec2.internal    <none>           <none>
rook-ceph-osd-6-6999896cdf-7d4mv   1/1     Running   0          86s     100.96.6.141   ip-172-20-93-28.ec2.internal    <none>           <none>
rook-ceph-osd-7-c66d49fcb-5lrwr    1/1     Running   0          89s     100.96.6.139   ip-172-20-93-28.ec2.internal    <none>           <none>
rook-ceph-osd-8-6bdf79c57c-prnzq   1/1     Running   0          88s     100.96.6.140   ip-172-20-93-28.ec2.internal    <none>           <none>

ええー!osd全然3ノードにバラけてないやん!!一応Cephで見てみると…

[utubo@tutsunom ceph]$ kubectl create -f toolbox.yaml 
[utubo@tutsunom ceph]$  kubectl -n rook-ceph exec -it `kubectl get pod -l app=rook-ceph-tools -o 'jsonpath={.items[].metadata.name}'` ceph osd tree
ID  CLASS WEIGHT  TYPE NAME                          STATUS REWEIGHT PRI-AFF 
 -1       0.07910 root default                                               
 -5       0.07910     region us-east-1                                       
 -4       0.02637         zone us-east-1a                                    
 -9       0.00879             host set1-0-data-bhmrj                         
  4   ssd 0.00879                 osd.4                  up  1.00000 1.00000 
-11       0.00879             host set1-2-data-klbr2                         
  2   ssd 0.00879                 osd.2                  up  1.00000 1.00000 
 -3       0.00879             host set1-6-data-57bft                         
  3   ssd 0.00879                 osd.3                  up  1.00000 1.00000 
-14       0.05273         zone us-east-1b                                    
-13       0.00879             host set1-1-data-dcr5r                         
  0   ssd 0.00879                 osd.0                  up  1.00000 1.00000 
-19       0.00879             host set1-3-data-7b2pk                         
  5   ssd 0.00879                 osd.5                  up  1.00000 1.00000 
-17       0.00879             host set1-4-data-249gz                         
  1   ssd 0.00879                 osd.1                  up  1.00000 1.00000 
-23       0.00879             host set1-5-data-znf49                         
  6   ssd 0.00879                 osd.6                  up  1.00000 1.00000 
-21       0.00879             host set1-7-data-sm244                         
  8   ssd 0.00879                 osd.8                  up  1.00000 1.00000 
-25       0.00879             host set1-8-data-cd5hb                         
  7   ssd 0.00879                 osd.7                  up  1.00000 1.00000 

(´・ω・`)
yamlosd Affinity ruleを見ると、

      placement:
        podAntiAffinity:
          preferredDuringSchedulingIgnoredDuringExecution:
          - weight: 100
            podAffinityTerm:
              labelSelector:
                matchExpressions:
                - key: app
                  operator: In
                  values:
                  - rook-ceph-osd
                - key: app
                  operator: In
                  values:
                  - rook-ceph-osd-prepare
              topologyKey: kubernetes.io/hostname

うーん…確かに3osdより多くなるとうまく散らばらなそうやけど3nodeにすら散らばらんのはどうなっとるんや…

実はコレ想定内の問題なんです。サンプルのcluster-on-pvc.yamlで使われているpodAntiAffinity ruleではうまく散らばらないのは織り込み済みです。yamlのコメントをよく読んでみると次のことが書かれています。

      # Since the OSDs could end up on any node, an effort needs to be made to spread the OSDs
      # across nodes as much as possible. Unfortunately the pod anti-affinity breaks down
      # as soon as you have more than one OSD per node. If you have more OSDs than nodes, K8s may
      # choose to schedule many of them on the same node. What we need is the Pod Topology
      # Spread Constraints, which is alpha in K8s 1.16. This means that a feature gate must be
      # enabled for this feature, and Rook also still needs to add support for this feature.
      # Another approach for a small number of OSDs is to create a separate device set for each
      # zone (or other set of nodes with a common label) so that the OSDs will end up on different

なるほど。要約すると、

  • いまのPod Anti-Affinityだと上手くosdをバラけさせられない。まだ努力が必要やわ。
  • Pod Topology Spread Constraints(k8s 1.16 alpha)を使えばピチッとバラけさせられるけど、Rookがまだ対応してないねん。これからの子やねん。
  • Node Affinityも使った組み合わせたらイケるかも。それでどないや?

ということです。そうか、この分野ではRookはまだまだこれからの子なんか。それならしゃーない。

そういうわけで、今の時点ではマニュアルで3nodeの散らばるように書く必要がありそうですね。

まとめ

今回はOSD on PVCのコンセプトの紹介をし、実際にやってみて問題に直面しました。
この悲しき現実に立ち向かう方法たるやいかに。次回バジリスク甲賀忍法帖…というノリはやめといて、次回はこの問題を回避する方法を紹介します。

BGMは甲賀忍法帖でした。(やっぱり)