2024年08月19日
連載 第9回
karpenterの安全な使用方法
karpenterはノードのコストを最適化するために、ノードの数を積極的に変更します。新規のPodを生成するために新しいノードが実行すると、Podに設定されたリクエスト容量で最もコストが安いノードを新規に生成し、既存ノードの中で使用量が低いノードは削除し、既存ノードで実行中のPodを新しいノードへ移します。この作業で、既存のPodは削除されて新しいPodが開始します。Podが再開しても顧客サービスに影響を与えないためには、以下のオプションが必要です。
TerminationGracePeriodSeconds、PodDisruptionBudget、PriorityClass
今回はこのオプションを確認します。
1. Graceful Shutdown (安定的な終了)
ユーザの増減によりPodオートスケールが発生してノード数が増減すると、karpenterはコストを最適化するようにノードの数とタイプを変更します。
従来のクラスターオートスケールに比べてより頻繫にノード数を変更します。karpenterが自動的にノードを変更し、EC2ノードコストが以前に比べて約30%減少しました。しかし、ノードが終了するとノードで実行中のPodも同時に終了させられるので、サービスに影響を与える可能性があります。そのため、Podが安全に終了する設定が必要です。
クバネティスはノード(又はユーザ)が終了(以下Terminating)信号を受信すると、Podは下記のプロセスによりPodを終了します。
① ユーザ、ノードなどでterminating信号を受信すると、Podはサービスエンドポイント(endpoint)で除外されて新しいリクエストを受信しません。そして、コントロールプレーン(Control plane)の etcdデータベースにもノードの状態はterminating状態に変更されます。
② Podがterminating状態に変わると、クバネティスはPod内の全てのコンテナにpreStopハンドラーを実行できるようにリクエストします。preStopハンドラーはPodが終了する前に、コンテナが実行すべき整理処理を実行するときに使用されます。例えば、コンテナがデータベースに変更事項を保存するなどの終了準備ができます。preStopの設定はマニュフェストに指定する必要があります。
③ preStopハンドラーの処理が完了すると、クバネティスはPodの全てのコンテナへSIGTERMシグナルを送ります。SIGTERMはgraceful shutdownのためのシグナルで、コンテナにプロセスを終了するようにリクエストします。このシグナルを受信したコンテナはpreStop整理処理を終えて自然に終了します。
④ Podはこの後でterminationGracePeriodSecondsに指定された時間待機します。この待機の間にコンテナは終了準備の処理をします。時間内に終了できないとPodは強制的に終了されます。PodのterminationGracePeriodSecondsのデフォルトの設定時間は30秒で、それ以上の時間が必要であればYAMLファイルの設定で変更します。筆者は4時間を設定する場合もあります。
⑤ terminationGracePeriodSeconds時間が経過後もコンテナが終了しない場合は、クバネティスはSIGKILLシグナルを送って強制的にコンテナを終了します。SIGKILLは強制terminatingシグナルで、コンテナが決まった時間に終了できない場合に使用されます。
既存のセッションが維持すべきVoIPなどの電話サービスの場合、セッションを正常終了する必要があります。もし、karpenterによりノードが終了されてPodが異常終了すると、ユーザの電話が切断されるため障害となります。電話中の顧客のサービスセッションは最後まで維持する必要があります。そのため、このようなサービスの設定は、VoIPなどの電話と関連Podは別の専用ノードで実行し、Podが終了する待機時間をterminationGracePeriodSecondsで設定し、例えば、4時間(14,400s)と設定し、全てのセッションを正常終了させてPodが終了できるようにします。この設定により、Podがterminating信号を受信しても4時間の追加時間を置いて、その時間に既存の通話が終了できるように待機します。
例で確認します。
busybox-graceful-shutdown-pod.yaml
1. apiVersion: v1
2. kind: Pod
3. metadata:
4. name: pods-termination-grace-period-seconds
5. spec:
6. containers:
7. - command:
8. - sleep
9. - "3600"
10. image: busybox
11. name: pods-termination-grace-period-seconds
12. terminationGracePeriodSeconds: 5 # Time to wait before moving from a TERM signal to the pod's main process to a KILL signal.
terminationGracePeriodSeconds時間を5秒に設定しました。Podは終了リクエストを受信すると直ちに削除できず5秒間、正常終了できるまで待機した後で削除されます。
Podを終了して5秒間維持しているかを確認します。busybox Podを実行します。
1. (jerry-test:default)k8s-class:jerry$ cd karpenter/
2. (jerry-test:default)k8s-class:jerry$ k apply -f busybox-graceful-shutdown-pod.yaml
3. pod/pods-termination-grace-period-seconds created
Podを終了して別ウィンドウで ‘k get pod -w(wait)’ コマンドを使用して、リアルタイムでPodの変更事項を確認します。
1. (jerry-test:default)k8s-class:jerry$ k delete pod pods-termination-grace-period-seconds
2. pod "pods-termination-grace-period-seconds" deleted
1. (jerry-test:default)k8s-class:jerry$ k get pod -w
2. NAME READY STATUS RESTARTS AGE IP NODE NOMINATED NODE READINESS GATES
3. pods-termination-grace-period-seconds 1/1 Running 0 3s 10.110.42.118 ip-10-110-47-109.ap-southeast-1.compute.internal <none> <none>
4.
5. pods-termination-grace-period-seconds 1/1 Terminating 0 14s 10.110.42.118 ip-10-110-47-109.ap-southeast-1.compute.internal <none> <none>
6.
7. pods-termination-grace-period-seconds 0/1 Terminating 0 20s 10.110.42.118 ip-10-110-47-109.ap-southeast-1.compute.internal <none> <none>
上記で確認できるようにTerminating(1/1)コマンドを受信して5秒後に完全に終了、Terminating(0/1)されました。つまり、直ちに終了せずに5秒間の猶予時間を維持しました。上記のような既存セッションを維持すべきサービスにはterminationGracePeriodSecondsオプションを設定し、既存のサービスが完全に正常終了するのに必要な時間を待ちます。
筆者はPodが終了しても既存セッションを維持すべき全サービスのPodに、Graceful Shutdownの設定を追加しました。
2.PodDisruptionBudgetの設定
karpenterがノードを停止すると、ノードで実行中のPodは終了して、他のノードで新しいPodが実行されます。terminatingコマンドを受信したPodは先にTerminating状態に入り、次の実行可能な他のノードで実行(Running)されます。先にTerminatingして次のPodを実行するため、既存のPodが終了して新しいPodが実行する(Terminating -> Running)間にサービス停止現象が発生することがあります。単純ですがサービス停止は障害ですので、サービス停止が発生しないようにクバネティスでPodDisruptionBudget(Pod停止状態の予算)の設定を追加できます。
PodDisruptionBudget(以下、PDB)はPodの停止を制御するポリシーです。PDBを使用するとクラスター内のPodの停止時点における制限を設定して、特定のPodが停止するまでの間でアプリケーションの可用性と安定性を維持できます。
PDBを理解するため次のようなシナリオを考えて見ます。
仮定する状況は以下の通りです。
- クラスターにWebサービスをサポートする3つのPodがあります。
- このサービスでは可用性を高めるために3つのPodが全て稼働中です。
- もし、2つ以上の Podが同時に停止すると、サービス停止又は障害が発生することがあります。
上記の問題の発生を防止するためにPDBを設定して次の規則を定義できます。
- MinAvailable:維持すべきPodの最小数を指定します。例えば、’MinAvailable: 2’を設定すると、最小2つのPodが常に稼働します。
- MaxUnavailable:同時に停止する可能性があるPodの最大数を指定します。例えば、 ‘MaxUnavailable: 1’を設定すると、同時に停止できるPodは1つだけです。
上記のようにサービス特性に合わせてPDBを適切に設定すると、安定したサービスを提供できます。
実習で確認します。実習のシナリオは1つのノードで2つのPodを実行し、それらのPodにPDBを設定します。ノードをドレイン(Drain、他のノードへ移動)、つまり、ノードのPodを他のノードへ移動します。このときにPDBを設定すると1つ(minAvailable)のPodは移動しないで、元のノードで継続的に実行されます。
実習用のDeploymentリソースを以下の様に定義します。
busybox-nodeSelector.yaml
1. apiVersion: apps/v1
2. kind: Deployment
3. metadata:
4. name: busybox
5. labels:
6. app: busybox
7. spec:
8. replicas: 2
9. selector:
10. matchLabels:
11. app: busybox
12. template:
13. metadata:
14. labels:
15. app: busybox
16. spec:
17. containers:
18. - command:
19. - sleep
20. - "3600"
21. image: busybox
22. name: busybox
23. nodeSelector:
24. kubernetes.io/hostname: ip-10-110-47-109.ap-southeast-1.compute.internal
前に使用したBusybox Deployment Manifestと同じですが、nodeSelectorオプションを追加しました。nodeSelectorは名前から推測できるように、指定したノードでPodを実行するオプションです。上記の例は特定ホスト名のノード(ip-10-110-47-109.ap-southeast-1.compute.internal)でPodを実行する設定です。各EKSノードのホスト名を入力してマニュフェストファイルを変更します。
PDBもクバネティスリソースで、YAMLファイルで実行できます。設定は下記の様に非常に簡単です。
busybox-pdb.yaml
1. apiVersion: policy/v1
2. kind: PodDisruptionBudget
3. metadata:
4. name: busybox
5. namespace: default
6. spec:
7. minAvailable: 1
8. selector:
9. matchLabels:
10. app: busybox
. spec.minAvailable: 1
Podが終了しても実行すべきPodの最小数です。アプリケーションの特性により適切に数を調整します。
. spec.selector.matchLabels
PDBを適用するPodを指定します。サービスリソースがPodを指定するときselectorを使用することと同じLabel基準でTarget Podを指定します。
上記の2つのリソースを配布します。
1. (jerry-test:default)k8s-class:jerry$ cd podDisruptionBudget/
2. (jerry-test:default)k8s-class:jerry$ k apply -f busybox-pdb.yaml -f busybox-nodeSelector-deploy.yaml
3. poddisruptionbudget.policy/busybox created
4. deployment.apps/busybox created
PodDisruptionBudgetはクバネティスリソースで下記のコマンドで確認できます。
1. (jerry-test:default)k8s-class:jerry$ k get pdb
2. NAME MIN AVAILABLE MAX UNAVAILABLE ALLOWED DISRUPTIONS AGE
3. busybox 1 N/A
更に、詳細な情報を確認します。
1. (jerry-test:default)k8s-class:jerry$ k describe pdb busybox
2. Name: busybox
3. Namespace: default
4. Min available: 1
5. Selector: app=busybox
6. Status:
7. Allowed disruptions: 1
8. Current: 2
9. Desired: 1
10. Total: 2
11. (省略)
. Selector
Label(app=busybox)基準でPDBを適用するリソースを選択しました。
. Status: Allowed disruptions: 1
停止(disruptions)可能なPodの数が‘1’です。Target対象のbusybox Podを1つ(まで)終了できます。
PDB、デプロイメントリソースが実行中です。ノードを終了してPDBが正常動作するかを確認します。ノードで実行中のPodを他のノードへ強制移動するdrainコマンドを使用します。
karpenterはノードの数を最適化するconsolidation(統合)の処理を進めると、ノードにPodを実行できないようにして(cordon)、ノードにあるPodを他のノードへ移動(drain)するステップを実行します。その状況を再現するためにbusybox Podが実行中のノードを移動します。
Podが実行中のノードの情報を確認します。
1. (jerry-test:default)k8s-class:jerry$ k get pod -o wide
2. NAME READY STATUS RESTARTS AGE IP NODE NOMINATED NODE READINESS GATES
3. busybox-5784bfcbcd-2p9cj 1/1 Running 0 8m31s 10.110.37.91 ip-10-110-47-109.ap-southeast-1.compute.internal <none> <none>
4. busybox-5784bfcbcd-gnf9l 1/1 Running 0 8m31s 10.110.39.167 ip-10-110-47-109.ap-southeast-1.compute.internal <none> <none>
nodeSelector設定で2つのPodが全てip-10-110-47-109.ap-southeast-1.compute.internalのノードで実行中です。このPodのノードを移動します。k drainコマンド実行時に–ignore-daemonsetsと–delete-emptydir-dataのオプションを追加します。
1. (jerry-test:default)k8s-class:jerry$ k drain ip-10-110-47-109.ap-southeast-1.compute.internal --ignore-daemonsets --delete-emptydir-data
2. node/ip-10-110-47-109.ap-southeast-1.compute.internal cordoned
3. (省略)
4. error when evicting pods/"busybox-5784bfcbcd-2p9cj" -n "default" (will retry after 5s): Cannot evict pod as it would violate the pod's disruption budget.
5. (省略)
6. error when evicting pods/"busybox-5784bfcbcd-2p9cj" -n "default" (will retry after 5s): Cannot evict pod as it would violate the pod's disruption budget.
7. (省略)
–ignore-daemonsetsはノードで実行中のDaemonSet Podを終了するオプションで、–delete-emptydir-dataは特定のPodがノードの一時ディレクトリを使用させます。ノードを削除すると、データを使用できない警告メッセージを表示しました。他のノードでPodが再開可能なPodであることを確認して、そのオプションを追加してコマンドを実行します。
drainコマンドの実行時に出力されたメッセージを確認すると ‘busybox-5784bfcbcd-2p9cj’ のPodがEvict(ノードのリソースを確保するために kubeletがPodを停止)で、他のノードへ移動できません。つまり、PDBの設定で1つのPodは必ず実行しなければならないため、他のノードへPodが移動できないということです。
他のターミナルウィンドウで確認すると、1つのbusybox Podは実行中です。
1. (jerry-test:default)k8s-class:jerry$ k get pod -o wide
2. NAME READY STATUS RESTARTS AGE IP NODE NOMINATED NODE READINESS GATES
3. busybox-5784bfcbcd-2p9cj 1/1 Running 0 12m 10.110.37.91 ip-10-110-47-109.ap-southeast-1.compute.internal <none> <none>
4. busybox-5784bfcbcd-5d4cc 0/1 Pending 0 2m24s <none> <none> <none> <none>
このようにPDBを設定すると、karpenterがノードを終了してもminAvailableの設定で該当する Podは必ず実行されます。サービスを安定的に運用するためには必要なデフォルト設定で、筆者はこの設定を実運用の環境サービスでも使用しています。Karpenterの使用環境で必ず設定をお勧めするオプションです。次は実習に使用したdrainなどのノードスケジューリングのコマンドの詳細を確認します。
3.ノードスケジューリングの設定 (drain、cordon、uncordon)
クバネティスはクラスターのノードを柔軟に管理し、Podの移動とスケジューリングを調整して安定性と可用性を保証できるようにdrainとcordon、uncordonコマンドを提供します。
drainコマンドはノードを安全に非活性化し、ノードの Podを他のノードへ移動(マイグレーション)するときに使用します。これはノードの整備や、クラスターのバージョンアップ、ノードの障害などの状況に対して有用です。実務で特定のノードに障害が発生して、ノードのPodを他のノードへ移動するときや、クラスターのバージョンをアップグレードするときに使用します (drainの実習は以上で終了です)。
次にcordon/uncordonコマンドはノードのスケジューリング状態を調節するために使用するコマンドです。ノードのスケジューリング状態を変更して、新しいPodのスケジューリングを許可したり、ブロックしたりできます。ノードをcordonすると、ノードにこれ以上新しいPodはスケジューリングされません。cordonは主にノードの障害や、整備、アップグレードなどで、ノードを一時的に使用できないようにするときに使用します。
Cordonの作業が終了すると uncordonコマンドを実行します。Cordonでスケジューリングが停止されたノードを再度活性化して、ノードに新しいPodのスケジューリングを許可します。cordon状態でノードを障害から回復するときと、整備が完了して再度使用可能にするためにuncordonコマンドを使用します。
上記のPDBのテストを終了して、ノードで新しいPodがスケジューリングできるようにするために uncordonコマンドを実行します。
先ず、ノードの状態を確認します。ip-10-110-47-109-*のノードは drainコマンド実行後で、 SchedulingDisabled状態です。
1. (jerry-test:default)k8s-class:jerry$ k get nodes -o wide ip-10-110-47-109.ap-southeast-1.compute.internal
2. k NAME STATUS ROLES AGE VERSION INTERNAL-IP EXTERNAL-IP OS-IMAGE KERNEL-VERSION CONTAINER-RUNTIME
3. ip-10-110-47-109.ap-southeast-1.compute.internal Ready,SchedulingDisabled <none> 8d v1.26.4-eks-0a21954 10.110.47.109 <none> Amazon Linux 2 5.10.178-162.673.amzn2.x86_64 containerd://1.6.19
テストが完了したため、uncordonコマンドでノードのcordon状態を解除します。
1. (jerry-test:default)k8s-class:jerry$ k uncordon ip-10-110-47-109.ap-southeast-1.compute.internal
2. node/ip-10-110-47-109.ap-southeast-1.compute.internal uncordoned
ノードを確認すると、再度Podを実行できる状態(Ready)になりました。
1. (jerry-test:default)k8s-class:jerry$ k get nodes -o wide ip-10-110-47-109.ap-southeast-1.compute.internal
2. NAME STATUS ROLES AGE VERSION INTERNAL-IP EXTERNAL-IP OS-IMAGE KERNEL-VERSION CONTAINER-RUNTIME
3. ip-10-110-47-109.ap-southeast-1.compute.internal Ready <none> 8d v1.26.4-eks-0a21954 10.110.47.109 <none> Amazon Linux 2 5.10.178-162.673.amzn2.x86_64 containerd://1.6.19
uncordonコマンドが正常に実行されました。
今回はkarpenterを適用した状態で安全にPodを終了できるGraceful Shutdown、Pod Disruption Budgetと追加でノードのdrainとcordon/uncordonを確認しました。
PDBのテストが完了したため、デプロイメントとPDBの設定を削除します。
1. (jerry-test:default)k8s-class:jerry$ k delete deployments.apps busybox
2. k ddeployment.apps "busybox" deleted
3.
4. (jerry-test:default)k8s-class:jerry$ k delete pdb busybox
5. poddisruptionbudget.policy "busybox" deleted
1. 参考までに、‘kubectl rollout restart {deploy_name}’ コマンドを実行すると、先ず、Podを実行(running)し、実行が完了すると既存のPodを終了(terminating)状態に変更します。
2. PDBの詳細は次のURLを参照してください。https://kubernetes.io/docs/tasks/run-application/configure-pdb/
3. nodeSelectorに関する詳細は次回のadvancedSchedulingで説明します。