GKEのアップグレードのログを眺める

2024/12/14に公開

Google Cloud Japan Advent Calendar 2024の14日目の記事です.

Kubernetesを運用しているとどうしても避けられないのがアップグレードです.Google Cloud上のマネージドなKubernetes環境を提供するGKEでも例外ではありません.
GKEには様々なアップグレードタイミングをコントロールして影響を抑えることができる機能がありますが、それでもアップグレードの仕組みを知らなければ予期しない一時的なワークロードへの影響を与えてしまう可能性があります.

この記事では、アップグレードで何が起きるか説明し、その様子をログなどから観察する方法を説明します.

GKEのアップグレードを図で理解する

今回は主にStandardモードのGKEクラスタについて説明します.(Autopilotクラスタでも基本的には同じです)
GKEのアップグレードについて、公式ドキュメントの「Standardクラスタのアップグレード」を見ると大まかにどのような考慮事項が存在するか記述されています.
この記事ではせっかくなので何が起きているか図解して説明します.

アップグレードの図解をする前に、クラスタの通常の状態を図解しておきます.

コントロールプレーン

コントロールプレーンでは、kube-apiserverやさまざまなコントローラーが動作しています.ユーザがkubectlを用いて通信をする先も、このkube-apiserverです.
Kubernetesでは特定のリソースが作成されると、それに応じて実際のリソースが構成されたり、あるいは別のKubernetesリソースが作成されます.例えば、Deploymentを作成すると、ReplicaSetが作られますがこの役割はkube-controller-managerの中のdeployment-controllerが行います.また、GKEではIngressを作成するとLBをユーザプロジェクト内に構成しますがこれもコントロールプレーン上のコントローラの一つが行う役割です.

ノードプール
ユーザのワークロードが動作するノードプールは、実際にはマネージドインスタンスグループ(MIG)に含まれるGCEのインスタンスによって構成されています.MIGのインスタンスなのでそれぞれ同一のMIGテンプレートをもとに作成されています.ベースとなるOSイメージに加えてKubernetesのノードに必要な、kubeletcontainerdなどのkubeletの動作上必要な様々なプログラムが含まれた状態のイメージがMIGのテンプレートに含まれています.ノードプールが複数のゾーンにまたがっている場合には、ゾーンごとにMIGが作成され、一つのノードプールに複数のMIGが所属します.

ノードプールのアップグレード

ノードプールのアップグレードは「サージアップグレード」、「Blue/Greenアップグレード」に大別されます.ここでは「サージアップグレード」だけ説明します.
また、以下の説明ではGKEのノードプールを作成するときのデフォルトの設定である「Max Unavailable=0」、「Max surge=1」という前提で説明します.

  1. ノードプールアップグレードの最初のステップでは、GKEは新たなMIGインスタンステンプレートを作成します.こちらはノードのイメージに紐づいているのでこのイメージの中には新しいバージョンのOS、kubeletなどが含まれています.
    今回の場合、「Max surge=1」なのでノードを消す前に一つ余分にノードを追加します.


2. ノードプールに定めたサイズ+1つのノードができると、旧バージョンのノードが一つdrainされます.

2-1 . drainされると、ノードにnode.kubernetes.io/unschedulable=:NoScheduletaintがつき、tolerationを保持しているPod以外そのノードにスケジュールされないようになります.

2-2. コントロールプレーンは該当ノード上で稼働するDaemonSetによって管理されていないPodに紐づくevictionサブリソースを作成します.もし、対象のPodを消すとPod Disruption Budget(PDB)が満たされない場合にはこの作成リクエストはブロックされます.

2-3. evictionサブリソースが作成されるとkubeletは対象のPodをgracefulに削除を試みます.

  1. 対象ノード上のDaemonSetによって管理されていないPodとStatic Pod以外が削除されるとノードが消されます.削除が終了したら、全てのノードがアップグレードされるまで1に戻ることを繰り返します.

クラスタのアップグレード

クラスタのアップグレードではGoogle Cloudが管理しているコントロールプレーン側のノードを再作成します.
クラスタのアップグレード時にはコントロールプレーン側で「maxUnavailable=1、Max surge=0」のアップグレードのようなことが起きます.

Zoneクラスタでは、コントロールプレーンに1台しか用いていないので単にコントロールプレーンのノードが存在しないタイミングが存在します.一方、Regionalクラスタでは3台のノードがいて1つ1つノードの削除、作成が行われます.




また、クラスタのアップグレード時には、コントロールプレーンの更新だけでなく、DaemonSetとしてデプロイされているユーザーのワーカーノード上で稼働しているワークロードも更新されます.

HA構成なコントロールプレーンのコンポーネントとLease

クラスタのアップグレードのコントロールプレーン上での動きを理解するためには、Kubernetesの各種コントロールプレーン上のコンポーネントが一般的にどのようにHA構成を成し遂げるか理解する必要があります.Regionalクラスタでは、3台のコントロールプレーンのノードにそれぞれ同じコントロールプレーンのコンポーネントが起動しています.

ステートレスなkube-apiserverは3台ともActive-Active構成で動作しています.

それ以外の様々なコントロールプレーン上の要素kube-scheduler,kube-controller-managerなども3つのノードで1つずつ稼働しています.これらも例えばkube-schedulerがそれぞれ同じPodに違う宛先を同時に指定したりなどの競合が発生しないためにもリーダーだけが稼働しています.KubernetesではこれをLeaseリソースを用いて行っています.

実際、kubectlを用いてLeaseリソースを確認してみることができます.

$ kubectl get leases -nkube-system kube-scheduler -oyaml
apiVersion: coordination.k8s.io/v1
kind: Lease
metadata:
  creationTimestamp: "2024-11-13T04:27:45Z"
  name: kube-scheduler
  namespace: kube-system
  resourceVersion: "26966130"
  uid: a2b2fe95-da96-4e69-abf3-9e38440fa51f
spec:
  acquireTime: "2024-12-06T09:00:52.064820Z"
  holderIdentity: gke-d1b8861fffcb46c0b38c-f2e7-f783-vm_97f68005-6e5c-4e80-a535-526ae376c2a6
  leaseDurationSeconds: 15
  leaseTransitions: 1
  renewTime: "2024-12-09T09:29:07.105575Z"

このリソースをwatchしてみると、数秒おきに更新されており、renewTimeが更新されていることがわかります.この更新はリーダーとなっている対応したコントロールプレーンのコンポーネントから行われており、これがleaseDurationSecondsを超えるとスタンバイ状態のコントロールプレーンのコンポーネントがリーダーに取って代わりLeaseを更新し始め、リーダーとして稼働し始めます.

複雑になってしまいましたが、要するに以下のようなところが要点となります.

  • kube-apiserverは3台とも稼働しているActive-Active構成である
  • それ以外のHA構成な要素はLeaseリソースを継続的に更新し、これが更新されなくなった時に別のコントロールプレーンのコンポーネントがリーダーを肩代わりする

クラスタアップグレードで何が起きる?

kube-apiserverへの影響
Regionalクラスタでも、コントロールプレーンのアップグレードの過程では接続しているkube-apiserverの稼働するノードがシャットダウンするタイミングに繋げようとしていたリクエストがタイムアウトしたり、接続が切られたりします

Regionalクラスタでは、クライアントがたまたまそのタイミングで、シャットダウンされるコントロールプレーン上のkube-apiserverと通信している場合には、既存のkube-apiserverへの接続が閉じられます.再接続すればつながるはずですので問題は解消しますが、Kubernetes APIを用いるワークロードが接続が閉じた際の再接続を考慮していなかったりすると問題になります.Zonalクラスタでは、コントロールプレーン側で再度新しいバージョンのコントロールプレーンが上がってくるまで接続がタイムアウトします.Zonalクラスタでは数分間程度Kubernetes APIサーバを使うことができないタイミングが存在します.

Leaseの更新間隔による影響
Regionalクラスタでも、Leaseを持っているリーダーとなるコントロールプレーンのコンポーネントを動作させているノードがシャットダウンされると、他のノード上の対象のコントロールプレーンのコンポーネントがLeaseを新たに獲得しリーダーに昇格するまでの数秒間のリーダー交代の時間があります.
例えば、kube-schedulerのリーダーがアップグレードでシャットダウンされると、最大15秒ほど他のノードのkube-schedulerがリーダーに昇格する必要があると判断するまでのラグが生じます.
実際には別の待機状態の別のコントロールプレーンのコンポーネントも定期的にこのLeaseを確認しているだけですから、実際に自身がリーダーに移り変わる必要があると判断するにはさらに追加で時間を要します.

どのようなコントローラーがどの程度の間隔で最悪Leaseを更新するべきかどうかは、以下のように確認できます.

$ kubectl get leases -nkube-system -ojson | jq '.items[]|[.metadata.name,.spec.leaseDurationSeconds]|@csv' -r
"addon-manager",15
"addon-resizer",15
"apiserver-3azxvx3ymvln5hwpjticjxdo4a",3600
"apiserver-aiktatzwgznnrhzs4fchu2eynm",3600
"apiserver-hxgrmcsccw3sktgkfgmaaup7c4",3600
"cloud-controller-manager",15
"cloud-provider-extraction-migration-pt2",15
"cluster-autoscaler",15
"cluster-kubestore",15
"clustermetrics",15
"external-attacher-leader-pd-csi-storage-gke-io",15
"external-resizer-pd-csi-storage-gke-io",15
"external-snapshotter-leader-pd-csi-storage-gke-io",15
"gcp-controller-manager",15
"gke-common-webhook-lock",15
"ingress-gce-lock",15
"ingress-gce-neg-lock",15
"kube-controller-manager",15
"kube-scheduler",15
"maintenance-controller",15
"managed-certificate-controller",15
"pd-csi-storage-gke-io",15
"service-steering.networking.gke.io",15
"snapshot-controller-leader",15
"vpa-recommender",30

なお、apiserver-から始まるLeaseに紐づく時間は、1.26から追加されたAPI ServerのIdentityによるものなのでリーダー選出の時間とは関係ありません.

このラグによって例えば以下のようなことが起きえます.

  • Podは作成後、kube-schedulerによってノードにスケジュールされるタイミングまでの時間のラグが大きくなる
  • Jobは作成されたのにそのJobが作成するPodがkube-controller-manager内のjob-controllerにより生成されるタイミングまでの時間のラグが大きくなる

その他システムリソースの更新

クラスタをアップデートすると、クラスタにデフォルトで含まれているシステムワークロードや、その動作に必要なConfigMapRoleやCRDなど様々なリソースも変更されます.

ワーカーノード上に含まれているシステムワークロードはコントロールプレーン側のバージョンと紐づいています.例えば、gke-metadata-servergke-metrics-agentなど様々なシステムワークロードはDaemonSetとしてデプロイされており、これらシステムワークロードはコントロールプレーンの更新時に一緒に更新されます.

この影響は実際に更新されるシステムワークロードにより様々ですが、例えば以下のような例があります.

  1. gke-metrics-agentが更新されることにより、一時的にノードのメトリクスが無い瞬間が生じる
  2. gke-metadata-serverが更新されたタイミングで、ワークロードがWorkload Identityを用いようとするとメタデータサーバと通信が一時的にできず認証に失敗する

ただし、どれも影響は数秒に収まる限定的なものですので、リトライにより多くの場合は問題の影響はかなり軽減されます.

場合によっては、多少のリソースのrequst量の上下があり得ます.システムワークロードが多い、様々な機能が有効化されているStandardクラスタで、ギリギリのサイズのノードで運用していた結果、システムワークロードが更新されて少しだけrequest量が上がった結果、今までユーザが動かしていたギリギリのサイズのワークロードがUnschedulableになってしまうといったケースもあります.
特に追加でDaemonSetで動作させているワークロードがある場合には、システムワークロードのリソースリクエストとリクエスト量の合計をノード全体のリソース量から引いても十分にワークロードをスケジュールできるだけの容量があるか確認しておきましょう.

アップグレード関連のログを観察して理解する

実際にアップグレードを行った際にワークロードが影響を受けたとして、ログからアップグレードの様子を見れなければ次のアップグレードの際の影響を軽減することができません.
アップグレードのタイミングそのもののログクエリは公式ドキュメントを参考にしていただき、この記事ではアップグレードに付随して生じる様々な現象のログの確認方法を深掘りたいと思います.

ノードプールアップグレード

PDBのログを見て振る舞いをログから確認する

GKEではkube-apiserverに対する操作のログは監査ログとして記録されます.一部のログはDATA_WRITE監査ログを有効にしなければ残りませんが、リソースの作成、削除は少なくとも監査ログに残ります.この監査ログを用いてPodに関連する変更、およびPodのサブリソースへの変更は、例えば以下のようにクエリできます.

resource.type="k8s_cluster"
protoPayload.resourceName:"core/v1/namespaces/<調査するPodの名前空間>/pods/<調査するPodの名前>"

実際にクエリしてみると、以下のように途中までevictionサブリソースの作成をブロックされ、あるタイミングで作成が完了するとそのすぐ後にPodが消えているのがわかります.

evictionリソースの作成がブロックされている時間を見ることで、PDBによってPodの削除がどの程度の時間抑止されているかわかります.PDBの設定が例えばPodが1つしかないのにmaxUnavailableが0等に構成されていると、このログが1時間の間観測され、最終的に強制的にPodが終了されます.結果としてどの程度の時間PDBを待つのかどうか、最終的にPDBが満たされて消されているかどうかわかることで、アップグレード時にワークロードに一瞬の障害が起きた際に適切にPDBが設定されていたか振り返るのに有用です.

クラスタアップグレード

Leaseのログを見て振る舞いをログから確認する

Leaseの更新がしばらくされず、別のコントロールプレーンのコンポーネントがリーダーになるとLeaderElectionのk8sイベントログが発生します.

例えば.以下のようなクエリでCloud Loggingからリーダー変更に関するイベントのログを確認できます.

LOG_ID("events")
jsonPayload.reason="LeaderElection"
kube-schedulerのリーダー変更ログの例

.jsonPayload.involvedObject.nameからどのLeaseに関連しているリーダーが移り変わったのかが分かる.

{
  "insertId": "1xdsvs5f4e1557",
  "jsonPayload": {
    "message": "gke-d1b8861fffcb46c0b38c-64e6-ef22-vm_6d326d93-de8d-41ce-ac08-d85707f13db9 became leader",
    "lastTimestamp": "2024-12-09T12:15:55Z",
    "metadata": {
      "name": "kube-scheduler.180f814e8fa4a30b",
      "namespace": "kube-system",
      "creationTimestamp": "2024-12-09T12:15:56Z",
      "managedFields": [
        {
          "apiVersion": "v1",
          "operation": "Update",
          "manager": "kube-scheduler",
          "fieldsType": "FieldsV1",
          "fieldsV1": {
            "f:involvedObject": {},
            "f:source": {
              "f:component": {}
            },
            "f:message": {},
            "f:count": {},
            "f:reportingComponent": {},
            "f:lastTimestamp": {},
            "f:type": {},
            "f:reason": {},
            "f:firstTimestamp": {}
          },
          "time": "2024-12-09T12:15:56Z"
        }
      ],
      "uid": "da6f72f4-ffa3-4f19-bbaa-23cd47b3067b",
      "resourceVersion": "19165"
    },
    "type": "Normal",
    "reportingComponent": "default-scheduler",
    "reason": "LeaderElection",
    "reportingInstance": "",
    "eventTime": null,
    "apiVersion": "v1",
    "source": {
      "component": "default-scheduler"
    },
    "kind": "Event",
    "involvedObject": {
      "kind": "Lease",
      "namespace": "kube-system",
      "uid": "a2b2fe95-da96-4e69-abf3-9e38440fa51f",
      "apiVersion": "coordination.k8s.io/v1",
      "resourceVersion": "27085286",
      "name": "kube-scheduler"
    }
  },
  "resource": {
    "type": "k8s_cluster",
    "labels": {
      "cluster_name": "standard",
      "location": "us-central1",
      "project_id": "tse-kakeru"
    }
  },
  "timestamp": "2024-12-09T12:15:55Z",
  "severity": "INFO",
  "logName": "projects/tse-kakeru/logs/events",
  "receiveTimestamp": "2024-12-09T12:15:57.984663951Z"
}

イベントログだけでなく、LeaseリソースそのものへのpatchリクエストもKubernetes監査ログとして記録されています.kube-schedulerの更新するLeaseの監査ログとLeaderElectionイベントログを確認して見ると振る舞いがよくわかります.

Cloud Loggingのログフィルタ
(
    resource.labels.cluster_name="standard" AND
    protoPayload.resourceName="coordination.k8s.io/v1/namespaces/kube-system/leases/kube-scheduler" AND
    protoPayload.methodName:"update"
) OR
(
    LOG_ID("events") AND
    jsonPayload.reason="LeaderElection" AND
    jsonPayload.involvedObject.name="kube-scheduler"
)

以下のスクリーンショットでは、このクエリに加えてprotoPayload.requestMetadata.callerIpをログの概要行に加えてみました.

このログを見てみると以下のようなことがわかります.

  • 普段、kube-schedulerのLeaseリソースはおよそ2秒間隔で更新されている様子が確認できます.
  • イベント発生する直前のLeaseの更新から17秒開いて新たなLease更新がされるとともにイベントが発生しています.
    kube-schedulerのLeaseのleaseDurationSecondsが15秒なので、別のkube-schedulerがLeaseが更新されないことを見てリーダーに昇格したことがわかります.実際にはleaseDurationSecondsに加えて、Leaseを確認している間隔の時間、リーダーになり変わって稼働を開始するための時間があるため、厳密にはleaseDurationSecondsよりも若干長引く場合が多いです.
  • ログの概要欄に表示したリクエストもとのIPを見てみるとイベントの前後で変わっていることがわかります.

リージョナルクラスタでは、3つ別々のゾーンに存在するコントロールプレーンにこれらコントロールプレーンのコンポーネントが動作しています.実際、これはコントロールプレーンをアップグレードした時に一つのコンポーネントにつき、最小1回、最大3回のリーダー変更イベントが起きることからも確認できます.つまり、リージョナルクラスタでも最大leaseDurationSeconds + αの時間掛ける3回、kube-schedulerkube-controller-managerが動作していないタイミングが生じることになります.なお、参考までにzonalクラスタで行った場合にはこの長さは5分程度生じていました.

通常、スケジュールや各種リソースのReconcilingにかかる時間を考えると決して長い時間ではありませんが、例えば毎分実行されるCronJobなどがあって、実行タイミングが遅れると困る場合等には気をつけておくと良いかと思います.

終わりに

GKEのアップグレードに関連する挙動についてかなり深く見てきました.
GKEに限らずKubernetesを使う上でアップグレードは避けられない年に数回行わなければんらないオペレーションです.GKEではアップグレードを安全に、確実に行うために「メンテナンスウィンドウの設定」や「ロールアウトシーケンシング」、「ノードプールアップグレードにおけるBlue/Greenアップグレード」などの便利な機能を提供しています.

しかし、アップグレードの振る舞いを理解し、事前にワークロード側にも対策をしていただくと、殆どゼロに近いダウンタイムが多くの種類のワークロードで実現ができます.ぜひ、今回の記事を契機にクラスタをアップグレードした際のログを見ていたいて、その間にワークロードに問題は起きなかったか、起きたとして次回のアップグレードでは問題が起きないように対策ができるかなど検討してみてください.

Google Cloud Japan

Discussion