やってみて理解する kubernetes の Graceful Node Shutdown

#初めに

kubernetes 1.21 が 2021/4/8 にリリースされました。
その新機能の概要は [Kubernetes 1.21: Power to the Community][Power-to-the-Community] に記載されています。
その中で、 [Graceful Node Shutdown][Graceful-Node-Shutdown] がちょっと気なったので、試してみました。

Release note や kubernetes docs などをベースにやってみた内容です。
そのため、もし理解違いなどがあればコメント等いただければありがたいです。
可能な範囲で、ソースコードも確認し、Markdown の注釈でリンクを張っています。
必要に応じてご参照ください。

[Power-to-the-Community]: https://kubernetes.io/blog/2021/04/08/kubernetes-1-21-release-announcement/
[Graceful-Node-Shutdown]: https://kubernetes.io/blog/2021/04/08/kubernetes-1-21-release-announcement/#graceful-node-shutdown

#用意するもの

- kubernetes 1.21 クラスター (Worker Node 2台以上（shutdown するため））

#環境

KVM 環境上に 4つの VM を立て、 1 Controller / 3 Worker 構成の kubernetes クラスターを構成しています。
主に確認した環境は以下の通りです。 

- KVM ホスト
    - CentOS Linux release 8.2.2004 (Core)
- Controller / Node  ( 1 Contoller / 3 Worker 構成 )
    - OS: 下表参照
    - containerd 1.4.4
    - calico 3.19
    - kubernetes 1.21

また、ノードの OS を変えて各種確認を行っているのですが、その OS 情報は以下の通りです。

| 略称 | バージョン | kernel バージョン |systemd バージョン|
|:-:|:-:|:-:|:-:|
|CentOS 8|8.2.2004|4.18.0-193|239-29|
|CentOS 7|7.9.2009|3.10.0-1160.24.1|219-78|
|RHEL 8|8.1|4.18.0-147|239-18|
|RHEL 7|7.9|3.10.0-1160|219-78|
|ubuntu21|21.04|5.11.0-16-generic|247.3-3ubuntu3|
|ubuntu20|20.04 LTS|5.4.0-72-generic|245.4-4ubuntu3|


#サマリー

まず、そもそも想定通りに動かないケースがある、という事象が発生しています。
現時点で「この環境なら全く問題なく稼働する。」というケースが無く、どの環境も何かしら問題点がありました。
もしうまくいっている方がいらっしゃれば、コメント等でアドバイスいただければ助かります。

- CentOS / RHEL は、 Graceful Node Shutdown はうまく動くが、他の問題があり結局使えない
- Ubunts 20, 21 は、 そもそも Graceful Node Shutdown がうまく動かない

完全に解決してから記事にしたかったのですが、時間がかかりそうなので取り急ぎ事実をまとめました。

詳細は後述しますが、**Graceful Node Shutdown は、 kubelet が systemd-logind （以下 logind ）の inhibitor-lock を取得し、shutdown のシグナル（ PrepareForShutdown ）が発生した際に shutdown を遅らせる、という仕組みです。**
各OS で確認したところ、以下のようになりました。

| ワーカー OS | 概要 |
|:-:|:-|
| CentOS 8  |GracefulNodeShutdown自体は動くが、そもそも iptables 関連で kubernetes 自体が正しく稼働しない。（コントローラー再起動後、ワーカーが NotReady になる）|
|RHEL 8|同上|
|CentOS 7|inhibitor-lock は取得されるが、poweroff 時に PrepareForShutdown シグナルが発行されない。 PrepareForShutdown シグナルをコマンドで発行した場合は kubelet は正しく応答する（Graceful Node Shutdown される）|
|RHEL 7|同上|
|ubuntu20|inhibitor-lock 自体が取得されない（Graceful Node Shutdown しない)|
|ubuntu21|inhibitor-lock 自体が取得されない（Graceful Node Shutdown しない)|

Graceful Node Shutdown が有効な場合、ノードを shutdown させると、該当のノード上の Pod の Status が `Shutdown` となり他のノードで必要数の Pod が立ち上がりました。
なお、`Shutdown` 状態の Pod は5分間残り続けるため、その間は `kubectl get pods` で確認すると ReplicaSet のPod 数指定よりも多く見えます。

うまくいっていない環境については、OS 上の設定等で何らかの対応が必要だと思いますが
現時点ではそこまでの調査ができていない状況です。


#Graceful Node Shutdown の仕組みの概要

Graceful Node Shutdown の機能は、 kubernetes 1.20 で アルファ、 kubernetes 1.21 でベータとなっており
ベータリリースされた機能はデフォルトで有効になっています。ただし設定上は 0秒指定になっており
事実上無効状態です。

Graceful Node Shutdown の動作の概要は下図のようになります。

1. kubelet の Graceful Node Shutdown 関連設定（`config.yaml`）を変更し、kubelet を再起動する
2. kubelet が設定に基づき、logind の `InhibitDelayMaxSec` を上書きする[^OverrideInhibitDelayMaxSec]
3. kubelet が logind の `inhibitor-lock` を取得する[^GetInhibitorLock]
4. ユーザー操作/障害等での停止に伴い、D-Bus経由で送信される `PrepareForShutdown` シグナルを受信する[^MonitorSignal]
5. kubelet は Pod を Graceful Shutdown させる[^PodGracefulShutdown] (その後 inhibitor-lock を解除する[^ReleaseInhibitorLock]）
6. logind は inhibitor-lock 解消または指定時間経過後に shutdown を行う

[^OverrideInhibitDelayMaxSec]: https://github.com/kubernetes/kubernetes/blob/release-1.21/pkg/kubelet/nodeshutdown/systemd/inhibit_linux.go#L166-L187
[^GetInhibitorLock]: https://github.com/kubernetes/kubernetes/blob/release-1.21/pkg/kubelet/nodeshutdown/systemd/inhibit_linux.go#L72-L93
[^MonitorSignal]: https://github.com/kubernetes/kubernetes/blob/release-1.21/pkg/kubelet/nodeshutdown/systemd/inhibit_linux.go#L125-L158
[^PodGracefulShutdown]: https://github.com/kubernetes/kubernetes/blob/release-1.21/pkg/kubelet/nodeshutdown/nodeshutdown_manager_linux.go#L221
[^ReleaseInhibitorLock]: https://github.com/kubernetes/kubernetes/blob/release-1.21/pkg/kubelet/nodeshutdown/nodeshutdown_manager_linux.go#L269-L278


|![GNS01.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/310495/bc01fede-6a70-bb29-b7fc-506b13061503.png)
|:-:|

<details><summary> [inhibitor-lock][inhibitor-lock] 補足（セクション内参照）</summary>
systemd 183 以降で実装されている、システムのシャットダウンやスリープを制御する仕組みです。
アプリケーション側からシャットダウンやスリープを遅らせたり、させないようにしたりすることができます。
最近の Linux では、 シャットダウンなどは logind が制御しており、D-Bus経由で logind と連携することにより間接的にシャットダウンを制御できます。
アプリケーション（今回はkubelet）は logind の Manager インターフェース（org.freedesktop.login1.Manager.Inhibit）を call し、inhibitor-lock を取得します。
inhibitor-lock には、図中にもある通り以下の4つのパラメータがあります。
・ What  :  イベント(shutdown 等)
・ Who   :  ロックを取得するプロセス情報
・ Why   :  ロック取得理由概要
・ Mode :   delay or block
　　・ delay : 指定秒数遅らせる
　　・ block : ロック解除まで待たせる
　
なお、 Mode : block を指定すると、ロックしたプロセスがロックを解除するまでイベントが実行されませんので注意が必要です。多くの場合は delay を指定します。
delay の場合、inhibitor-lock が解除されるか、`InhibitDelayMaxSec` （デフォルト 5秒）が経過するまで logind はイベントの実行を遅らせます。 
`InhibitDelayMaxSec` 自体は logind の設定です。 ( /etc/systemd/logind.conf 参照 )
　
kubelet は kubelet の `config.yaml` 設定に従い、 `/etc/systemd/logind.conf.d/99-kubelet.conf`　を作成し、その中で `InhibitDelayMaxSec` を上書きします。[^99-kubelet.conf]

[inhibitor-lock]: https://www.freedesktop.org/wiki/Software/systemd/inhibit/
[^99-kubelet.conf]: https://github.com/kubernetes/kubernetes/blob/release-1.21/pkg/kubelet/nodeshutdown/systemd/inhibit_linux.go#L160-L187

</details>

# kubelet の設定

- 設定対象ノードの kubelet の設定（`/var/lib/kubelet/config.yaml`）で下表の2つのパラメータを指定する
- kubelet の再起動 （ `systemctl stop kubelet ; systemctl start kubelet` )

|パラメータ|今回の設定値|デフォルト|概要|
|:-|:-|:-|:-|
|shutdownGracePeriod|60s|0s|shutdown 開始までの総待機秒数|
|shutdownGracePeriodCriticalPods|30s|0s|Critical Pod 停止用の待機秒数|

<details><summary>※kubelet の設定についての補足（セクション内参照）</summary>
<Div>

- shutdownGracePeriod 内に shutdownGracePeriodCriticalPods 秒数が含まれます
    - shutdownGracePeriod = RegularPod の待機秒数(nonCriticalPodGracePeriod) + CriticalPod の待機秒数(CriticalPodGracePeriod)です [^GracePeriod]
- [Critical Pod][CriticalPod]
    - Podの [PriorityClass] に指定する優先度設定(32bit Integer が有効値(最大2,147,483,647)、ユーザー設定は 10億以下の整数。大きいほど優先される）。デフォルトで以下の2つが設定されている。このどちらかが設定されている Pod が Critical Pod として扱われ、優先的にスケジュールされます
        - system-cluster-critical : 2000000000 (20億)
        - system-node-critical    : 2000001000 (20億1000)
- Pod の GracefulShutdown パラメータ `pod.Spec.TerminationGracePeriodSeconds` が、`nonCriticalPodGracePeriod` または `CriticalPodGracePeriod` 以下の場合、`pod.Spec.TreminationGracePeriodSeconds` の値が利用されます。[^PodSpecTerminationGracePeriodSeconds]
- kubelet の設定ファイルは kubeadm で構築した場合は `/etc/kubernetes/kubelet.conf` です
    - `systemctl cat kubelet` 等で確認できます
- `shutdown -h now` では動作しません（詳細は [KEP-2000] を参照ください）

[^GracePeriod]: https://github.com/kubernetes/kubernetes/blob/release-1.21/pkg/kubelet/nodeshutdown/nodeshutdown_manager_linux.go#L225
[CriticalPod]: https://kubernetes.io/docs/tasks/administer-cluster/guaranteed-scheduling-critical-addon-pods/#marking-pod-as-critical
[PriorityClass]: https://kubernetes.io/ja/docs/concepts/configuration/pod-priority-preemption/#priorityclass
[^PodSpecTerminationGracePeriodSeconds]: https://github.com/kubernetes/kubernetes/blob/release-1.21/pkg/kubelet/nodeshutdown/nodeshutdown_manager_linux.go#L241-L244
[KEP-2000]: https://github.com/kubernetes/enhancements/tree/masjter/keps/sig-node/2000-graceful-node-shutdown
</Div>
</details>


#確認内容

Graceful Node Shutdown に関して、うまく動かない状況のため、
ワーカーノードを各種 OS に変更してみて、以下の項目を確認しました。

1. kubelet の設定を変更した後、 inhibitor-lock が取得されるか？
2. 各種停止、再起動コマンド実施時に PrepareForShutdown シグナルが D-Bus 経由で送付されるか？
3. inhibitor-lock 取得状態で、コマンドで PrepareForShutdown シグナルを送り、kubelet が応答するか？


#実施ケース＆結果

## 1. kubelet 設定変更後、 inhibitor-lock が取得されるか？

inhibitor-lock が取得されたかどうかは `systemd-inhibit --list` で確認することができます。

```shell-session
# systemd-inhibit --list
     Who: kubelet (UID 0/root, PID 918/kubelet)
    What: shutdown
     Why: Kubelet needs time to handle node shutdown
    Mode: delay
```

※ubuntu では見え方が異なります

kubelet が shutdown イベント時に Mode: delay で inhibitor-lock を取得しています。[^kubelet-get-inhibitor-lock]
各ワーカー OS について、 Minimum インストールの際に inhibitor-lock が取得されたかどうかを下表に記載します。

[^kubelet-get-inhibitor-lock]: https://github.com/kubernetes/kubernetes/blob/release-1.21/pkg/kubelet/nodeshutdown/systemd/inhibit_linux.go#L74-L81

|ワーカーOS|inhibitor-lock 取得|
|:-:|:-|
|CentOS 8|取得|
|CentOS 7|取得|
|RHEL 8|取得|
|RHEL 7|取得|
|ubuntu21|取得せず|
|ubuntu20|取得せず|


## 2. PrepareForShutdown シグナルが送付されるか？

2点目の、「各種停止、再起動コマンド実施時に PrepareForShutdown シグナルが D-Bus 経由で送付されるか？」に関してです。

シグナルが送付されたかどうかは、D-Bus をモニターすることにより確認できます。
ターミナル上で、`dbus-monitor` コマンドを利用することにより確認できます。
下記の例では、 `PrepareForShutdown` が `true` で送信されていることがわかります。

```shell-session
# dbus-monitor --system "type='signal',interface='org.freedesktop.login1.Manager'"
... (略) ...

signal time=1620471576.944039 sender=:1.12 -> destination=(null destination) serial=171 path=/org/freedesktop/login1; interface=org.freedesktop.login1.Manager; member=PrepareForShutdown
   boolean true
```

上記の例は、 CentOS 8 で、 1つのターミナルで `dbus-monitor` でモニターをしながら、他のターミナルで、 `systemctl poweroff` を行って停止した際のシグナルです。
`PrepareForShutdown (true)` シグナルが出ていることがわかります。

また、以下のコマンドで手動でシグナルを送信することも可能です。（実際にシャットダウンはしません）
kubelet は `PrepareForShutdown : true` を受け取ると、inhibitor-lock を開放し、 `false` を受け取ると inhibitor-lock を取得します。

```shell-sessison
# gdbus emit --system --object-path /org/freedesktop/login1 --signal org.freedesktop.login1.Manager.PrepareForShutdown true
```

下表は、各種コマンド発行時に `PrepareForShutdown (true)` シグナルが出るかどうかを確認した結果です。
（systemctl や shutdown コマンドの仕様を丁寧に調べたらここまで確認する必要はなさそうな気もします。。。）
○：シグナル送付、×：シグナルなし

|ワーカーOS|systemctl poweroff|systemctl reboot|shutdown -h now|shutdown -r now|halt -p -f|virsh destroy|virsh shutdown|gdbus(手動)|
|:-:|:-:|:-:|:-:|:-:|:-:|:-:|:-:|:-:|
|CentOS 8|○|○|×|×|×(*1)|×(*1)|○|○|
|CentOS 7|×|×|×|×|×(*1)|×(*1)|×|○|
|RHEL 8|○|○|×|×|×(*1)|×(*1)|○|○|
|RHEL 7|×|×|×|×|×(*1)|×(*1)|×|○|
|ubuntu21|○|○|×|×|×(*1)|×(*1)|○|○|
|ubuntu20|○|○|×|×|×(*1)|×(*1)|○|○|

※当環境は KVM 上の VM ですので、 KVM ホスト側からの強制停止 `virsh destroy`、`virsh shutdown` も試してみています。
(*1) : ターミナル自体がハングしたため、ターミナル上に表示されていないだけの可能性あり（ファイル出力していても出力されていなかったため ×としています） halt のマニュアル上 `-f` を1回指定した場合は system manager と連携しクリーンな shutdown が行われるという記載のため、PrepareForShutdown シグナルが出てもよいのでは、と思っていましたが、実際は出ていないようです。

---
2021/05/30 追記 --
@hodahgi さんにアドバイスいただきました。ありがとうございます！
shutdown コマンド実行時に PrepareForShutdown が出ないのは、 `now` を引数にしているからで、
`shutdown --r +1` など、遅延指定の場合は PrepareForShutdown が出るのではないか、という話でした。
下記のリンク先にも記載があります。

https://trstringer.com/systemd-inhibitor-locks/

これについても、見てはいたのですが、さっと確認してダメだったので放置していましたが
特定の環境で出なかっただけだったようです。
アドバイスをいただき改めて確認してみました。
結果としては RHEL 7 / CentOS 7 では PrepareForShutdown が出ませんでしたが、
その他の環境では PrepareForShutdown シグナルが発生しました。

|ワーカーOS|shutdown --r +1|
|:-:|:-:|
|CentOS 8|○|
|CentOS 7|×|
|RHEL 8|○|
|RHEL 7|×|
|ubuntu21|○|
|ubuntu20|○|

---


## 3.inhibitor-lock 取得状態で、コマンドで PrepareForShutdown シグナルを送り、kubelet が応答するか？

このケースに関しては、 kubelet の設定変更を行った際に、 inhibitor-lock が取得されることが前提のため、検証の対象は CentOS 7,8,  RHEL 7,8 が対象になります。
このケースが、実際に Graceful Node Shutdown できるかどうか、というケースになります。

|ワーカーOS|inhibitor-lock|Graceful Node Shutdown|備考|
|:-:|:-:|:-:|:-|
|CentOS 8|取得済み|Pod が Shutdown に遷移|systemctl poweroff でシグナル生成|
|CentOS 7|取得済み|Pod が Shutdown に遷移|gdbus コマンドでシグナル生成|
|RHEL 8|取得済み|Pod が Shutdown に遷移|systemctl poweroff でシグナル生成|
|RHEL 7|取得済み|Pod が Shutdown に遷移|gdbus コマンドでシグナル生成|


Graceful Node Shutdown が発生すると、下記のように Pod の STATUS が `Shutdown` になります。

Node の STATUS は `NotReady` になり、　Pod の STATUS は `Shutdown` になっています。そして、 `Shutdown` になった数と同じ数の Pod が ContainerCreating になっています。　( 下のログは `--replicas=6` のケース ）

また、デフォルトでは、 `Shutdown` 状態の Pod は 5分経過後に Terminating を経て削除されます。

```shell-session
# kubectl get nodes,pods -o wide                                                                                                                        

NAME            STATUS     ROLES                  AGE    VERSION   INTERNAL-IP       EXTERNAL-IP   OS-IMAGE                   KERNEL-VERSION           CONTAINER-RUNTIME
node/k8sctl20   Ready      control-plane,master   120m   v1.21.1   192.168.199.220   <none>        Red Hat Enterprise Linux   3.10.0-1160.el7.x86_64   containerd://1.4.6
node/k8swkr20   NotReady   <none>                 118m   v1.21.1   192.168.199.230   <none>        Red Hat Enterprise Linux   3.10.0-1160.el7.x86_64   containerd://1.4.6
node/k8swkr21   Ready      <none>                 118m   v1.21.1   192.168.199.231   <none>        Red Hat Enterprise Linux   3.10.0-1160.el7.x86_64   containerd://1.4.6
node/k8swkr22   Ready      <none>                 118m   v1.21.1   192.168.199.232   <none>        Red Hat Enterprise Linux   3.10.0-1160.el7.x86_64   containerd://1.4.6

NAME                                  READY   STATUS              RESTARTS   AGE     IP               NODE       NOMINATED NODE   READINESS GATES
pod/graceful-nginx-6cf96bc766-2wpn2   1/1     Running             0          7m23s   10.200.149.75    k8swkr22   <none>           <none>
pod/graceful-nginx-6cf96bc766-9vb85   1/1     Running             0          7m23s   10.200.211.205   k8swkr21   <none>           <none>
pod/graceful-nginx-6cf96bc766-d4ql9   1/1     Running             0          7m23s   10.200.149.76    k8swkr22   <none>           <none>
pod/graceful-nginx-6cf96bc766-j6w9x   1/1     Running             0          7m23s   10.200.211.204   k8swkr21   <none>           <none>
pod/graceful-nginx-6cf96bc766-82jn5   0/1     Shutdown            0          7m23s   <none>           k8swkr20   <none>           <none>
pod/graceful-nginx-6cf96bc766-nmr5b   0/1     Shutdown            0          7m23s   <none>           k8swkr20   <none>           <none>
pod/graceful-nginx-6cf96bc766-kcqlr   0/1     ContainerCreating   0          4s      <none>           k8swkr22   <none>           <none>
pod/graceful-nginx-6cf96bc766-xlhmc   0/1     ContainerCreating   0          4s      <none>           k8swkr21   <none>           <none>
```

なお、 Graceful Node Shutdown 設定なしで、 いきなり Node を `shutdown -r now` などとすると、
Pod は  Ready 状態のままになり、 Node 起動後、  Unknown ステータスとなります。
その後 Pod が再起動（起動？）されて  Running になりました。
（ Pod については Terminating ステータスのままで止まるケースもあるようです）


#終わりに

GracefulNodeShutdown を軽く試してみるつもりが、かなりはまってしまいいろいろと検証しました。
logind の inhibitor-lockの取得、PrepareForShutdownシグナルを受けて Pod の Shutdown といった
一連の流れをパーツごとに確認することができました。

検証前は、もっと Pod の Graceful Shutdown と密接に関連しているだろうと思っていたのですが、
nonCriticalPodGracePeriod や CriticalPodGracePeriod と pod.Spec.TreminationGracePeriodSeconds の短い方が使われる、というぐらいでしか関連していないのが意外でした。あえて疎な関係にしているのかもしれません。

ubuntu に関しては、 おそらく inhibitor-lock するようにする設定があり、
それを設定すれば正しく動くのだと思いますがそこまでたどり着けませんでした。
kubernetes だけの知識ではだめで Linux の知識も必要だなと痛感しました。

結局筆者の環境では問題ない形では動かすことができなかったのが残念ではありますが、良い勉強になりました。
また　他の機能についても試してみたいと思います。

---
参考）
■Graceful Node Shutdown
https://kubernetes.io/docs/concepts/architecture/nodes/#graceful-node-shutdown

■Graceful Node Shutdown Goes Beta
https://kubernetes.io/blog/2021/04/21/graceful-node-shutdown-beta/#:~:text=With%20graceful%20node%20shutdown%2C%20the,any%20resources%20they%20are%20holding.

■KEP-2000: Graceful Node Shutdown
https://github.com/kubernetes/enhancements/tree/master/keps/sig-node/2000-graceful-node-shutdown

■Pod Lifecycle
https://kubernetes.io/docs/concepts/workloads/pods/pod-lifecycle/

■Kubernetes: 詳解 Pods の終了
https://qiita.com/superbrothers/items/3ac78daba3560ea406b2

■ inhibitor-lock
https://www.freedesktop.org/wiki/Software/systemd/inhibit/

■ inhibitor-lock deep dive
https://trstringer.com/systemd-inhibitor-locks/

■kubernetes/pkg/kubelet/nodeshutdown/nodeshutdown_manager_linux.go
https://github.com/kubernetes/kubernetes/blob/master/pkg/kubelet/nodeshutdown/nodeshutdown_manager_linux.go

以下、ソースリンク