2022.01.12

Rancher Kubernetes Engine(RKE)の無停止更新の検証

TL;DR

  • RKEでKubernetesを更新してもDaemonSetの場合はアプリケーションのダウンタイムが発生するのでKubernetesクラスタを切り離して更新した方が良い
  • RKE2でコンテナランタイムにcontainerdが利用できる

はじめに

こんにちは。次世代システム研究室のT.Tです。

現在開発運用に携わっているサービスで利用しているKubernetesのバージョンが古くなってきたのでバージョンアップを検討しています。前回バージョンアップした際の記事ではKubernetesのクラスタを2つ用意して、片系で本番環境のリクエストを受け付けるようにして、もう一方のクラスタをバージョンアップしてから本番環境に戻す方針で進めました。

この方針でのバージョンアップでも特にサービスには支障はありませんでしたが、各環境を更新すると3人日程掛かってしまうためKubernetesの更新サイクルに合わせてバージョンアップするのが難しい問題がありました。今回はその更新作業の工数を減らすためにRKEの無停止更新が利用できないかを検証します。

また、今回のバージョンアップの対象となるKubernetesではコンテナランタイムにDockerが使えますが、それ以降のバージョンでは使えなくなりそうなので次回のバージョンアップに向けてコンテナランタイムの更新についても若干触れておきたいと思います。

1.RKEの無停止更新の仕組み

RKE v1.1.0以降でKubernetesのバージョンアップ時にKubernetes上のアプリケーションを無停止で更新できるようになったようです。How Upgrades WorkConfiguring the Upgrade Strategyに動作概要が書いてあり、これによるとバージョンアップの基本戦略としては1ノードずつ順番にcordonによりノードにコンテナをデプロイしないようにしながらノード上のコンポーネントを更新する仕組みのようです。今回はこの仕組みを使ってアプリケーションを無停止でKubernetesをバージョンアップできるかを検証します。

2.Kubernetesバージョンの検討

RKEのリリース情報で最新の安定版を確認しました。検証時は最新版がv1.3.2だったので、RKEはこのバージョンを利用することにしました。このバージョンのRKEが標準でサポートしているKubernetesの最新のバージョンはv1.21.6で、これは安定版となっています。

今回のKubernetesのバージョンアップに当たってもう一つ留意点があります。まだDocker以外のコンテナランタイムを検証できていないので、今回のバージョンアップではコンテナランタイムにDockerを使える必要があります。この点についてのソースを見てみるとDockerが使えなくなるのはv1.24からなので、バージョンアップするのはv1.21.6で問題なさそうです。

3.検証内容

検証は管理ノードが2つ、ワーカーノードが2つで各ワーカーノードにアプリケーションが稼働している環境で行います。この環境に対してrke upを実行して、更新中にgatlingによりアプリケーションに5qpsでリクエストを送信してエラーが発生しないかを測定します。

バージョンアップ元のKubernetesは1.14.6で、rkeの更新用の設定は以下のデフォルトのものを使います。
upgrade_strategy:
  max_unavailable_worker: 10%
  max_unavailable_controlplane: 1
  drain: false
  node_drain_input:
    force: false
    ignore_daemonsets: true
    delete_local_data: false
    grace_period: -1 // grace period specified for each pod spec will be used
    timeout: 60

4.無停止更新の検証結果

4.1.RKEの実行結果

RKEのワーカーノードを更新している辺りのログは以下のようになりました。cordonを実行してからノードを切り離し、コンテナイメージを更新しながら新しいバージョンのコンテナを1つずつ起動しているのがわかります。
...
time="2021-12-28T15:50:36+09:00" level=info msg="[workerplane] Processing host XXX.XXX.XXX.XXX"
...
time="2021-12-28T15:50:36+09:00" level=debug msg="[worker] Cordoning node XXX.XXX.XXX.XXX"
time="2021-12-28T15:50:36+09:00" level=debug msg="Checking node list for node [XXX.XXX.XXX.XXX], try #1"
time="2021-12-28T15:50:37+09:00" level=debug msg="Checking node list for node [XXX.XXX.XXX.XXX], try #1"
time="2021-12-28T15:50:37+09:00" level=debug msg="Node XXX.XXX.XXX.XXX is already cordoned: true"
time="2021-12-28T15:50:37+09:00" level=debug msg="[workerplane] upgrading host XXX.XXX.XXX.XXX"
time="2021-12-28T15:50:37+09:00" level=debug msg="[worker] Container [nginx-proxy] is already running on host [XXX.XXX.XXX.XXX]"
time="2021-12-28T15:50:37+09:00" level=debug msg="[worker] Checking if container [nginx-proxy] is eligible for upgrade on host [XXX.XXX.XXX.XXX]"
time="2021-12-28T15:50:37+09:00" level=debug msg="[worker] Container [nginx-proxy] is eligible for upgrade on host [XXX.XXX.XXX.XXX]"
...
time="2021-12-28T15:50:47+09:00" level=debug msg="[worker] Stopping old container"
time="2021-12-28T15:50:47+09:00" level=info msg="Checking if container [old-kubelet] is running on host [XXX.XXX.XXX.XXX], try #1"
time="2021-12-28T15:50:47+09:00" level=info msg="Stopping container [kubelet] on host [XXX.XXX.XXX.XXX] with stopTimeoutDuration [5s], try #1"
time="2021-12-28T15:50:48+09:00" level=info msg="Waiting for [kubelet] container to exit on host [XXX.XXX.XXX.XXX]"
time="2021-12-28T15:50:48+09:00" level=debug msg="Exit code for [kubelet] container on host [XXX.XXX.XXX.XXX] is [0]"
time="2021-12-28T15:50:48+09:00" level=info msg="Renaming container [kubelet] to [old-kubelet] on host [XXX.XXX.XXX.XXX], try #1"
time="2021-12-28T15:50:48+09:00" level=debug msg="[worker] Successfully stopped old container kubelet on host [XXX.XXX.XXX.XXX]"
time="2021-12-28T15:50:48+09:00" level=info msg="Starting container [kubelet] on host [XXX.XXX.XXX.XXX], try #1"
time="2021-12-28T15:50:48+09:00" level=info msg="[worker] Successfully updated [kubelet] container on host [XXX.XXX.XXX.XXX]"
...
time="2021-12-28T15:51:27+09:00" level=info msg="[worker] Now checking status of node XXX.XXX.XXX.XXX, try #1"
time="2021-12-28T15:51:27+09:00" level=debug msg="Checking node list for node [XXX.XXX.XXX.XXX], try #1"
time="2021-12-28T15:51:27+09:00" level=debug msg="[worker] Found node by name XXX.XXX.XXX.XXX"
time="2021-12-28T15:51:27+09:00" level=debug msg="Checking node list for node [XXX.XXX.XXX.XXX], try #1"
time="2021-12-28T15:51:27+09:00" level=debug msg="Checking node list for node [XXX.XXX.XXX.XXX], try #1"
time="2021-12-28T15:51:27+09:00" level=debug msg="Node XXX.XXX.XXX.XXX is already cordoned: false"
time="2021-12-28T15:51:27+09:00" level=info msg="[workerplane] Processing host YYY.YYY.YYY.YYY"
...

4.2.gatlingの実行結果

RKEでの更新中にgatlingの実行ログに502のレスポンスが発生する時間帯が15:55:08と15:55:58の2つありました。
================================================================================
2021-12-28 15:55:08                                        1400s elapsed
---- Requests ------------------------------------------------------------------
> Global                                                   (OK=6967   KO=12    )
> Token                                                    (OK=1      KO=0     )
> Get                                                      (OK=6966   KO=12    )
---- Errors --------------------------------------------------------------------
> status.find.is(200), but actually found 502                        12 (100.0%)

---- GetSimulation --------------------------------------------------------
[##################################-                                       ] 46%
          waiting: 8007   / active: 15     / done:6978  
---- GetTokenSimulation --------------------------------------------------------
[##########################################################################]100%
          waiting: 0      / active: 0      / done:1     
================================================================================
================================================================================
2021-12-28 15:55:58                                        1450s elapsed
---- Requests ------------------------------------------------------------------
> Global                                                   (OK=7188   KO=41    )
> Token                                                    (OK=1      KO=0     )
> Get                                                      (OK=7187   KO=41    )
---- Errors --------------------------------------------------------------------
> status.find.is(200), but actually found 502                        41 (100.0%)

---- GetSimulation --------------------------------------------------------
[###################################-                                      ] 48%
          waiting: 7757   / active: 15     / done:7228  
---- GetTokenSimulation --------------------------------------------------------
[##########################################################################]100%
          waiting: 0      / active: 0      / done:1     
================================================================================

4.3.ワーカーノードのコンテナ起動時刻

ワーカーノードのコンテナ起動時刻は以下のようになっており、gatlingでエラーが発生した時刻と一致しています。
...
app.test/app                                    2021-12-28 15:55:09 +0900 JST
app.test/nginx                                  2021-12-28 15:55:08 +0900 JST
rancher/hyperkube:v1.21.6-rancher1              2021-12-28 15:55:07 +0900 JST
rancher/rke-tools:v0.1.78                       2021-12-28 15:54:33 +0900 JST
rancher/mirrored-coreos-etcd:v3.4.16-rancher1   2021-12-28 15:48:19 +0900 JST
...
...
app.test/app                                    2021-12-28 15:56:00 +0900 JST
app.test/nginx                                  2021-12-28 15:55:57 +0900 JST
rancher/hyperkube:v1.21.6-rancher1              2021-12-28 15:55:30 +0900 JST
rancher/rke-tools:v0.1.78                       2021-12-28 15:55:24 +0900 JST
rancher/mirrored-coreos-etcd:v3.4.16-rancher1   2021-12-28 15:49:01 +0900 JST
...

4.4.検証結果

RKEでの更新中にgatlingで502エラーが発生したため、RKEによるバージョンアップ時にアプリケーションでダウンタイムが発生するという検証結果になりました。

ワーカーノードの更新時にcordonを実行してノードの切り離しは出来ているようですが、これは検証したPodがDaemonSetになっているためだと思われます。運用中の本番環境ではPodがDaemonSetになっているためDaemonSetでのみ検証を行いましたが、本番環境では前回のバージョンアップ時のようにクラスタを切り離してバージョンアップするのが良さそうです。

5.コンテナランタイムについて

Kubernetes v1.24からはDockerをコンテナランタイムとして使えなくなりそうなので、次回のバージョンアップに向けてコンテナランタイムの更新も置き換えの検討も必要になりそうです。今のところ置き換えの候補としてはcontainerdを考えていますが、RKE2ではcontainerdをコンテナランタイムとして使えるようなので、Kubernetes環境の構築ツールも引き続きRKEが使えそうです。

6.まとめ

今回の検証で使用したDaemonSetではうまく無停止更新できませんでしたが、他の種類のPodであれば無停止で更新できそうに見えます。Kubernetesの導入初期段階でアプリケーションのfrontendを確実に各ノードに分散するために採用しましたが、ノードを更新する際は不便なところもあります。

そろそろDocker以外のコンテナランタイムも検討が必要な段階になってきています。運用時はDockerの機能を使うこともあるので別のコンテナランタイムに置き換える際はそういった部分もKubernetes側の機能で運用することも考慮に入れながら移行を進める必要がありそうです。

次世代システム研究室では、アプリケーション開発や設計を行うアーキテクトを募集しています。アプリケーション開発者の方、次世代システム研究室にご興味を持って頂ける方がいらっしゃいましたら、ぜひ 募集職種一覧 からご応募をお願いします。

皆さんのご応募をお待ちしています。

Pocket

関連記事