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 のフローについては外部リンクの記事が詳しい:
また、Rook-Ceph においてはこの機能を前提として、Node 上の out-of-service
taint を検知し、Node の Network Fencing および RBD ボリュームのデタッチを自動で行う機能が実装されている。
仕様は下記のリンクを参照:
以上の機能により、Node が障害によってダウンした場合にも RBD PV + Pod の StatefulSet が別の正常なNode 上に移動される。
本記事では、out-of-service
taint の付与によって Non-Graceful Node Shutdown をトリガーし、Rook-Ceph の RBD が failover される様子を確認する。
Failover の実験
前提条件
- Kubernetes Cluster は、3-node 構成の OKD4 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 を作成する:
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 名を表示するものである:
#!/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
}
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 だけ付与した場合はどうなるのだろうか。
再度、事前準備を行い、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 を実行する処理が書かれている。
ログを見る限り、 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 がマージされたのが 2023年5月末。
Kubernetes の Non-Graceful Node Shutdown が stable 版としてとして GA されたのが v1.28 1(2023年08月15日2)。Kubernetes や Rook-Ceph は万能なイメージがあったが、実はこの類の Failover ができるようになってまだ1年ほどなのか。
Medik8s を用いれば障害発生時の Taints の付与も自動化できるようで、検証した結果は次の記事を参照:
これらの機能をうまく使うことでアプリケーション側の failover 実装の負担が軽くなりそう。