k8s1.22 on EKS) 同じノードで多数のポッドが短時間で作成および完了すると、新たに配置されたポッドのステータスが「OutOfCpu」になる可能性がある
Kubernetes本体のIssue(1.22.9
/ 1.23.6
/ 1.24.0
で修正済みのためclosed)
AMIのIssue(2022/06/17に解決して閉じた)
コメントの日本語訳
これを認識し、詳細を提供していただきありがとうございます。本当に感謝しています。
他のリリースラインでいくつかのコミットを取り込むように取り組んでいるので、同じ努力でこれに対処しようと思います。
EKSでは、2022年4月から1.22が使用できるようになっていた
2022/05/18現在、EKS最適化Amazon Linux AMIの最新バージョンが1.22.6
aws ssm get-parameter --name /aws/service/eks/optimized-ami/1.22/amazon-linux-2/recommended/release_version --query "Parameter.Value" --output text
1.22.6-20220429
ワーカーノードを1.22に上げてしまった場合の対応は
このログは、kubeletがポッドイベント(updateType=0(SyncPodWork))を処理している間にポッドのすべてのコンテナが終了した場合の
PodWorkers.podSyncStatuses
とk8s APIサーバー間の競合状態を示していると思います。
kubeletは、このイベントがupdateType=0(
SyncPodWork
)であったため、PodWorkers.acknowledgeTerminating()
を呼び出さずにポッドがCompleted
であると報告しました。
kubernetesスケジューラは、k8s APIサーバーの状態に基づいてスケジュールされているため、
Completed
ポッドから要求されたリソースを考慮せずに新しいポッドをスケジュールしました。
その結果、Kubelet.filterOutTerminatedPods()
がKubelet.HandlePodAdditions
によって呼び出されたときに、Completed
ポッドをフィルタリングしなかったため、新しいポッドがOutOfCpu
状態になる可能性があります。
大まかに言って、スケジューラーはポッドフェーズの成功または失敗のみをポッドの終了状態と見なし、ノードに別のポッドを配置するスペースがあると見なしますが、kubeletは、ポッドフェーズが成功または失敗し、ポッドがアクティブに終了中ではないという両方の条件を考慮します(!kl.podWorkers.IsPodTerminationRequested(p.UID))が満たされると、ポッドはターミナルになります)。したがって、これは一貫性がなく、v1.21とv1.22+のkubeletsの違いでもあります。
何回も読んだら半分くらいわかってくるかもしれない
これは意図的な設計上の選択でした。Kubeletは終了中のポッドのリソースを考慮する必要があります。終了している最中で終了しきっていないポッドと、強制的に削除された(APIに表示されない)ポッドは、依然としてリソース(たとえば、ボリュームや予約済みCPU)を消費しています。以前は、この場合、ノードがサポートできる範囲を超えて消費がバーストすることを許可していました(そして、排他的リソースがまったく割り当てられない可能性がありました)。
これによりスケジューラとkubeletの間の競合が発生することに同意しますが、変更を加えなくても、システムで観察できるよりも多くのリソースがkubeletにコミットされる可能性があります(強制的に削除されたポッドは、完全に終了するまではリソースを消費します)。そして、これらの一時的なブリップに関係なく、システムはそれを最小限に抑えるために全体として機能する必要があります。
実際には、ほとんどの実稼働ワークロードは、削除後に終了するのにゼロでない時間がかかります。スケジューラーは熱心です-それはすぐに配置しようとします。スケジューラー+kubeletは、ネットワーク要求を任意に遅らせることができる分散システムを形成するため、スケジューラーが古くなっていることにより、kubeletは着信要求を拒否する可能性があります。全体的な目標は、この種の競合を最小限に抑えることです。
スケジューラーの意欲を低下させることはできますが、それでも複数のスケジューラーが実行されているシナリオが必ずしも修正されるわけではなく、重要なことに、バーストシナリオに対する回復力が向上するわけではありません。変更を元に戻すと、ノードの復元力が低下します。これを競合状態と考えると、問題をより効果的に組み立てるのに役立ちます。
削除は1つの方法であり、最終的には終了中のポッドは終了することがわかっています。ただし、これらのポッドを開始できるようになるまでにどのくらいの時間がかかるかは予測できません。現在のアドミッション拒否パスは、スケジューラーにポッドを別のノードに配置する機会を与える場合があります(ポッドを拒否すると、ワークロードコントローラーが別のノードに移動できる可能性のある別のポッドを作成します)。これは保証されていません。
考慮すべきシナリオがいくつかあります。
- 排他的なリソース(分離されたCPU、クラウドシステム上のPVスロット、固有のデバイス)へのアクセスを必要とするポッド
- チャーンが多いノード(複数のポッドの開始と終了)
どちらの場合も、ポッドアドミッションの新しいカテゴリを追加することを想像できます。ポッドは、他の終了ポッドが最終状態に達すると、近い将来実行される可能性があります。現在のKubeletsはそのプロパティを保証しないため、これは理想的には順序付けられていないリストです(ノードが再起動し、以前よりもメモリが少ない場合、kubeletはポッドを半任意に拒否します)。アドミッションをチェックするとき、ポッドがその境界(アクティブ+終了中)内に収まる場合、ポッドがハードアドミッションを通過することを許可しますが、そのポッドにリソースを割り当てたり、それらのリソースが解放されるまで開始したりしません。
これには、kubeletのアドミッションにいくつかの変更(潜在的に重要)が必要になります。 kubeletポッドのアドミッションを検証フェーズとミューテーションフェーズ(apiserverなど)に分割する必要があり、cpu-manager(およびその他?)などのアドミッションプラグインを変更して対応する必要があります。ポッドは検証され(このポッドは実行可能である)、受け入れられ(近い将来、このPodの要求を満たすことができる)、ポッド同期ループでソフトアドミッションに到達すると、リソースを割り当てる必要があります(このPodを開始できる)。割り当てが失敗した場合は、それをクリーンアップするように注意する必要があります(停止したポッドへの部分的な割り当てのセットは不良です)。同様に、ポッドのソフトアドミッションが長すぎる場合は、ただ終了したいと思うでしょう(ただし、それを許可すると、エンドユーザーを待つだけで安全であるという認識が生まれるかどうかを検討する必要があります)。
簡単に言えば:
- ポッドハードアドミッションは、不可能なリクエストをブロックする必要があります(8コアがある場合に16コアをリクエスト)
- ポッドハードアドミッションでは、終了中ポッド+アクティブポッドを考慮して、将来可能なリクエスト(空きが0のときに2つのコアを要求しますが、ポッドp2が終了すると2つが解放されます)を許可する必要があります(8つの利用可能なコア、6つの実行中、0の待機中、2つの終了中がある場合、2つのコアを受け入れることができます)。ポッドハードアドミッションは、合計8コアを超えて受け入れてはなりません。
- ポッドソフトアドミッションは、アドミッションされたがまだ起動されていないものを開始できるかどうかを決定する責任があります(2つの解放されたCPUが存在するまで待ってから、そのポッドを開始できます)。
- リソースのすべての「割り当て」はソフトアドミッションで行われる必要があり、割り当てに失敗すると、割り当てられたリソースは、時間T以内にアドミッションプラグイン内でクリーンアップされる必要があります。
- もちろん、これは潜在的に競合しうる状況であるため、アドミッションプラグインがクリーンアップループによって実行中のポッドのセットを通過する方が、自分で実行するよりもおそらく良いでしょう(フォローアップが必要です)
ポッドが次のフェーズの間を通過するときに、これする場合があります。
candidate
(ポッドはノード上で実行されているとは見なされていませんが、APIまたは静的ポッドを介して表示されるようになりました)
- pod config loopによって追跡
admitted
(定義されたリソース制約を前提としてこのポッドを実行できますが、一部のポッドが終了するまで実行できない場合があります)
- ハードアドミッションでゲート
- pod config loopによって追跡
syncing
(ポッドが必要とするすべてのリソースが利用可能であり、ポッドはそれらのリソースを消費できます)
- ソフトアドミッションでゲート
- pod workerによって追跡
terminating
(ポッドはまだkubeletで実行されているため、ポッドが使用するリソースはすべて、アドミッションによって考慮される必要があります)
- pod workerによって追跡
terminated
(ポッドは、リソース消費の観点から無視できます)
既存の用語:
active pods
(1.22より前では、これは「終了し始めていない許可されたポッド」を意味していましたが、これは間違っていました。現在は、終了していない許可されたポッドを意味します)admitted pods
(ハードアドミッションを通過したポッドである必要があります)
現在のバグは、リソースがハードアドミッションで割り当てられていることと、終了中のポッドのためにリソースが将来解放されるポッドのアドミッションをkubeletが許可しないことです。
(https://github.com/kubernetes/kubernetes/issues/106884#issuecomment-1005328268)
kubeletが終了したポッドのリソースを処理できるという設計アイデアを理解しました。そして、将来的にハードアドミッションとソフトアドミッションを導入するのは素晴らしいデザインだと思います。この問題だけでなく、より一般的な問題も解決します。
ちなみに、kubelet 1.22の実装はこの不安定な動作をしており、1.22より前には存在しません。
少なくとも、クラスターには多くのバッチジョブがあり、多くのポッドが同じノード上で短時間で開始および完了するため、私が管理するクラスターはこの動作の影響を非常に受けます。
社内でパッチを適用したkubelet(#106955)をクラスターにデプロイすることで、この問題に対処しました。
kubeletがこの競合状態をうまく処理するまで、回避策(PR#106955など)を導入することを提案したいと思います。
k8sを1.21から1.22に更新するユーザーに役立つと思います。
(https://github.com/kubernetes/kubernetes/issues/106884#issuecomment-1005860495)
提案された回避策は、他のクラスターを壊した修正を後退させます。 実際の問題を修正する方がよいでしょう。私は、あなたが見ている影響を最小限に抑えるために優先順位を付けることが重要であることに同意します。
1.22より前のKubeletは、すべてのケースでリソースの終了を考慮していなかった(壊れていた)ため、1.22より前には存在しませんでした。また、他の競合状態がありました。あなたが見ている振る舞いにつながる修正の変更の結果、これは間違いなく望ましくありませんが、kubeletsが受け入れたポッドを実行できるかどうかについてのかなりの不確実性を取り除きます。
(https://github.com/kubernetes/kubernetes/issues/106884#issuecomment-1005864944)
説明されている#106884(コメント)のような変更を行う際には、ポッドがソフトアドミッションに到達するが、ポッドの終了を待機するために、複数の異なる専用リソースのセット(cpus + pvスロット)が必要になる可能性も考慮に入れる必要があります。リソースは同時に準備ができている可能性があります。現在の「割り当ててからエラー時に解放する」が削除されます(現在、単一のスレッドがハードアドミッションチェックを実行しますが、ソフトアドミッションは明示的に並列です)。偶発的なデッドロックを最小化し(すべてのソフトアドミッションが順番に行われた場合でも、少なくとも1つのポッドが続行されます)し、誘導される遅延を最小化するために、syncPodでソフトアドミッションが割り当てられたリソースに対してより効果的な「予約->割り当て->ロールバック」フローを追加する必要がある場合があります。
(https://github.com/kubernetes/kubernetes/issues/106884#issuecomment-1005891654)
この問題をキャッチしたであろうe2eテストの説明(追加する必要があります)
- 1つ以上のポッドリソース(1〜5秒)を消費する既知の短時間で実行される多数のポッドを作成します
- ポッドが停止したら、sum_resources(アクティブ+終了ポッド)>max_resources_on_nodeとなるように別のポッドを作成します
- ポッドがアドミッションに失敗しないことを確認します
私はポッドがアドミッションに失敗したことをe2eがチェックすることを疑っていますが、確認していません(またはそのようなテストは存在せず、これらの変更とともにその状態で追加されました)。そのため、1.22での変更は、「間違った」ユーザーエクスペリエンスに固定されています。
これは、分散システムの課題のカテゴリに当てはまります。
k8sでは、プレーンポッドの使用はアンチパターンです。ジョブを使用するときは、再試行を許可する必要があります。間違いなく、再試行から除外するシナリオをより細かく制御する必要があります。この問題を再検討する予定です#17244
とはいえ、この設計変更は、クラスターが1.22にアップグレードするときに既存のワークロードに影響を与える可能性があります。したがって、少なくとも、リリースノートでユーザーに警告する必要があります。おそらくここ? https://github.com/kubernetes/kubernetes/blob/master/CHANGELOG/CHANGELOG-1.22.md#known-issues
話を緩和策に戻すと...
ソフトアドミッションの代替として:ポッドが完全に終了したかどうかをスケジューラが認識できるようにする新しいポッド条件を設定することは可能でしょうか?
もちろん、ポッドの強制削除は依然として問題になります。
ソフトアドミッションの代替として:ポッドが完全に終了したかどうかをスケジューラが認識できるようにする新しいポッド条件を設定することは可能でしょうか?
つまり、スケジューラーがポッドの終了を待機するように切り替えて、新しいポッドをスケジュールできるようにするためですか?はいの場合、1.22での変更(この動作を変更)により、ポッドのステータスを確認することでこれが可能になるはずです(ポッドのライフサイクルに対する修正の一部は、kubeletが利用可能になる時期についてより具体的でした)。ポッドが終了(deletionTimestamp set)としてマークされ、すべてのコンテナのステータスが終了状態になると、kubeletが完了します。まだバグがある可能性はありますが、以前よりも上手くいっています。
このような緩和策の1つの欠点は、アプリの更新をローリングするための知覚可能な遅延が、おそらくより劇的なユーザーの目に見える影響を与える方法で増加する可能性があることです。そして長期的には、kubeletのアドミッションはそのままではかなり正しくありません。強制的に削除されたポッドがアドミッションで考慮されない場合(#104824)がいくつかあり、すぐに修正する必要があります。
他の緩和策
いくつか話をしましたが、スケジューラーを含まない緩和策は、kubeletのアドミッションを修正するよりも厳密に悪いものでした。ほとんどは、実際の修正のさまざまなサブセットを実行したが、より多くのユーザーに見える動作を公開したkubeletアドミッションへのハックでした。より多くの案を受け入れる余地があります。
リリースノート
はい、起草します。
この時点で、スケジューラーの関与を検討する必要があります。変更が些細なことだと言っているわけではありませんが、おそらく調査する価値があります(cc @ ahg-g @ Huang-Wei)。ただし、kubeletバージョンのスキューに問題がある可能性があります(古すぎるkubeletはコンテナの状態を入力しません)。パッチを適用できる最も古いkube-schedulerバージョンは何ですか?バージョンスキューポリシーを正しく思い出せば、1.24だけですか?
この変更に関連して開かれている問題が他にもあります。別の問題があります(Argo workflows):#107679
振り返ってみると、kubeletの変更はsig-schedulingと調整されている必要があります。
ポッドの強制削除のケースに適切に対処できるとは思いません。APIサーバーに状態がないため、スケジューラーはそれについて何もできません。
@smarterclayton Kubeletがポッドのステータスを失敗/成功に更新した場合、ポッドが終了し、そのリソースが他のポッドで利用可能であると実際に見なされた場合はどうなりますか?
「OutOf{CPU| Memory}」は、マルチスケジューラが実行されている場合にも発生する可能性があります。つまり、限られたリソースを求めて競合する同じポッドにポッドをスケジュールすることを競う可能性があります。ユーザーの観点からすると、ランディングノードのリソースが解放されない限りOutOf {CPU|Memory}状態に永久にとどまる代わりに、最悪でも最終的にはポッドを他の場所でスケジュールできる必要があります。問題を軽減するためにUnBindingAPIを定義できるかどうか気になっています。
- ポッドがOutOf{CPU| Memory}状態に長時間留まっていることを観察すると、kubeletはUnBindingAPIを発行します
- APIサーバーはポッドの.spec.nodeNameを削除します
- スケジューラーはイベントに適切に対応します(ポッドを内部キャッシュから削除して、再スケジュールできるようにします)
マルチスケジューラの問題は、おそらくスケジューラ間でノードを分割することで解決する必要があります。
ポッドのライフサイクルはすでに複雑です。これは複雑さを増すだけであり、ポッドの状態/フェーズについて推論するのが難しくなります。
Kubeletがポッドのステータスを失敗/成功に更新した場合、ポッドが終了し、そのリソースが他のポッドで利用可能であると実際に見なされた場合はどうなりますか?
可能ですが、これはポッドのエンドツーエンドのレイテンシの大幅な低下であり、ポッドの終了には kubeletのさまざまな競合のためリソースのクリーンアップに最大30秒かかる可能性があるため、ここで行った変更よりもエンドユーザーに2桁の影響を与える可能性があります。これは、ジョブ、バッチ、knative、およびローリングリスタートのワークロードにかなり強い影響を与え、ほとんどすべてのユーザーに表示されると思います。そのため、多くのテストを行わないうちはかなり慎重になります(それを考慮すべきではないというわけではありません)。
ここでの本当の課題の1つは、SLI測定とSLOにおいて、変更の影響がどのようなものであったかを実際に言えるほどにkubeエンドツーエンドは十分に厳密ではないことです。 短いオーバーラップの周りのオーバーサブスクリプションの影響を最小限に抑える必要があります。 私たちは間違いなく、ライフサイクルの懸念があるkubeletの排他的な内部リソースを正確に表現したいと考えています。 また、ポッドの作成->ポッドの開始->ポッドの終了->ポッドの表示の成功/失敗からの合計遅延を大幅に削減したいと考えています。 私はこれを体系的に着手したいので、端から端までの角度を見て、いくつかの制約を課してから、いくつかのオプションをテストすることで着手できるかもしれません。
@smarterclayton私の理解は正しいですか?
kubeletは、リソースが解放されており他のポッドで利用可能であると見なす前に、ポッドがAPIでターミナルであると記録し、リソースが不足しているために他のポッドの承認を拒否します。
最初の部分について、kubeletは常にそれを行ってきました。 2番目の部分は、非終端状態のポッドがまだある場合、kubeletが新しいポッドを拒否することです。 kubeletが、apiserverがターミナルであることを通知したターミナルポッドに対して新しいポッドが拒否されているという証拠がある場合、それは確かにバグです(おそらく、リファクタリングによって可能になったものか、より厳格なために単に公開されたものです)。
#106884(コメント)は問題を正しく特定します。
→このスクラップでいうとこれ
IsPodTerminationRequestedが広すぎます。つまり、その状態と「終了していない」の交差であるブールチェックが必要です。つまり、終了中とコンテナの終了の間のウィンドウのみです。現在、終了中のポッドと、終了したがクリーンアップされていないポッドを拒否しています。
このリグレッションを防ぐe2eは、かなり簡単に記述できるはずです。
- ボリュームの破棄のためにかなり長い終了期間があり、コード0で終了する前に1秒間実行され、ノードを制約するポッドを作成します(おそらく、一意のリソースまたは単にCPUが原因です)
- 最初のスケジュールの直後に2番目のポッドを作成します。これは、最初のポッドがターミナルになるとスケジュールする必要があります(1のリソースの競合のため)
node_e2eは、重要なティアダウン時間やノードリソースを使い果たす可能性のあるボリュームを実際には使用していないため、これをキャッチできないと思います。これを確実に、または非常に大量にトリガーするには、コンテナーの停止からボリュームのクリーンアップまでの間に些細ではない時間が必要です。
2番目の注意(bobby in slackから)-これは一意性を必要としないコアkubeletリソース(CPU、ポッド)のみを修正します-リソースを解放するために終了後の割り当てを実行する必要がある場合、アドミッションで割り当てられたリソースはまだ壊れています(つまり、アドミッションチェックが終了後のクリーンアップループを待機する必要がある場合、アドミッションチェックは失敗します。失敗した場合、これらのコンポーネントのバグになります)。 CPUとデバイスは、アドミッション時に割り当ての現在の状態を確認すると思いますが、それを間違えるのは簡単で、確認する必要があります。
Succeeded: All containers in the pod have terminated in success, and will not be restarted. Failed: All containers in the pod have terminated, and at least one container has terminated in failure. The container either exited with non-zero status or was terminated by the system.
ポッドではなく、すべてのコンテナの状態に関するものです。コンテナを気にするコントローラは、次のアクションを実行できるように、このプロパティをできるだけ早く更新することに依存しています。
残念ながら、これがポッドオブジェクトの唯一のプロパティであり、スケジューラはリソースがポッドによってまだ消費されているかどうかを判断するために使用できます。 「ボリューム」などのポッドレベルのリソースの場合、リソースがノードで解放されたかどうかを判断する良い方法は実際にはありません。
ここでkubeletとschedulerの間に必要なものは、コンテナーのステータスではなくポッドリソースの状態であるように思われますか?
怠惰にクリーンアップされた(調整する必要がある)リソースに基づいてkubeletがポッドを拒否するべきではないことを考えると、成功と失敗で十分なはずです。
将来のコンテナを開始できず、すべてのコンテナが停止することが100%確実になるまで、フェーズをターミナルに設定しないでください。 現在のコードは最初の部分を実行できると思います(これを検出するときsyncPodにいるため、syncPodが終了しても、将来のコンテナーは作成されません)が、2番目の部分は実際にはkubeletの観点からは正確ではない可能性があります(考える必要があります)。
最も簡単な修正(読んでください:おそらくシャットダウン待ち時間以外は劇的に後退しない可能性が高い)はおそらく次のとおりです。
Kubeletがターミナルフェーズを検証するまで、Kubeletはapiserverにターミナルフェーズへの移行について通知しないでください(podWorker.CouldHaveRunningContainers()はfalseを返します)
これによりレイテンシが追加されますが、管理しにくいわけではありません。ポッドが最終段階にあることを検出した場合は、syncPodを早期に終了することをお勧めします。
私は本当に排出をどうするかわかりません-上記の提案された修正では、排出を開始した後にkubeletが再起動すると、ポッドを排出しようとしたという知識が失われます。短期的にレイテンシーを受け入れてから、明確なメッセージを持つ新しい条件Evictedを追加し、条件を検出した場合は、それを単に排出を記録したかのように扱います。それには本当に他の利点があります...
ターミナルフェーズがapiserverに到達するのを設定する更新を正しく遅らせることは、他のステータス更新に影響を与えずに安全に実装することは困難です。別の微妙な方法で後退するリスクを冒さずに、変更を行う方法について考えるのに少し時間が必要になります。
Issueを閉じたPR
コントロールプレーンを1.22に上げてしまったクラスターで、ノードグループのバージョンも1.22に上げてしまった場合、それを1.21に戻すことはできない
コントロールプレーンを1.22に上げてしまったクラスターで、1.21のノードグループを新規作成することもできない
起動テンプレートの側でAMIを指定することで、1.21のワーカーノードを作成することができる。起動テンプレートではイメージIDを指定するのでこんな感じで取る。
aws ssm get-parameter --name /aws/service/eks/optimized-ami/1.21/amazon-linux-2/recommended/image_id --query "Parameter.Value" --output text
AMIカタログを検索するならamazon-eks-node-1.21-v2022
とかで検索する。
起動テンプレートでAMIを指定したとき、userdataも入力しておかないと、NodeCreationFailure: Instances failed to join the kubernetes cluster
となってノードグループの作成が失敗する。
userdataではbootstrap.shを実行して最低限クラスター名を引数に入力する。
#!/bin/bash
/etc/eks/bootstrap.sh my-cluster-name