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

Rook-Ceph OSD on PVC(後半)

f:id:ututaq:20191125022735p:plain

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

昨日OSD on PVCのCephクラスタをデプロイできたのはいいものの、OSDがうまく散らばってくれないという問題に直面しました。
しかも織り込み済みの問題だという。今回はこの問題を回避する方法の一例を紹介します。

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

3nodeに散らばるようにする方法

Kubernetesにまかせてカッコよくosdを散らすのは難しそうなので、マニュアルで散らせます。
色々失敗した検討した結果、こんな感じでNode Affinityを使ったら3osdずつ散らばりました。

[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: set-1a
      count: 3
      portable: true
      placement:
        nodeAffinity:
          requiredDuringSchedulingIgnoredDuringExecution:
            nodeSelectorTerms:
            - matchExpressions:
              - key: failure-domain.beta.kubernetes.io/zone
                operator: In
                values:
                  - us-east-1a
      resources:
      volumeClaimTemplates:
      - metadata:
          name: data
        spec:
          resources:
            requests:
              storage: 10Gi
          storageClassName: my-gp2
          volumeMode: Block
          accessModes:
            - ReadWriteOnce
    - name: set-1b
      count: 3
      portable: true
      placement:
        nodeAffinity:
          requiredDuringSchedulingIgnoredDuringExecution:
            nodeSelectorTerms:
            - matchExpressions:
              - key: failure-domain.beta.kubernetes.io/zone
                operator: In
                values:
                  - us-east-1b
      resources:
      volumeClaimTemplates:
      - metadata:
          name: data
        spec:
          resources:
            requests:
              storage: 10Gi
          storageClassName: my-gp2
          volumeMode: Block
          accessModes:
            - ReadWriteOnce
    - name: set-1c
      count: 3
      portable: true
      placement:
        nodeAffinity:
          requiredDuringSchedulingIgnoredDuringExecution:
            nodeSelectorTerms:
            - matchExpressions:
              - key: failure-domain.beta.kubernetes.io/zone
                operator: In
                values:
                  - us-east-1c
      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

どうですか。ダサい、ダサすぎる。と風が語りかけてくるようです。

恥ずかしい思いをグッと我慢して解説すると、storageClassDeviceSets:OSDのセットを指定します。
サンプルのyamlだと、9個のOSDを1つのセットにするからPod AntiAffinity ruleでうまく分けろって言っているのです。
しかし、9個の唐揚げを1枚の皿に乗せて3人の子供達に仲良く分けて食えと言ってもうまくいかないように、(今のルールだと)意図するように分けるのはなかなか難しい。
そこで3個のOSDのセットを3つ用意してそれぞれのzoneにあるworker nodeに分けさせるruleにしてるのが、上のyamlです。
3枚の皿に3個ずつ唐揚げを乗せて1皿ずつ取れって形にしたら話早いやん、という感じです。

めっちゃhard codedなんで結局これかい感満々ですが、これでもう一回クラスタをデプロイしてみましょう。

[utubo@tutsunom ceph]$ kubectl create -f my-cluster-on-pvc.yaml
[utubo@tutsunom ceph]$ kubectl -n rook-ceph get pod
NAME                                              READY   STATUS      RESTARTS   AGE
csi-cephfsplugin-5cp9g                            3/3     Running     0          5m44s
csi-cephfsplugin-fhr5b                            3/3     Running     0          5m44s
csi-cephfsplugin-l6556                            3/3     Running     0          5m44s
csi-cephfsplugin-provisioner-974b566d9-frwz6      4/4     Running     0          5m44s
csi-cephfsplugin-provisioner-974b566d9-tqswk      4/4     Running     0          5m44s
csi-rbdplugin-4qxgj                               3/3     Running     0          5m44s
csi-rbdplugin-crc8t                               3/3     Running     0          5m44s
csi-rbdplugin-h5dkw                               3/3     Running     0          5m44s
csi-rbdplugin-provisioner-579c546f5-sfx72         5/5     Running     0          5m44s
csi-rbdplugin-provisioner-579c546f5-z7rmk         5/5     Running     0          5m44s
rook-ceph-mgr-a-7cf896f748-m7czk                  1/1     Running     0          3m37s
rook-ceph-mon-a-568dc96b5f-xl9x4                  1/1     Running     0          4m49s
rook-ceph-mon-b-5cbc9dcd9d-gxnjp                  1/1     Running     0          4m32s
rook-ceph-mon-c-647db6d4b5-wmkzg                  1/1     Running     0          4m1s
rook-ceph-operator-fb8b96548-n9jv6                1/1     Running     0          5m52s
rook-ceph-osd-0-8cbc88874-k6dlj                   1/1     Running     0          2m2s
rook-ceph-osd-1-5b746b478f-fzs8z                  1/1     Running     0          2m3s
rook-ceph-osd-2-64c746c558-mwzn2                  1/1     Running     0          2m7s
rook-ceph-osd-3-86f8984495-b7b7n                  1/1     Running     0          2m5s
rook-ceph-osd-4-67889676b9-wjswz                  1/1     Running     0          2m2s
rook-ceph-osd-5-687847c9f-6z7ps                   1/1     Running     0          2m3s
rook-ceph-osd-6-f8997d5b7-bmblb                   1/1     Running     0          114s
rook-ceph-osd-7-65dc45747c-fckw7                  1/1     Running     0          111s
rook-ceph-osd-8-76bf67bcf8-s9vmm                  1/1     Running     0          114s
rook-ceph-osd-prepare-set-1a-0-data-kx7k4-n4wrn   0/1     Completed   0          3m10s
rook-ceph-osd-prepare-set-1a-1-data-kdwjp-n4fm9   0/1     Completed   0          3m9s
rook-ceph-osd-prepare-set-1a-2-data-j5fm6-7vjwg   0/1     Completed   0          3m8s
rook-ceph-osd-prepare-set-1b-0-data-tsv5x-mcx9q   0/1     Completed   0          3m8s
rook-ceph-osd-prepare-set-1b-1-data-gdpqg-9knxk   0/1     Completed   0          3m7s
rook-ceph-osd-prepare-set-1b-2-data-jfwcn-d67gp   0/1     Completed   0          3m7s
rook-ceph-osd-prepare-set-1c-0-data-9tcl4-87946   0/1     Completed   0          3m6s
rook-ceph-osd-prepare-set-1c-1-data-h4wk7-c9d76   0/1     Completed   0          3m5s
rook-ceph-osd-prepare-set-1c-2-data-svq64-9sdzb   0/1     Completed   0          3m5s
rook-discover-fmprp                               1/1     Running     0          5m51s
rook-discover-m895f                               1/1     Running     0          5m51s
rook-discover-wnpj7                               1/1     Running     0          5m51s

ここまではええねん。MONとOSDは?

[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-568dc96b5f-xl9x4   1/1     Running   0          5m19s   100.96.6.111   ip-172-20-93-28.ec2.internal    <none>           <none>
rook-ceph-mon-b-5cbc9dcd9d-gxnjp   1/1     Running   0          5m2s    100.96.7.178   ip-172-20-42-193.ec2.internal   <none>           <none>
rook-ceph-mon-c-647db6d4b5-wmkzg   1/1     Running   0          4m31s   100.96.8.99    ip-172-20-102-19.ec2.internal   <none>           <none>
[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-8cbc88874-k6dlj    1/1     Running   0          2m37s   100.96.6.118   ip-172-20-93-28.ec2.internal    <none>           <none>
rook-ceph-osd-1-5b746b478f-fzs8z   1/1     Running   0          2m38s   100.96.8.104   ip-172-20-102-19.ec2.internal   <none>           <none>
rook-ceph-osd-2-64c746c558-mwzn2   1/1     Running   0          2m42s   100.96.8.103   ip-172-20-102-19.ec2.internal   <none>           <none>
rook-ceph-osd-3-86f8984495-b7b7n   1/1     Running   0          2m40s   100.96.6.116   ip-172-20-93-28.ec2.internal    <none>           <none>
rook-ceph-osd-4-67889676b9-wjswz   1/1     Running   0          2m37s   100.96.6.117   ip-172-20-93-28.ec2.internal    <none>           <none>
rook-ceph-osd-5-687847c9f-6z7ps    1/1     Running   0          2m38s   100.96.7.182   ip-172-20-42-193.ec2.internal   <none>           <none>
rook-ceph-osd-6-f8997d5b7-bmblb    1/1     Running   0          2m29s   100.96.8.105   ip-172-20-102-19.ec2.internal   <none>           <none>
rook-ceph-osd-7-65dc45747c-fckw7   1/1     Running   0          2m26s   100.96.7.184   ip-172-20-42-193.ec2.internal   <none>           <none>
rook-ceph-osd-8-76bf67bcf8-s9vmm   1/1     Running   0          2m29s   100.96.7.183   ip-172-20-42-193.ec2.internal   <none>           <none>

お、3個ずつ散らばってますね。(そらそうやろ

Cephの観点で見た注意点

ここでちょっと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                                         
-14       0.02637         zone us-east-1a                                      
-13       0.00879             host set-1a-0-data-kx7k4                         
  5   ssd 0.00879                 osd.5                    up  1.00000 1.00000 
-27       0.00879             host set-1a-1-data-kdwjp                         
  7   ssd 0.00879                 osd.7                    up  1.00000 1.00000 
-23       0.00879             host set-1a-2-data-j5fm6                         
  8   ssd 0.00879                 osd.8                    up  1.00000 1.00000 
-10       0.02637         zone us-east-1b                                      
 -9       0.00879             host set-1b-0-data-tsv5x                         
  3   ssd 0.00879                 osd.3                    up  1.00000 1.00000 
-17       0.00879             host set-1b-1-data-gdpqg                         
  0   ssd 0.00879                 osd.0                    up  1.00000 1.00000 
-19       0.00879             host set-1b-2-data-jfwcn                         
  4   ssd 0.00879                 osd.4                    up  1.00000 1.00000 
 -4       0.02637         zone us-east-1c                                      
 -3       0.00879             host set-1c-0-data-9tcl4                         
  2   ssd 0.00879                 osd.2                    up  1.00000 1.00000 
-25       0.00879             host set-1c-1-data-h4wk7                         
  6   ssd 0.00879                 osd.6                    up  1.00000 1.00000 
-21       0.00879             host set-1c-2-data-svq64                         
  1   ssd 0.00879                 osd.1                    up  1.00000 1.00000 

なんか3つずつに分かれて良さそうに見えますね。しかしこのトポロジーではCephのデフォルトのCRUSH ruleを使う場合に落とし穴があります。
各zoneの中に3つのhostがありますね。この3つのhostは実際は同一のworker nodeなんですが、Cephとしては異なるhostが3つあるトポロジーとして捉えます。そして各hostが1つosdを持ちます。

デフォルトのreplicated_ruleは「regionとかzoneとか無視して、とにかく1つhostを選んでその中のosdを1つ選ぶ」というルールになっています。

[utubo@tutsunom ceph]$ kubectl -n rook-ceph exec -it `kubectl get pod -l app=rook-ceph-tools -o 'jsonpath={.items[].metadata.name}'` sh
sh-4.2# ceph osd getcrushmap -o crushmap.bin
sh-4.2# crushtool -d crushmap.bin -o crushmap.txt 
sh-4.2# grep -A 10 rules crushmap.txt 
# rules
rule replicated_rule {
    id 0
    type replicated
    min_size 1
    max_size 10
    step take default
    step chooseleaf firstn 0 type host
    step emit
}

つまり、上記のようなトポロジーのCephクラスタでreplicated_ruleを使って3x Replicated Poolを作ると、同一のzoneから3つのOSDを選んだPGが作られる可能性があります。
実際やって見てみましょう。面倒なんでCephで直接コマンド打ってPoolを作ります。すんません。

[utubo@tutsunom ceph]$ kubectl -n rook-ceph exec -it `kubectl get pod -l app=rook-ceph-tools -o 'jsonpath={.items[].metadata.name}'` ceph osd pool create test 256 256
pool 'test' created
[utubo@tutsunom ceph]$ kubectl -n rook-ceph exec -it `kubectl get pod -l app=rook-ceph-tools -o 'jsonpath={.items[].metadata.name}'` ceph pg ls-by-pool test
PG   OBJECTS DEGRADED MISPLACED UNFOUND BYTES OMAP_BYTES* OMAP_KEYS* LOG STATE        SINCE VERSION REPORTED UP        ACTING    SCRUB_STAMP                DEEP_SCRUB_STAMP           
1.0        0        0         0       0     0           0          0   0 active+clean   26s     0'0    26:10 [0,1,2]p0 [0,1,2]p0 2019-12-05 03:57:01.395376 2019-12-05 03:57:01.395376 
1.1        0        0         0       0     0           0          0   0 active+clean   26s     0'0    26:10 [5,2,0]p5 [5,2,0]p5 2019-12-05 03:57:01.395376 2019-12-05 03:57:01.395376 
1.2        0        0         0       0     0           0          0   0 active+clean   26s     0'0    26:10 [4,1,2]p4 [4,1,2]p4 2019-12-05 03:57:01.395376 2019-12-05 03:57:01.395376 
1.3        0        0         0       0     0           0          0   0 active+clean   26s     0'0    26:10 [7,2,4]p7 [7,2,4]p7 2019-12-05 03:57:01.395376 2019-12-05 03:57:01.395376 
1.4        0        0         0       0     0           0          0   0 active+clean   26s     0'0    26:10 [7,4,2]p7 [7,4,2]p7 2019-12-05 03:57:01.395376 2019-12-05 03:57:01.395376 
1.5        0        0         0       0     0           0          0   0 active+clean   26s     0'0    26:10 [8,1,4]p8 [8,1,4]p8 2019-12-05 03:57:01.395376 2019-12-05 03:57:01.395376
...
1.34       0        0         0       0     0           0          0   0 active+clean   26s     0'0    26:10 [6,2,1]p6 [6,2,1]p6 2019-12-05 03:57:01.395376 2019-12-05 03:57:01.395376 
...
1.97       0        0         0       0     0           0          0   0 active+clean   26s     0'0    26:10 [5,7,8]p5 [5,7,8]p5 2019-12-05 03:57:01.395376 2019-12-05 03:57:01.395376 
...
1.c2       0        0         0       0     0           0          0   0 active+clean   26s     0'0    26:10 [0,3,4]p0 [0,3,4]p0 2019-12-05 03:57:01.395376 2019-12-05 03:57:01.395376 
...

ceph osd pool createコマンドでruleを指定せずにpoolを作るとデフォルトのreplicated_ruleが使われるのですが、どうでしょう。
1.34に割り当てられたOSD[6,2,1]は、ceph osd treeを見ると3つともzone us-east-1cから取られています。が、実際は1つのworker nodeにattachされてるOSDです。1.971.c2も同様です。
つまりworker nodeに障害が起きるとこれらのPGは完全にアウトとなり、データロスとなります。全然冗長化されてないpoolなので、これはアカン。

だから何かしら対応しなくてはいけません。
トポロジーを変えるのはRook-Ceph側の扱いになって難しそうなので、適切なCRUSH ruleを作って指定する方がよいでしょう。例えばこんな感じのruleを作ったらいけるはずです。

# rules
rule spread_across_all_zones {
        ruleset 1
        type replicated
        min_size 1
        max_size 10
        step take default
        step choose firstn 0 type zone
        step chooseleaf firstn 1 type host
        step emit
}

まとめ

前後半にわたって、OSD on PVCについて説明しました。worker nodeにブロックデバイスをぶら下げる作業がしなくていいのでKubernetes側でデバイス管理でき、Cloud Native Infra感出ますよね。本当に便利だと思います。
でも個人的にOSD on PVCで一番うれしいのは、Cephクラスタを作り直すたびにworker nodeに入ってMONが使うCephの構成ファイル(/var/lib/rook以下)をいちいち消さなくてよかったり、OSDになるデバイスをきれいに消してLVMのエントリーを消したりしなくていいところだったりします。クラスタ消したら全部自動的にPVCも消えてきれいさっぱり無くなるからです。本番のKubernetesクラスタでは全然関係ないけれど、テストする上ではスムーズにクラスタのdelete/createが繰り返しができるので、ホントーーーーにありがたい。

OSDをきれいに散らす方法は、Kubernetes一年生のうつぼではNode Affinityを使ったダサい方法しか思いつかなかったけど、やっぱりPod (Anti)Affinityをうまく使ったやりかたがあるんでしょうね。それかPod Topology Spread Constraintsを待つんでしょう。
こちらのissueを見ると、Rook 1.2にはPod Topology Spread Constraintsに対応するとのことです。
Pod Topology Spread Constraintsについてはsatさんが追究されています。やはり今日本で一番のRook-Cepherだと思います。最敬礼。

blog.cybozu.io

というわけで、今回はおしまい。

BGMは境界の彼方でした。