Amazon Web Services ブログ
Karpenter による Kubernetes のスケーリング: Pod Affinity と Volume Topology Awareness による高度なスケジューリング
この記事は Scaling Kubernetes with Karpenter: Advanced Scheduling with Pod Affinity and Volume Topology Awareness (記事公開日: 2022 年 7 月 18 日) を翻訳したものです。
この記事は、SUSE の Principal Technical Evangelist、AWS の Container Hero、そして HashiCorp Ambassador であるLukonde Mwila と共同で執筆したものです。
イントロダクション
クラウドネイティブのテクノロジーはますますユビキタスになってきており、Kubernetes はこのムーブメントの最前線にいます。現在、Kubernetes は様々な業種の組織での採用が広がっています。Kubernetes を適切に導入することで、これらの組織のワークロードにおいて、高い可用性、スケーラビリティ、回復力を実現できます。Kubernetes をクラウドコンピューティングの特徴である比類のないスケーラビリティ、弾力性と組み合わせることで、コンテナ化されたアプリケーションの回復力と可用性を強化することができます。
この紹介記事 (日本語翻訳:Karpenter のご紹介 – オープンソースの高性能 Kubernetes Cluster Autoscaler) で詳しく説明していますが、Karpenter の目的は、クラスターのワークロードが必要な時に必要なだけのコンピュートを利用できるようにすることです。
直近のアップデートで Karpenter は、Pod Affinity、Pod Anti-affinity、Pod Topology Spread、Node Affinity、nodeSelector、Resource Requests など、より高度なスケジューリング制約のサポートを追加しています。この記事では特に podAffinity
、podAntiAffinity
、Volume Topology Awareness について掘り下げ、それらが最も適しているユースケースについて詳しく説明します。
前提
この記事のサンプルを実行するためには、Karpenter がインストールされた AWS 上の Kubernetes クラスターが必要です。今回は検証のために Amazon EKS を使用します。Karpenter をアドオンとして、Terraform EKS blueprints を活用することで、 EKS クラスターの構築プロセスを自動化できます。
Pod Affinity と Pod Anti-affinity のスケジューリング
スケジューリング制約の Pod への適用は、Pod と特定の Node、または Pod 間の関係を確立することで実現されます。後者は Inter-Pod Affinity として知られています。Inter-Pod Affinity を使用することで、他の Pod との関係に基づいて、どの Pod をどの Node に配置するかを決定する際のスケジューラーのアプローチに情報を提供するルールを割り当てることができます。Inter-Pod Affinity には、Pod Affinity と Pod Anti-affinity の両方が含まれます。
Node Affinity のように、要件に応じてrequiredDuringSchedulingIgnoredDuringExecution
と preferredDuringSchedulingIgnoredDuringExecution
のルールを使用して実行することができます。その名のとおり required (必須) と preferred (優先) は、スケジューリング制約がどの程度厳密か、あるいは柔軟かを表す言葉です。Pod をスケジューリングする基準が必須のルールとして設定されたら、Kubernetes は Pod がそれを満たす Node に配置されることを保証します。同様に、優先ルールを含む Pod は最も高い優先度にマッチする Node にスケジュールされます。
Pod Affinity: podAffinity
ルールは、ラベルに基づく相互に関連性のある Pod をマッチングするようスケジューラーに通知します。新しい Pod が作成されたら、スケジューラーは新しい Pod のラベルセレクターのラベル指定に一致する Pod が動いている Node の探索を引き受けます。
Pod Anti-affinity: 対照的に podAntiAffinity
ルールは一致するラベルの条件を満たした場合に、同じ Node 上で特定の Pod が実行されることを防ぎます。
これらのルールは様々な場面で役に立ちます。例えば podAffinity
は、同じ AZ あるいは Node 内に Pod を配置し、あらゆる内部的な依存関係をサポートしたり、サービス間のネットワークレイテンシを削減する際に役に立ちます。一方で podAntiAffinity
は、高可用性を目的として、 Pod を AZ あるいは Node に分散することで単一障害点を取り除く際に役に立ちます。このようなユースケースの場合、Pod Anti-affinity のために推奨される Topology Spread Constraints は、ゾーンまたはホスト名にすることができます。これは、クラスターノードの検索範囲を決定する topologyKey プロパティを使って実装することができます。topologyKey は、Node に付与されたラベルのキーです。
podAntiAffinity
の実装の例としては、CoreDNS の Deployment が挙げられます。Deployment リソース は、podAntiAffinity
ポリシーを指定しています。これは、スケジューラーが HA と VPC DNS スロットリングを避けるために、異なる Node で CoreDNS
Pod を起動するからです。Deployment の Pod Anti-affinity の topologyKey
が hostname を指定していることがわかります。さらに podAntiAffinity
を指定すると、排他的な Node 上で Pod あるいは Pod のリソースのセットを隔離することも、一部の Pod が他の Pod のパフォーマンスに干渉するリスクを軽減することもできます。
Karpenter を使うことで、追加のインフラストラクチャの設定をすることなく、ワークロードの規模に応じて、クラスター用にプロビジョニングされた新しいコンピュートが Pod Affinity ルールを満たすようにすることができます。Karpenter はスケジュールされなかった Pod を追跡し、リソースのマニフェストで定義された必須あるいは推奨の Affinity ルールに応じて、コンピュートリソースをプロビジョニングします。
Karpenter と Pod Affinity の例
この例では、同じ AZ (アベイラビリティゾーン) の Node に Pod をスケジューリングすることを必須とする podAffinity
ルールを指定した Deployment リソースを作成します。このプロセスでは、Karpenter はスケジューリングする必要がある Pod の要件を解釈し、それらの Affinity ルールを最適な形で満たすことができる Node をプロビジョニングします。
出発点として、クラスターに Karpenter Provisioner をインストールする必要があります。Provisioner は、設定の詳細とパラメータ (Node Type、ラベル、Taint、タグ、kubelet の構成、Resource Limits、サブネットとセキュリティグループアソシエーションによるクラスターへの接続など) を指定する CRD です。この例で使用される Provisioner マニフェストは以下のとおりです。
ターミナルで kubectl get nodes
コマンドを使用し、クラスターのすべての Node を取得するところから始めます。
これにより、じきにクラスターにデプロイするアプリケーションに対応して Karpenter が新しい Node を起動する前に、すでにある Node の状態を把握することができます。
その後、以下のマニフェストを使って Deployment リソースの作成にすすみます。
Karpenter はスケジュールされてない Pod を検知し、この Deployment の Inter-Pod Affinity の要件を満たす Node をプロビジョニングします。
新しく作成された Node のホスト名は ip-10-0-1-233.eu-west-1.compute.internal
です。
以下は、新しい Node の詳細の一部を抜粋したものです。
次に、適切なラベル (app=inflate
) を使用して関連する Pod を取得し、Pod がどのようにスケジュールされたかを確認することができます。
このように、6 つの Pod が新しい Node ip-10-0-1-233.eu-west-1.compute.internal
にスケジュールされており、一方で他の 2 つは podAffinity
ルールの topologyKey
にしたがって同一 AZ (eu-west-1b
) にスケジュールされていることがわかるでしょう。これらの Node はすべて同じ AZ にあるため、同じトポロジーの一部となり、スケジューリングの要件を満たすことができます。
Karpenter と Pod Anti-affinity の例
2 つ目の例では、podAntiAffinity
ルールを適用し、AZ に基づいてクラスター内の異なる Node に Pod を望ましくスケジュールします。前回と同様に、Karpenter は Pod の要件を読み取り、podAntiAffinity
の設定をサポートする Node を起動します。
前の例と同様に、クラスター内のすべての Node を取得するところから始めます。
その後、Deployment リソースの作成にすすみます。
Pod の要件に応じて Karpenter は新しい Node を起動します。
新しく作成された Node のホスト名は ip-10-0-1-69.eu-west-1.compute.internal
です。
以下は、新しい Node の詳細の一部を抜粋したものです。
前回と同様に、関連するラベルが付与された Pod を取得します。
3 つの Pod 以外のすべての Pod は、Affinity ルールの指定に従って、選択したリージョン (eu-west-1
) の異なる AZ にまたがって他の Node に分散されます。
次に、Karpenter をもうひとつの高度なスケジューリング技術である Volume Topology Awareness と一緒に使うことを検討します。
Volume Topology を意識したスケジューリング
Volume Topology Awareness 以前は、Node への Pod のスケジューリングと動的なボリュームのプロビジョニングのプロセスは独立していました。ご想像のとおり、ワークロードに予測不可能な結果が生じるという課題をもたらしていました。例えば、Persistent Volume Claim を作成すると、特定のAZ (eu-west-1a
) にボリュームが動的に作成されます。一方でそのボリュームを使用する必要がある Pod が別の AZ (eu-west-1b
) の Node に配置されると、結果的に Pod は起動に失敗します。
永続的なデータを提供するストレージボリュームに依存するステートフルなワークロードにとって、これは特に問題があります。然るべき AZ にストレージボリュームを手動でプロビジョニングすることは、非効率的かつ動的なプロビジョニングとは直感的に異なります。そこで登場するのが Topology Awareness です。
Topology Awareness は、Pod がトポロジーの要件 (今回のケースではストレージボリューム) を満たすよう Node に配置されることを担保することで、動的なプロビジョニングを補完します。Topology Awareness のスケジューリングのゴールは、 Topology リソースとワークロードの連携を提供することです。それによって、より高い信頼性と予測可能な結果を得ることができます。これは kubelet
のコンポーネントであるTopology Manager によって処理されます。つまり Topology Manager は、ステートフルなワークロードと動的に作成された Persistent Volume が正しい AZ に配置されることを担保します。
Volume Topology Awareness を使用するには、 StorageClass の volumeBindingMode
を WaitForFirstConsumer
に設定する必要があります。このプロパティは、Persistent Volume を使用する Pod によって Persistent Volume Claim が作成されるまで、Persistent Volume のプロビジョニングを遅延させます。
スケーリングイベントでは、Karpenter、スケジューラー、Topology Manager がうまく連携して動作します。これらの組み合わせにより、適切なコンピュートリソースをプロビジョニングするプロセスを最適化し、スケジュールされたワークロードを動的に作成された Persistent Volume と一致させます。
これらの技術は複数の AZ でステートフルなワークロードを実行し、信頼性のあるスケールを実現します。クラスター内のアプリケーションやデータベースをゾーンに分散させ、AZ が影響を受けた場合でも単一障害点になるのを防ぐことができます。Elastic Block Store (EBS) が AZ 固有であることを考慮した場合、ワークロードは再接続が成功したときに最初にスケジュールされたのと同じ AZ にプロビジョニングされるよう、nodeAffinity で設定する必要があります。
Karpenter と Volume Topology を考慮した例
この例では、 volumeBindingMode
が WaitForFirstConsumer
に設定されているStorageClass を使用して、20 個のレプリカを持つアプリケーションの StatefulSet を作成し、それぞれに Persistent Volume Claim を設定します。さらに、ワークロードのnodeAffinity は、eu-west-1a
AZ のトポロジーリソースにスケジュールするように指定します。
デフォルトの StorageClass を確認するためには kubectl get storageclass
コマンドを実行します。
次に、それぞれの AZ の Node を取得します。
kubectl get nodes -l topology.kubernetes.io/zone=eu-west-1a
を実行すると、以下の出力が得られます。
一方で、kubectl get nodes -l topology.kubernetes.io/zone=eu-west-1b
を実行すると、以下の出力が得られます。
Node の構成が決まったら、アプリケーションのための StatefulSet と付随する LoadBalancer Service の作成にすすみます。
アプリケーションがデプロイされた後、Persistent Volume は StatefulSet のレプリカからの要求に応じて動的に作成されます。各々は適切な AZ に作成され、結果的に各 Pod レプリカの作成が完了します。これに併せて、Karpenter はStatefulSet のコンピュート要件を満たすために eu-west-1a
に新しい Node をプロビジョニングします。
ここで kubectl get nodes -l topology.kubernetes.io/zone=eu-west-1a
を実行すると、以下の出力が得られます。
このように、Karpenter は以下の追加の Node を起動しました。
- ip-10-0-0-176.eu-west-1.compute.internal
- ip-10-0-0-4.eu-west-1.compute.internal
- ip-10-0-0-53.eu-west-1.compute.internal
- ip-10-0-0-96.eu-west-1.compute.internal
さらに、作成された Persistent Volume Claim、Persistent Volume、Pod を以下のコードで示したような適切なコマンドを実行して確認しました。
クリーンアップ
追加コストが発生することを避けるために、この記事で紹介したサンプルに関連するプロビジョニングされたインフラストラクチャをすべて削除するようにしてください。
結論
この記事では、Karpenter を使った Kubernetes のスケジューリングについて、特に Inter-Pod Affinity と Volume Topology Awareness の高度なスケジューリング技術をサポートするためのハンズオンアプローチを取り上げました。
Karpenter についてさらに学ぶためには、ドキュメントを参照し、Kubernetes Slack ワークスペースのコミュニティチャンネル #karpenter に参加するとよいでしょう。またこのプロジェクトが気に入ったら、GitHub リポジトリに Star をつけてください。
翻訳はプロフェッショナルサービスの後藤が担当しました。原文はこちらです。