GKEのアップグレードのログを眺める
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のノードに必要な、kubelet
やcontainerd
などのkubelet
の動作上必要な様々なプログラムが含まれた状態のイメージがMIGのテンプレートに含まれています.ノードプールが複数のゾーンにまたがっている場合には、ゾーンごとにMIGが作成され、一つのノードプールに複数のMIGが所属します.
ノードプールのアップグレード
ノードプールのアップグレードは「サージアップグレード」、「Blue/Greenアップグレード」に大別されます.ここでは「サージアップグレード」だけ説明します.
また、以下の説明ではGKEのノードプールを作成するときのデフォルトの設定である「Max Unavailable=0」、「Max surge=1」という前提で説明します.
- ノードプールアップグレードの最初のステップでは、GKEは新たなMIGインスタンステンプレートを作成します.こちらはノードのイメージに紐づいているのでこのイメージの中には新しいバージョンのOS、
kubelet
などが含まれています.
今回の場合、「Max surge=1」なのでノードを消す前に一つ余分にノードを追加します.
2. ノードプールに定めたサイズ+1つのノードができると、旧バージョンのノードが一つdrainされます.
2-1 . drain
されると、ノードにnode.kubernetes.io/unschedulable=:NoSchedule
のtaint
がつき、toleration
を保持しているPod以外そのノードにスケジュールされないようになります.
2-2. コントロールプレーンは該当ノード上で稼働するDaemonSetによって管理されていないPodに紐づくeviction
サブリソースを作成します.もし、対象のPodを消すとPod Disruption Budget(PDB)が満たされない場合にはこの作成リクエストはブロックされます.
2-3. eviction
サブリソースが作成されるとkubelet
は対象のPodをgracefulに削除を試みます.
- 対象ノード上の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
により生成されるタイミングまでの時間のラグが大きくなる
その他システムリソースの更新
クラスタをアップデートすると、クラスタにデフォルトで含まれているシステムワークロードや、その動作に必要なConfigMap
、Role
やCRDなど様々なリソースも変更されます.
ワーカーノード上に含まれているシステムワークロードはコントロールプレーン側のバージョンと紐づいています.例えば、gke-metadata-server
やgke-metrics-agent
など様々なシステムワークロードはDaemonSetとしてデプロイされており、これらシステムワークロードはコントロールプレーンの更新時に一緒に更新されます.
この影響は実際に更新されるシステムワークロードにより様々ですが、例えば以下のような例があります.
-
gke-metrics-agent
が更新されることにより、一時的にノードのメトリクスが無い瞬間が生じる -
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イベントログを確認して見ると振る舞いがよくわかります.
(
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-scheduler
やkube-controller-manager
が動作していないタイミングが生じることになります.なお、参考までにzonalクラスタで行った場合にはこの長さは5分程度生じていました.
通常、スケジュールや各種リソースのReconcilingにかかる時間を考えると決して長い時間ではありませんが、例えば毎分実行されるCronJobなどがあって、実行タイミングが遅れると困る場合等には気をつけておくと良いかと思います.
終わりに
GKEのアップグレードに関連する挙動についてかなり深く見てきました.
GKEに限らずKubernetesを使う上でアップグレードは避けられない年に数回行わなければんらないオペレーションです.GKEではアップグレードを安全に、確実に行うために「メンテナンスウィンドウの設定」や「ロールアウトシーケンシング」、「ノードプールアップグレードにおけるBlue/Greenアップグレード」などの便利な機能を提供しています.
しかし、アップグレードの振る舞いを理解し、事前にワークロード側にも対策をしていただくと、殆どゼロに近いダウンタイムが多くの種類のワークロードで実現ができます.ぜひ、今回の記事を契機にクラスタをアップグレードした際のログを見ていたいて、その間にワークロードに問題は起きなかったか、起きたとして次回のアップグレードでは問題が起きないように対策ができるかなど検討してみてください.
Discussion