GKE 1.20+ で preemptible / spot VM とうまく付き合う

インフラの駒崎です。
Google Kubernetes Engine (GKE) の 1.20+ で有効な kubelet graceful node shutdown と、それを活用した preemptible VM の利用について書かせていただきます。

GCP の Preemptible VM とは

Preemptible VM は、いくつかの制限があるかわりに通常のインスタンスよりも安く利用できるインスタンスです。制限はいくつかありますが、最も留意すべきは 「いつでも停止される可能性があり、最長でも起動から 24 時間で停止される」点でしょうか。

※ Preemptible VM の新バージョンとして Spot VM もアナウンスされました (2021/10/13 現在 preview) 。

Graceful node shutdown

GKE 1.20 以降のバージョンでは kubelet graceful node shutdown が有効化されています。これにより Compute Engine VM の終了を kubernetes 側で簡単にハンドリングできるようになりました。このバージョンは、2021/09 より STABLE リリースチャネルでも使えるようになっています。

以前との比較

この機能が無い状態では、VM の終了は kubernetes からは突然ノードが使用不能になったように見えるため、ワークロードが正常に終了されませんでした。具体的には、Pod の lifecycle.preStop の実行やコンテナへの終了シグナル送信、また Service からの除外などが行われませんでした。これらを適切に扱うためには、k8s-node-termination-handler のようなツールをユーザが導入する必要がありました。

Graceful node shutdown のデフォルト有効化により、preemptible VM の中断や delete instance API などによる VM 終了の際に、ほぼ正常系の終了処理が自動的に実行されるようになりました。

動作確認

この機能の有無による挙動を比較してみます。Deployment + Service で作成した PHP アプリケーションに、リクエストを流しながら VM の終了を行います。

使うアプリケーションは、php:8.0-apache イメージに以下のスクリプトを配置したものです。リクエスト途中に終了してしまうといった、安全でないケースをわかりやすくするため、レスポンスに1秒かかるようにします。

また、preStop の実行を確認しやすいよう sleep 10 を使ったログを仕込みます。

ここに Service をかぶせてクラスタ内から webapp の名前でアクセスできるようにし、httperf でリクエストを流します。

preemptible VM の終了をシミュレートするには、gcloud compute instances simulate-maintenance-event が使えます。これをリクエストを流している最中に実行します。

1.19.14-gke.1900

まずは、graceful node shutdown が無効な環境で挙動を確認します。

  • クラスタバージョン、ノードバージョン: ともに 1.19.14-gke.1900
  • ノード:  3台 (preemptible VM)、自動スケール無効
  • アプリケーション: Deployment の replicas: 9 とし 3Pods/ノードになるよう分散して配置
  • resources.requests は 1ノードが利用できなくても別ノードで十分に再配置可能な値

この環境で、リクエストを流しながら simulate-maintenance-event を実行しました。

結果の一部を掲載します。

合計で 2000 リクエストを送信していますが、114 件がタイムアウトや切断により失敗しています。

1.20.10-gke.301

つぎに、graceful node shutdown の有効な GKE 1.20 での確認です。クラスタ、ノードともに 1.20.10-gke.301 にしている以外は前述の 1.19 と同様の環境です。

同じようにリクエストを流しながら simulate-maintenance-event を実行した結果です。

このとき、3ノード中 2台に対して間隔をあけて simulate-maintenance-event を実行しましたが、2000 リクエスト全てでエラーは発生しませんでした。Pod の安全な終了と再配置が適切に行われたものと思われます。

Cloud Logging からも確認してみます。

sleep 10 を含む preStop が適切に実行されています。

また php:8.0-apache イメージSTOPSIGNAL SIGWINCH が指定されていますが、適切に指定されたシグナルを受け取っていることが見て取れます。

期待通りの動作が確認できました。

注意点

上記のような簡単なケースでは期待通りに動作しますが、いくつか注意点もあります。基本はドキュメントを都度ご参照いただくのがよいかと思いますが、主なものを挙げてみます。

Kubernetes 本来の挙動との差異

Kubernetes 制約違反 等にあるように、preemptible VM 使用時に注意すべき挙動があります。

VM の中断時には PodDisruptionBudget (PDB) は保証されません。PDB の設定よりも多くの Pod が利用できなくなる可能性があります。

また、VM 中断時にそのノード上のユーザ Pod には、ベストエフォートで 25秒の猶予期間が与えられるとされています。terminationGracePeriodSeconds は 25秒以内にする必要があります。

StatefulSet では使用しない

StatefulSet では強制削除時に "ID ごとに最大 1つの Pod" の保証が守られなくなるために preemptible VM を使うべきではありません。

DaemonSet の preStop に頼らない

DaemonSet に対しても graceful node shutdown による終了処理はトリガーされるようです。しかし少なくとも今回の検証環境では、ノードプールをリサイズしたときに preStop の終了を待たずに VM は停止していました。終了時の後始末としての preStop には期待しないのが無難です。

Network endpoint group (NEG) 利用時の確認箇所

これは本質的には preemptible VM に関係は無いのですが、Web アプリケーションの安全な終了に関わりがあるため簡単に言及しておきたいと思います。

GKE 1.17.6-gke.7 以降のクラスタでは、network endpoint group (NEG) がデフォルトで有効化されています。GKE で特に意識せずに Service と Ingress を使って Web アプリケーションを公開すると、NEG を利用してロードバランサから直接 Pod の IP へトラフィックが配送される形になります。

この場合、ロードバランサから Pod へのトラフィックが流されるかどうかは、 kubernetes 側の Ready 状態ではなく、 GCP 側の該当の NEG に Pod の IP が含まれているか (と、ヘルスチェック状態) に依存します。

通常は GKE の ServiceNetworkEndpointGroup リソースが適切に kubernetes 内の状態によって NEG との同期を行います。Pod 作成の際に NEG に追加され、Pod 終了時には NEG から IP を除外してくれますので、正常動作していればあまり意識する必要はありません。もしも、想定外のリクエストを受け取っている場合など、詳細な情報を確認したいときには GCP NEG リソースを調べることで挙動を観察できます。

おわりに

kubelet graceful node shutdown のデフォルト有効化により、preemptible VM がぐっと使いやすくなりました。25秒の制限のなかで安全にアプリケーションを終了するのは難しいケースもありますが、このような運用上の制限を設計時より意識しておくことでアプリケーションのコスト最適化がやりやすくなるのではないかと思います。

さらに、本記事を書いている最中に後継バージョンとなる GCE Spot VM の更新もありました。これは preemptible VM にあった 24 時間強制終了の制限がなくなっているようです。preemptible VM をまとめて起動すると一斉に落ちがちな問題の回避が期待できそうです。

本記事が preemptible / spot VM の利用検討の参考になれば幸いです。

 

参考