Rook-Ceph RBD の failover 実験

## TL;DR
ある Kubernetes Node に障害が発生した際、Non-Graceful Node Shutdown により StatefulSet (Pod + RBD PV) が別の正常な Node に移される様子を確認する。

## 背景
Kubernetes では通常 、Node がダウンすると Pod は他の Node に再作成される。
しかし、StatefulSet の場合はそのような failover がうまく機能せず、Podが "Terminating" 状態のまま残り、新しい Pod が再作成されることがない。

この問題を解決するため、Kubernetes v1.28 からは Non-Graceful Node Shutdown 機能が安定版として実装された。この機能では、障害が発生した Node に `out-of-service` taints を付与することで、その Node から別の Node に StatefulSet を再作成して再稼働させることができるようになる。

Kubernetes の Non-Graceful Node Shutdown のフローについては外部リンクの記事が詳しい：

https://jpn.nec.com/oss/community/blog/kubernetes-failover-improvement-non-graceful-node-shutdown.html

また、Rook-Ceph においてはこの機能を前提として、Node 上の `out-of-service` taint を検知し、Node の Network Fencing および RBD ボリュームのデタッチを自動で行う機能が実装されている。

仕様は下記のリンクを参照：

https://github.com/rook/rook/blob/master/design/ceph/node-loss-rbd-cephfs.md

以上の機能により、Node が障害によってダウンした場合にも RBD PV + Pod の StatefulSet が別の正常なNode 上に移動される。

本記事では、`out-of-service` taint の付与によって Non-Graceful Node Shutdown をトリガーし、Rook-Ceph の RBD が failover される様子を確認する。


## Failover の実験

### 前提条件
* Kubernetes Cluster は、3-node 構成の [OKD4](https://www.okd.io/) cluster を用いる。各 Node は master と worker 両方の role を持つ。
* Rook-Ceph 環境は　OpenShift Data Foundation に組み込まれたものを用いる。
* Medik8s 系統の Operator は未導入である。

```
$ oc get node
NAME      STATUS   ROLES                         AGE   VERSION
master0   Ready    control-plane,master,worker   9h    v1.28.7+6e2789b
master1   Ready    control-plane,master,worker   10h   v1.28.7+6e2789b
master2   Ready    control-plane,master,worker   10h   v1.28.7+6e2789b
```

### 事前準備

#### StatefulSet の作成

まずは次の manifest より StatefulSet を作成する：
```test-sts.yaml
apiVersion: apps/v1
kind: StatefulSet
metadata:
  name: test-sts
spec:
  serviceName: "nginx"
  replicas: 1
  selector:
    matchLabels:
      app: nginx
  template:
    metadata:
      labels:
        app: nginx
    spec:
      containers:
      - name: nginx
        image: nginx:latest
        volumeMounts:
        - name: www
          mountPath: /usr/share/nginx/html
  volumeClaimTemplates:
  - metadata:
      name: www
    spec:
      accessModes: ["ReadWriteOnce"]
      resources:
        requests:
          storage: 1Gi
      storageClassName: "ocs-storagecluster-ceph-rbd"
```

リソースを作成する：
```
$ oc apply -f test-sts.yaml 
statefulset.apps/test-sts created
```

StatefulSet が作成されたことを確認する：
```
$ oc get sts
NAME       READY   AGE
test-sts   1/1     31s
```

Pod が稼働していることを確認する：

```
$ oc get pod -o wide          
NAME         READY   STATUS    RESTARTS   AGE   IP            NODE      NOMINATED NODE   READINESS GATES          
test-sts-0   1/1     Running   0          36s   10.130.0.28   master0   <none>           <none>      
```

PVC がBound されていることを確認する：
```
$ oc get pvc
NAME             STATUS   VOLUME                                     CAPACITY   ACCESS MODES   STORAGECLASS                  AGE
www-test-sts-0   Bound    pvc-fb4be4f3-b0db-4db2-b981-bfb0a81262a1   1Gi        RWO            ocs-storagecluster-ceph-rbd   40m
```

作成された PV を確認する：

```
$ oc get pv
NAME                                       CAPACITY   ACCESS MODES   RECLAIM POLICY   STATUS   CLAIM                                                      STORAGECLASS                  REASON   AGE
pvc-fb4be4f3-b0db-4db2-b981-bfb0a81262a1   1Gi        RWO            Delete           Bound    default/www-test-sts-0                                     ocs-storagecluster-ceph-rbd            40m
```

#### RBD の確認
次に、PV に対応する RBD がどのNode 上に存在するか確認する。
これには、次のスクリプト：`find_rbd.sh` を作成すると便利である。
これは、PV 名を与えると RBD デバイスが存在する Node 名を表示するものである：
```bash:find_rbd.sh
#!/bin/bash

usage()
{
  echo "Usage:"
  echo "  ./find_rbd.sh <pvname>"
}

# Parse arguments
if [ "$#" -ne 1 ]; then
    usage
    exit 1
fi

pvname=$1
nodes=$(oc get nodes --no-headers)

while read -r line; do
  node=$(echo "$line" | awk '{print $1}')
  status=$(echo "$line" | awk '{print $2}')
  if [ "$status" != "Ready" ]; then
    continue
  fi

  lsblk_result=$(oc debug node/${node} -- chroot /host lsblk -d 2> /dev/null)
  echo "${lsblk_result}" | grep "/${pvname}/" > /dev/null
  if [[ $? -eq 0 ]]; then
    echo "An RBD found on $node:"
    # Output until the indentation ends
    echo "${lsblk_result}" | awk -v pvname="${pvname}" '
      {
        if (found) {
          if ($0 ~ /^ {32}/) {
            print $0
          } else {
            found = 0
          }
        }
        if ($0 ~ pvname) {
          print $0
          found = 1
        }
      }'
    exit 0
  fi
done <<< "$nodes"

echo RBD not found.
exit 1
```

これを利用して、PV に対応する RBD が存在する Node を特定する：
```
$ ./find_rbd.sh pvc-fb4be4f3-b0db-4db2-b981-bfb0a81262a1
An RBD found on master0:
rbd0  251:0    0     1G  0 disk /var/lib/kubelet/pods/5b14419a-3a70-405b-b3ab-acc9b4c3a741/volumes/kubernetes.io~csi/pvc-fb4be4f3-b0db-4db2-b981-bfb0a81262a1/mount
                                /var/lib/kubelet/plugins/kubernetes.io/csi/openshift-storage.rbd.csi.ceph.com/eb0b839f0a984f3d244bb6ecb4d6bf909cd134af1f0cfc3e3c1d3fb26baf750b/globalmount/0001-0011-openshift-storage-0000000000000001-62513b81-263e-486e-aed3-623d41d3c40d
```

Node `master0` 上に `/dev/rbd0` として存在していることが確認できた。

#### Node に taints がないことの確認
すべての Nodes において、 `taints : null` になっていることを確認する。

```
$ oc get nodes -o json | jq '.items[] | {name: .metadata.name, taints: .spec.taints}'
{
  "name": "master0",
  "taints": null
}
{
  "name": "master1",
  "taints": null
}
{
  "name": "master2",
  "taints": null
}
```

### 実験1: Node を強制電源OFF + 手動で taints 付与

準備段階で対象の RBD が `master0` 上にあることがわかったので、この Node を強制的に電源OFFにする。

次に `master0` に `out-of-service` Taint を付与する：

```
oc adm taint nodes master0 node.kubernetes.io/out-of-service=nodeshutdown:NoExecute
oc adm taint nodes master0 node.kubernetes.io/out-of-service=nodeshutdown:NoSchedule
```

`master 0` に `out-of-service` taints が付与されたことを確認する：
```
$ oc get nodes -o json | jq '.items[] | {name: .metadata.name, taints: .spec.taints}'
{
  "name": "master0",
  "taints": [
    {
      "effect": "NoExecute",
      "key": "node.kubernetes.io/out-of-service",
      "value": "nodeshutdown"
    },
    {
      "effect": "NoSchedule",
      "key": "node.kubernetes.io/out-of-service",
      "value": "nodeshutdown"
    }
  ]
}
{
  "name": "master1",
  "taints": null
}
{
  "name": "master2",
  "taints": null
}
```

:::note
Node に障害が発生して 40 秒経つと、Kubernetes 側は Node の State を `NotReady` すると同時に、`unreachable` taints を付与してくる。そうすると出力結果は次のようになる。
```
{
  "name": "master0",
  "taints": [
    {
      "effect": "NoSchedule",
      "key": "node.kubernetes.io/out-of-service",
      "value": "nodeshutdown"
    },
    {
      "effect": "NoExecute",
      "key": "node.kubernetes.io/out-of-service",
      "value": "nodeshutdown"
    },
    {
      "effect": "NoSchedule",
      "key": "node.kubernetes.io/unreachable",
      "timeAdded": "2024-06-26T05:49:58Z"
    },
    {
      "effect": "NoExecute",
      "key": "node.kubernetes.io/unreachable",
      "timeAdded": "2024-06-26T05:50:04Z"
    }
  ]
}
{
  "name": "master1",
  "taints": null
}
{
  "name": "master2",
  "taints": null
}
```
:::


Pod の稼働状況を確認する。以下は、10秒おきに `oc get pod` を実行したログのうち、failover が起こった部分を抜粋したものである。
`master0` 上の pod が削除され、`master2` に再作成される様子が確認できる。
```
$ date
Wed Jun 26 03:14:12 PM JST 2024
$ oc get pod
NAME         READY   STATUS    RESTARTS   AGE     IP            NODE      NOMINATED NODE   READINESS GATES
test-sts-0   1/1     Running   0          3m48s   10.130.0.28   master0   <none>           <none>

$ date
Wed Jun 26 03:14:22 PM JST 2024
$ oc get pod
NAME         READY   STATUS        RESTARTS   AGE     IP            NODE      NOMINATED 
NODE   READINESS GATES
test-sts-0   1/1     Terminating   0          3m58s   10.130.0.28   master0   <none>           <none>

$ date
Wed Jun 26 03:14:32 PM JST 2024
$ oc get pod
NAME         READY   STATUS              RESTARTS   AGE   IP       NODE      NOMINATED NODE   READINESS GATES
test-sts-0   0/1     ContainerCreating   0          8s    <none>   master2   <none>           <none>

$ date
Wed Jun 26 03:14:43 PM JST 2024
$ oc get pod
NAME         READY   STATUS    RESTARTS   AGE   IP            NODE      NOMINATED NODE   READINESS GATES 
test-sts-0   1/1     Running   0          18s   10.129.0.32   master2   <none>           <none>
```

RBD を調べてみると、Node `master2` に移動していることが確認できる。
```
$ ./find_rbd.sh pvc-fb4be4f3-b0db-4db2-b981-bfb0a81262a1
An RBD found on master2:
rbd1  251:16   0     1G  0 disk /var/lib/kubelet/pods/37784274-29f0-43df-9a3e-4cd08da3a738/volumes/kubernetes.io~csi/pvc-fb4be4f3-b0db-4db2-b981-bfb0a81262a1/mount
                                /var/lib/kubelet/plugins/kubernetes.io/csi/openshift-storage.rbd.csi.ceph.com/eb0b839f0a984f3d244bb6ecb4d6bf909cd134af1f0cfc3e3c1d3fb26baf750b/globalmount/0001-0011-openshift-storage-0000000000000001-62513b81-263e-486e-aed3-623d41d3c40d
```

これで、Node 障害に対して Pod + RBD PV の StatefulSet が failover できていることを確認できた。

後始末として StatefulSet を削除する。
```
$ oc delete -f test-sts.yaml 
statefulset.apps "test-sts" deleted
```

Node `master0` の電源を ON にし、taints を削除する：

```
oc adm taint nodes master0 node.kubernetes.io/out-of-service-
```

### 実験2: Node を稼働させたまま taints だけ付与
今度は Node をシャットダウンせず、`out-of-service` taints だけ付与した場合はどうなるのだろうか。

再度、[事前準備](https://qiita.com/drafts/41742227cfa4ad35a444/edit#%E4%BA%8B%E5%89%8D%E6%BA%96%E5%82%99)を行い、`master0` 上に RBD が作成されている状況を作ってみる。

そして、Node の電源を落とさず、`master0` に `out-of-service` taints を付与してみる：
```
oc adm taint nodes master0 node.kubernetes.io/out-of-service=nodeshutdown:NoExecute
oc adm taint nodes master0 node.kubernetes.io/out-of-service=nodeshutdown:NoSchedule
```

以下に Pod の状態を 10秒おきに取得したログを貼る。
あるタイミングで Terminating に変化した後は、膠着してしまい Pod が再作成される気配はない。

```
$date
Wed Jun 26 04:05:09 PM JST 2024
$ oc get pods -o wide
NAME         READY   STATUS    RESTARTS   AGE    IP            NODE      NOMINATED NODE   READINESS GATES
test-sts-0   1/1     Running   0          3m2s   10.130.0.33   master0   <none>           <none>

$ date
Wed Jun 26 04:05:19 PM JST 2024
$ oc get pods -o wide
NAME         READY   STATUS        RESTARTS   AGE     IP            NODE      NOMINATED NODE   READINESS GATES
test-sts-0   0/1     Terminating   0          3m12s   10.130.0.33   master0   <none>           <none>

$ date
Wed Jun 26 04:05:29 PM JST 2024
$ oc get pods -o wide
NAME         READY   STATUS        RESTARTS   AGE     IP            NODE      NOMINATED NODE   READINESS GATES
test-sts-0   0/1     Terminating   0          3m22s   10.130.0.33   master0   <none>           <none>

$ date
Wed Jun 26 04:05:39 PM JST 2024
$ oc get pods -o wide
NAME         READY   STATUS        RESTARTS   AGE     IP            NODE      NOMINATED NODE   READINESS GATES
test-sts-0   0/1     Terminating   0          3m32s   10.130.0.33   master0   <none>           <none>

...
...
```

Rook 側の処理はどうなったのだろうか？Pod `rook-ceph-operator` から `ceph-cluster-controller` に関するログを見てみる。

```
oc logs -f -n openshift-storage $(oc get pods -n openshift-storage -o custom-columns=:metadata.name | grep rook-ceph-operator) | grep "| ceph-cluster-controller:"
```

次のようなログが断続的に吐かれている：

```
2024-06-26 08:08:28.042556 I | ceph-cluster-controller: node "master0" require fencing, found rbd volumes in use
2024-06-26 08:08:28.513866 W | ceph-cluster-controller: Blocking node IP [100.64.0.4/32]
2024-06-26 08:08:28.526738 I | ceph-cluster-controller: successfully created network fence CR for node "master0"
2024-06-26 08:13:12.613655 I | ceph-cluster-controller: Found taint: Key=node.kubernetes.io/out-of-service, Value=nodeshutdown on node master0
2024-06-26 08:13:12.613676 I | ceph-cluster-controller: volumeInUse after split based on '^' [openshift-storage.rbd.csi.ceph.com 0001-0011-openshift-storage-0000000000000001-62513b81-263e-486e-aed3-623d41d3c40d]
```

該当箇所は次のリンク先のあたりで、 node fencing を実行する処理が書かれている。

https://github.com/rook/rook/issues/1507#issuecomment-1122965274

ログを見る限り、 `out-of-service` taints は認識されていており、Network Fence も作成されているようだ。そして、 RBD を探してみると、どの Node 上にも RBD デバイスが存在していない。
```
$ ./find_rbd.sh pvc-fb4be4f3-b0db-4db2-b981-bfb0a81262a1
RBD not found.
```

結果を見る限り Rook 側がやるべき RBD の処理は終わっているように見える。
単に Kubernetes 側が detach 処理をしていないだけならば、 pod を強制的に削除してみる。

```
kubectl delete pod test-sts-0 --force --grace-period=0
Warning: Immediate deletion does not wait for confirmation that the running resource has been terminated. The resource may continue to run on the cluster indefinitely.
pod "test-sts-0" force deleted
```

直後、動きがあり Pod が別 Node `master2` に再作成された。
```
...
...
$ date
Wed Jun 26 06:01:12 PM JST 2024
$ oc get pods -o wide
NAME         READY   STATUS        RESTARTS   AGE   IP             NODE      NOMINATED NODE   READINESS GATES
test-sts-0   0/1     Terminating   0          53m   10.130.1.188   master0   <none>           <none>

$ date
Wed Jun 26 06:01:32 PM JST 2024
$ oc get pods -o wide
NAME         READY   STATUS              RESTARTS   AGE   IP       NODE      NOMINATED NODE   READINESS GATES
test-sts-0   0/1     ContainerCreating   0          1s    <none>   master2   <none>           <none>

$ date
Wed Jun 26 06:01:42 PM JST 2024
$ oc get pods -o wide
NAME         READY   STATUS    RESTARTS   AGE   IP            NODE      NOMINATED NODE   READINESS GATES 
test-sts-0   1/1     Running   0          11s   10.129.0.83   master2   <none>           <none>
```

RBD も `master2` に再作成されている。
```
./find_rbd.sh pvc-fb4be4f3-b0db-4db2-b981-bfb0a81262a1 
An RBD found on master2:
rbd1  251:16   0     1G  0 disk /var/lib/kubelet/pods/e67dad97-bfdb-4edc-8acb-23ecf639a7f5/volumes/kubernetes.io~csi/pvc-fb4be4f3-b0db-4db2-b981-bfb0a81262a1/mount
                                /var/lib/kubelet/plugins/kubernetes.io/csi/openshift-storage.rbd.csi.ceph.com/eb0b839f0a984f3d244bb6ecb4d6bf909cd134af1f0cfc3e3c1d3fb26baf750b/globalmount/0001-0011-openshift-storage-0000000000000001-62513b81-263e-486e-aed3-623d41d3c40d
```

`out-of-service` を taints を削除してみる：

```
oc adm taint nodes master0 node.kubernetes.io/out-of-service-
```

すると Ceph Rook Operator の ログが動き、unFence でエラーが吐かれている。

```
2024-06-26 09:37:51.688020 I | ceph-cluster-controller: waiting for network fence CR "master0-rbd-openshift-storage" status to get result "unfencing operation successful"
2024-06-26 09:37:53.688976 I | ceph-cluster-controller: successfully unfenced "rbd" network fence cr "master0-rbd-openshift-storage", proceeding with deletion
2024-06-26 09:37:53.714553 I | ceph-cluster-controller: successfully deleted network fence CR master0-rbd-openshift-storage
2024-06-26 09:37:53.721795 I | ceph-cluster-controller: node master0 does not have taint node.kubernetes.io/out-of-service, unfencing networkFence CR
2024-06-26 09:37:53.727081 E | ceph-cluster-controller: failed to unFence network fence CR. Operation cannot be fulfilled on networkfences.csiaddons.openshift.io "master0-rbd-openshift-storage": StorageError: invalid object, Code: 4, Key: /kubernetes.io/csiaddons.openshift.io/networkfences/master0-rbd-openshift-storage, ResourceVersion: 0, AdditionalErrorMsg: Precondition failed: UID in precondition: 17773a8b-85b9-4263-be02-953df54bb4a5, UID in object meta: 
2024-06-26 09:37:53.727117 E | ceph-cluster-controller: failed to handle node failure. failed to delete rbd network fence for node "master0".: Operation cannot be fulfilled on networkfences.csiaddons.openshift.io "master0-rbd-openshift-storage": StorageError: invalid object, Code: 4, Key: /kubernetes.io/csiaddons.openshift.io/networkfences/master0-rbd-openshift-storage, ResourceVersion: 0, AdditionalErrorMsg: Precondition failed: UID in precondition: 17773a8b-85b9-4263-be02-953df54bb4a5, UID in object meta: 
```

しかし、NetworkFence の CR は削除されているようだ。
```
$ oc get networkfence
No resources found           
```

実験の意図としては、Node が稼働中でも Pod と RBD の failover を意図的にトリガーできるか試したかった。結果的にはできていそうだが、Rook 側が想定した操作ではない気がする。


## 最後に

Rook-Ceph における failover の issue は 2018 年から issue が上がってきて、その対処 [core: faster recovery from rbd rwo node loss by subhamkrai · Pull Request #12286 · rook/rook](https://github.com/rook/rook/pull/12286) がマージされたのが 2023年5月末。

https://github.com/rook/rook/issues/1507

Kubernetes の Non-Graceful Node Shutdown が stable 版としてとして GA されたのが v1.28 [^1]（2023年08月15日[^2]）。Kubernetes や Rook-Ceph は万能なイメージがあったが、実はこの類の Failover ができるようになってまだ1年ほどなのか。

Medik8s を用いれば障害発生時の Taints の付与も自動化できるようで、検証した結果は次の記事を参照：

https://qiita.com/sawa2d2/items/9b57b8c3a2d7f58e5173

これらの機能をうまく使うことでアプリケーション側の failover 実装の負担が軽くなりそう。

[^1]: [Kubernetes 1.28: Non-Graceful Node Shutdown Moves to GA | Kubernetes](https://kubernetes.io/blog/2023/08/16/kubernetes-1-28-non-graceful-node-shutdown-ga/)

[^2]: [Kubernetes v1.28: Planternetes | Kubernetes](https://kubernetes.io/blog/2023/08/15/kubernetes-v1-28-release/)