Kubernetes: Non Graceful Node Shutdownの動作検証

# はじめに

本記事では、Kubernetes v1.28にて GA となった Non Graceful Node Shutdownの動作検証を行います。
Kubernetesのノードをシャットダウンした際、このシャットダウンが正常に完了しなかった場合、Volumeなどが外れないことがあります。
このような状態になると、iSCSIなどのブロックストレージでは、既にRead/Writeが可能な状態でマウントされているノードがあると、他ノードにてRead/WriteにてVolumeのアタッチが出来なくなることがあります。
複数ノードから1つのVolumeをアタッチすることをMulti Attachと呼び、上記のエラーケースはMulti Attach エラーと呼ばれます。
このような状態になった場合に、復旧する手段として登場したのが Non Graceful Node Shutdownです。

この機能の注意点としては、自動復旧するものではないという点です。
ユーザがノードが正常にシャットダウンされていないと判断したのち、taint(`node.kubernetes.io/out-of-service`)を付与してあげることで、この機能が実行されます。

なお、本機能のサポートは以下のバージョンからサポートされています

- Kubernetes v1.24: アルファ (デフォルト無効)
- Kubernetes v1.26: ベータ (デフォルト有効)
- Kubernetes v1.28: GA

# 検証環境

- Kubernetes v1.25.9, 1.26.4
- ノードOS: Ubuntu 20.04
- ストレージ: NetApp ONTAP AFF9.9 (iSCSIを利用)

# 検証

この検証では、Non Graceful Node Shutdownの挙動を確認するため、本機能が無効化されているKubernetesを使い本機能がない場合の挙動を検証します(検証1)。
その次に、本機能が有効化されているKubernetesを使い、本機能の挙動を確認します(検証2)。

## [検証1] Non Graceful Node Shutdownが無効の場合 (Kubernetes v1.25以前)

まずNon Graceful Node Shutdownが無効の場合の挙動について示します。
ここではNon Graceful Node ShutdownのFeature Gates(`NodeOutOfServiceVolumeDetach`)がデフォルトで無効となっているKubernetes v1.25を使い検証します。
始めに、StatefulSet(`sts.yaml`)を使いPod,PVC,PVを作成します。

- sts.yaml

```YAML
apiVersion: apps/v1
kind: StatefulSet
metadata:
  name: pv-test
spec:
  serviceName: pv-test
  replicas: 1
  selector:
    matchLabels:
      app: pv-test
  template:
    metadata:
      labels:
        app: pv-test
    spec:
      containers:
      - name: pv-test
        image: ubuntu:20.04
        volumeMounts:
        - name: block
          mountPath: /mnt/block
        command: 
        - sleep
        - infinity
  volumeClaimTemplates:
  - metadata:
      name: block
    spec:
      accessModes: [ "ReadWriteOnce" ]
      storageClassName: ontap-block
      resources:
        requests:
          storage: 10Gi
```

Manifest(`sts.yaml`)をデプロイし、Pod,PVC,PVを確認します。

```
$ kubectl apply -f sts.yaml 
statefulset.apps/pv-test created

$ kubectl get pod,pvc,pv -o wide
NAME            READY   STATUS    RESTARTS   AGE   IP             NODE                                       NOMINATED NODE   READINESS GATES
pod/pv-test-0   1/1     Running   0          55s   10.26.107.43   demo-ysakashita-w-default-a7102877-mmh9d   <none>           <none>

NAME                                    STATUS   VOLUME                                     CAPACITY   ACCESS MODES   STORAGECLASS   AGE   VOLUMEMODE
persistentvolumeclaim/block-pv-test-0   Bound    pvc-adc68176-5517-4274-85fa-33d5dfcd81c2   10Gi       RWO            ontap-block    55s   Filesystem

NAME                                                        CAPACITY   ACCESS MODES   RECLAIM POLICY   STATUS   CLAIM                     STORAGECLASS   REASON   AGE   VOLUMEMODE
persistentvolume/pvc-adc68176-5517-4274-85fa-33d5dfcd81c2   10Gi       RWO            Delete           Bound    default/block-pv-test-0   ontap-block             52s   Filesystem
```

次に、Podがデプロイされているノード(`demo-ysakashita-w-default-a7102877-mmh9d`)にsshで入り、OS Panicを発生させます。

```
ubuntu@demo-ysakashita-w-default-a7102877-mmh9d:~$ sudo bash
root@demo-ysakashita-w-default-a7102877-mmh9d:/home/ubuntu# vi /etc/sysctl.conf
root@demo-ysakashita-w-default-a7102877-mmh9d:/home/ubuntu# sysctl -p
kernel.panic = 0
root@demo-ysakashita-w-default-a7102877-mmh9d:/home/ubuntu# echo c > /proc/sysrq-trigger
```

OS Panicが発生した後、ノードのステータスを確認します。

```
$ kubectl get node
NAME                                        STATUS     ROLES    AGE    VERSION
...
demo-ysakashita-w-default-a7102877-mmh9d    NotReady   <none>   176m   v1.25.9
```

該当のノードがNotReadyとなりました。 しばらく待ち、Podのステータスも確認します.

```
$ kubectl get pod -o wide
NAME        READY   STATUS        RESTARTS   AGE   IP             NODE                                       NOMINATED NODE   READINESS GATES
pv-test-0   1/1     Terminating   0          10m   10.26.107.43   demo-ysakashita-w-default-a7102877-mmh9d   <none>           <none>
```

10分以上経ってもTerminatingのままとなります。 
この状態で、ノードにtaint (`node.kubernetes.io/out-of-service=nodeshutdown:NoExecute`)を付与します。 

Kubernetes v1.25では、Non Graceful Node Shutdownが無効のため、意味がないことを確認します。

```
$ kubectl taint node demo-ysakashita-w-default-a7102877-mmh9d "node.kubernetes.io/out-of-service=nodeshutdown:NoExecute"
node/demo-ysakashita-w-default-a7102877-mmh9d tainted
```

taintを付与した後、20分放置しましたがPodのステータスはTerminatingのままで復旧はしませんでした。 想定通りの挙動です。

```
$ kubectl get pod -o wide
NAME        READY   STATUS        RESTARTS   AGE   IP             NODE                                       NOMINATED NODE   READINESS GATES
pv-test-0   1/1     Terminating   0          30m   10.26.107.43   demo-ysakashita-w-default-a7102877-mmh9d   <none>           <none>
```


## [検証2] Non Graceful Node Shutdownが有効の場合 (Kubernetes v1.26以前)

Kubernetes v1.26にてNon Graceful Node ShutdownのFeature Gates(`NodeOutOfServiceVolumeDetach`)がベータとなり、デフォルト有効となりました。
そこで、Kubernetes v1.26を使い Non Graceful Node Shutdown の検証を行います。
上記の検証で用いた`sts.yaml`を使いPod,PVC,PVを作成します。

```
$ kubectl apply -f sts.yaml 
statefulset.apps/pv-test created

$ kubectl get pod,pvc,pv -o wide
NAME            READY   STATUS    RESTARTS   AGE   IP            NODE                                      NOMINATED NODE   READINESS GATES
pod/pv-test-0   1/1     Running   0          47s   10.26.26.23   demo-ysaka-net-w-default-4282754a-px2wm   <none>           <none>

NAME                                    STATUS   VOLUME                                     CAPACITY   ACCESS MODES   STORAGECLASS   AGE   VOLUMEMODE
persistentvolumeclaim/block-pv-test-0   Bound    pvc-e5b39376-33bb-44af-8753-85a2bba44a6a   10Gi       RWO            ontap-block    47s   Filesystem

NAME                                                        CAPACITY   ACCESS MODES   RECLAIM POLICY   STATUS   CLAIM                     STORAGECLASS   REASON   AGE   VOLUMEMODE
persistentvolume/pvc-e5b39376-33bb-44af-8753-85a2bba44a6a   10Gi       RWO            Delete           Bound    default/block-pv-test-0   ontap-block             45s   Filesystem
```

次に、Podがデプロイされているノード(`demo-ysaka-net-w-default-4282754a-px2wm`)にsshで入り、OS Panicを起こします。

```
ubuntu@demo-ysaka-net-w-default-4282754a-px2wm:~$ sudo bash
root@demo-ysaka-net-w-default-4282754a-px2wm:/home/ubuntu# vi /etc/sysctl.conf 
root@demo-ysaka-net-w-default-4282754a-px2wm:/home/ubuntu# sysctl -p
kernel.panic = 0
root@demo-ysaka-net-w-default-4282754a-px2wm:/home/ubuntu# echo c > /proc/sysrq-trigger
```

ノードのステータスを確認します。

```
$ kubectl get node
NAME                                       STATUS     ROLES    AGE     VERSION
...
demo-ysaka-net-w-default-4282754a-px2wm    NotReady   <none>   3h52m   v1.26.4
```

該当のNodeがNotReadyとなっていることを確認しました。 しばらく待ち、Podの状態を確認します。

```
$ kubectl get pod -o wide
NAME        READY   STATUS        RESTARTS   AGE     IP            NODE                                      NOMINATED NODE   READINESS GATES
pv-test-0   1/1     Terminating   0          8m43s   10.26.26.23   demo-ysaka-net-w-default-4282754a-px2wm   <none>           <none>
```

この状態で、先の検証と同様にノードにtaint(`node.kubernetes.io/out-of-service=nodeshutdown:NoExecute`)を付与します。

```
$ kubectl taint node demo-ysaka-net-w-default-4282754a-px2wm "node.kubernetes.io/out-of-service=nodeshutdown:NoExecute"
node/demo-ysaka-net-w-default-4282754a-px2wm tainted
```

taintを付与後、すぐに別のノードにスケジューリングされPodがRunningになり復旧しました。

```
$ kubectl get pod -o wide
NAME        READY   STATUS    RESTARTS   AGE   IP            NODE                                      NOMINATED NODE   READINESS GATES
pv-test-0   1/1     Running   0          30s   10.26.99.39   demo-ysaka-net-w-default-4282754a-4s68h   <none>           <none>

$ kubectl describe pod pv-test-0 
...
Events:
  Type    Reason                  Age   From                     Message
  ----    ------                  ----  ----                     -------
  Normal  Scheduled               102s  default-scheduler        Successfully assigned default/pv-test-0 to demo-ysaka-net-w-default-4282754a-4s68h
  Normal  SuccessfulAttachVolume  102s  attachdetach-controller  AttachVolume.Attach succeeded for volume "pvc-e5b39376-33bb-44af-8753-85a2bba44a6a"
  Normal  Pulled                  97s   kubelet                  Container image "ubuntu:20.04" already present on machine
  Normal  Created                 97s   kubelet                  Created container pv-test
  Normal  Started                 97s   kubelet                  Started container pv-test
```

# 感想

本記事では、Non Graceful Node Shutdownの検証を行いました。

この機能が登場する以前は、シャットダウンが正しく行われなかったノードに入りVolumeを強制アンマウントやiSCSIなどストレージとのセッションのログアウトを行う必要がありました。
ただし、シャットダウンが正しく行われないノードは、何らかの異常状態に陥っているため、そもそもSSHなどでノードに入ることができない場合もあり、この手段が取れないことも多々あります。
そのような場合は、iSCSIのセッションタイムアウトまで待つか、ストレージ側でセッションを強制的に切断しなければなりませんでした。

このように、ノードが正しくシャットダウンできなかった場合の復旧手順は複雑かつノードのroot権限やストレージの管理者の権限が必要となるなど実施できる管理者も限られていました。

今回検証したNon Graceful Node Shutdownでは、ノードが正しくシャットダウンできなかった場合でも Kubernetesのノードにtaintを設定できる権限をもった管理者であれば、比較的容易に復旧させることができます。
ただし、注意しなければならないのは、taintを手動で設定する必要がある点です。
シャットダウンが正しく行われたかどうかを正確に判断することは容易ではありません。
そのため、自動で復旧することは難しく管理者が「正しくシャットダウンがされていない」と判断したノードに対してtaintを設定することで復旧させるように設計されています。
つまり、管理者は「正しくシャットダウンされいない」ことを検知できるようにしておく必要があります。
簡単なやり方としては、ノードのステータスが`Ready`でなくなってから、ある程度長い時間(正しいシャットダウンではあり得ない時間)停止してしまっていることを検知する方法です。

Non Graceful Node Shutdownは障害発生時に素早く復旧するために有効な手段ですので使い方を理解するとともに、この機会に障害検出から復旧までが短時間で行われるよう運用方法を確認することをお勧めします。

# 参考情報

- [Kubernetes 1.26: Non-Graceful Node Shutdown Moves to Beta](https://kubernetes.io/blog/2022/12/16/kubernetes-1-26-non-graceful-node-shutdown-beta/)
- [KEP-2268: Non graceful node shutdown](https://github.com/kubernetes/enhancements/tree/master/keps/sig-storage/2268-non-graceful-shutdown)
- [kubernetes/kubernetes Issue#65392](https://github.com/kubernetes/kubernetes/issues/65392)