こんにちは。最近口を開くたびに「卒論やばい」と呟いてしまいます。 @sanposhiho です。卒論やばい。
この記事はCAMPHOR- Advent Calenderの 22 日目、かつKubernetes scheduler pluginsの 1 日目(1 日目とは)の記事です。
この記事では Pod Topology Spread Constraints に関する基本情報から内部の仕組みまでの”全て”を紹介します。
Pod Topology Spread Constraints の基本情報
You can use topology spread constraints to control how Pods are spread across your cluster among failure-domains such as regions, zones, nodes, and other user-defined topology domains. This can help to achieve high availability as well as efficient resource utilization. https://kubernetes.io/docs/concepts/workloads/pods/pod-topology-spread-constraints/
Pod Topology Spread Constraints はユーザーが定義したドメインをもとにして、Pod をいい感じに分散させることを目指した機能です。 regions や zones、nodes など、Node にラベルをつけることでドメインを定義することができます。
ただし、region, zone, node に関しては well-known labels として label が用意されています。
https://kubernetes.io/docs/reference/labels-annotations-taints/
Pod Topology Spread Constraints を用いて、例えば「特定の Zone の Node に Pod が集まることを防いでおき、均等に散らばらせておくことで、一つの Zone の障害発生時に、サービスの Pod の一部のみに影響を抑える」などの要件を満たすことができます。
apiVersion: v1
kind: Pod
metadata:
name: mypod
spec:
topologySpreadConstraints:
- maxSkew: <integer>
topologyKey: <string>
whenUnsatisfiable: <string>
labelSelector: <object>
設定はこのような感じです。pod.spec.topologySpreadConstraints
に定義します。また、この例では一つの constraint しか定義していませんが、見ての通りリストになっているので、複数定義することも可能です。
whenUnsatisfiable
constraint に違反していた場合にどのように対応するかを指定します。二つの値をとります。
DoNotSchedule
: 違反している Node には、Pod をスケジュールしません。具体的にいうと違反している Node は Filter で弾かれます。ScheduleAnyway
: 違反している Node には、Pod がスケジュールされにくくなります。具体的にいうと、違反している Node に与えるスコアが減ります。
labelSelector
constraint を適応する Pod を labelSelector を用いて決定します。
maxSkew
Pod の不均等をどの程度許容するかというのを示す値です。(default は 1) 例えば、以下のような constraint が定義されているとします
topologySpreadConstraints:
- maxSkew: 2
topologyKey: kubernetes.io/hostname
whenUnsatisfiable: DoNotSchedule
labelSelector:
matchLabels:
foo: bar
そして、Node が 3 つ存在し、NodeA には labelSelector にマッチする Pod が 1 つ、NodeB と NodeC には 3 つ存在しているとします。 この場合、次に作成された labelSelector にマッチする Pod は Pod Topology Spread Constraints によって、NodeB と NodeC にはスケジュールされません。
これはmaxSkew
が 2、すなわち、「マッチする Pod の数の差を 2 つまでなら許容する」としているためです。
例えば、NodeB に新たな Pod がいってしまうと 1:4:3 となり、 ドメインのうち、最も Pod の数が少ない NodeA と最も Pod の数が多い NodeB の差が 3 となってしまうため、上記の constraint の規約に違反します。
デフォルトの Constraints
Scheduler の設定であるKubeSchedulerConfiguration
にて、クラスターレベルでデフォルトの Constraint を定義しておくことができます。
Pod に.spec.topologySpreadConstraints
が定義されている場合はそちらが優先されます。
Pod Topology Spread Constraints の内部の仕組み
ここから先は内部実装でどのように Pod Topology Spread Constraints の動作を実現しているのかの概要を解説します。
大前提として Scheduler は Scheduling Framework という仕組みに沿ってそれぞれの機能が一つ一つ別のプラグインとして実装されています。
Scheduling Framework には拡張点として、プラグインの実行されるタイミングが定義されており、拡張点ごとに役割が異なっています。一つのプラグインは一つ以上の拡張点で動作します。
https://kubernetes.io/docs/concepts/scheduling-eviction/scheduling-framework/
こういった Scheduler 全体の仕組みを詳しく知りたい方は先日公開された以下の記事を参照してください。
自作して学ぶ Kubernetes Scheduler | メルカリエンジニアリング
さて、本題に移りましょう。Pod Topology Spread Constraints の実装は基本的に以下のフォルダに収まっています。
DoNotSchedule
まずは、DoNotSchedule の実装を見ていきましょう。
DoNotSchedule
の説明時に一瞬紹介しましたが、こちらは Filter プラグイン + PreFilter プラグインとして実装されています。
Filter 拡張点は要件に合わない Node を候補から弾く役割を持つプラグインが実行される拡張点です。 そして、PreFilter 拡張点は Filter 拡張点を実行するための準備(事前計算など)を行う拡張点です。
この PreFilter、Filter 拡張点の実装は/pkg/scheduler/framework/plugins/podtopologyspread/filtering.go
にあります。Scheduling Framework において、「特定の拡張点で動作するプラグイン」というのは拡張点に対応する interface を実装することを意味します。
例えば、PreFilter と Filter の拡張点に対応する interface はこれらです。
type PreFilterPlugin interface {
Plugin
PreFilter(ctx context.Context, state *CycleState, p *v1.Pod) *Status
PreFilterExtensions() PreFilterExtensions
}
type FilterPlugin interface {
Plugin
Filter(ctx context.Context, state *CycleState, pod *v1.Pod, nodeInfo *NodeInfo) *Status
}
PreFilter の概要
では PreFilter の実装から見ていきましょう。 https://github.com/kubernetes/kubernetes/blob/master/pkg/scheduler/framework/plugins/podtopologyspread/filtering.go#L145
PreFilter では事前計算を行い、その結果を CycleState に保存しています。CycleState とはプラグイン間での情報の共有をすることができる構造体で、Pod のスケジュール開始時に毎回リセットされるものです。つまり、一つの Pod のスケジュールにおいてのみ有効なデータというイメージになります。
Pod Topology Spread に限らず、PreFilter などの事前計算を行う拡張点では CycleState にその結果が置かれる場合が多くあります。
Pod Topology Spread では以下のpreFilterState
と言う構造体が PreFilter の計算結果として CycleState の中に置かれます。
type preFilterState struct {
Constraints []topologySpreadConstraint
// We record 2 critical paths instead of all critical paths here.
// criticalPaths[0].MatchNum always holds the minimum matching number.
// criticalPaths[1].MatchNum is always greater or equal to criticalPaths[0].MatchNum, but
// it's not guaranteed to be the 2nd minimum match number.
TpKeyToCriticalPaths map[string]*criticalPaths
// TpPairToMatchNum is keyed with topologyPair, and valued with the number of matching pods.
TpPairToMatchNum map[topologyPair]*int32
}
Constraints
は “その Pod のスケジュール中に考慮すべき Constraint”が格納されています。
pod.Spec.TopologySpreadConstraints
があり、尚且つ、labelSelector
にその Pod がマッチする場合、 それをpreFilterState
のConstraints
へ- なかった場合、デフォルトの Constraint を取得して、デフォルトの Constraint のうち
labelSelector
に Pod がマッチする Constraint を選び、preFilterState
のConstraints
へ
と言う二通りの処理で、“その Pod のスケジュール中に考慮すべき Constraint”を取得してpreFilterState
のConstraints
に格納しています。
TpKeyToCriticalPaths
にはそれぞれの Topology 毎に”ドメインが保持している「labelSelector
にマッチしている Pod の数」の最小値”が map で保存されます。
TpPairToMatchNum
には ”topologyPair
が示す Node 達に「labelSelector
にマッチしている Pod」がいくつ存在するか” が保持されます。
Filter の概要
Filter では基本的に PreFilter で計算したpreFilterState
をもとにフィルタリングを行います。
constraint 毎に条件を満たしているのかを確認して行って、Node を pass して OK かどうかを判断していきます。
Node を pass して OK かどうかは以下の基準で判断されます。
'existing matching num' + 'if self-match (1 or 0)' - 'global min matching num' <= 'maxSkew'
それぞれ
existing matching num
: 現在ドメインに存在している 「labelSelector
にマッチしている Pod」 の数if self-match
: Pod がlabelSelector
にマッチしているかどうかglobal min matching num
Topology のドメインのうち、labelSelector
にマッチする Pod の数が最も少ないドメインの Pod の数
を表しています。これによって「もしスケジュール中の Pod が その Node に行った場合に いづれかの Constraint のmaxSkew
に違反しないかどうか」を判断しています。
実際にここで計算が行われ、Node を pass するかどうかを判断しています。 https://github.com/kubernetes/kubernetes/blob/a8cb4e22bf982a39ae6f8a522bd2b74fdce04620/pkg/scheduler/framework/plugins/podtopologyspread/filtering.go#L315-L326
ScheduleAnyway
こちらは Score プラグイン + PreScore プラグイン + Normalized Score プラグインとして実装されています。
Score 拡張点は Node をスコアリングするための拡張点です。 そして、PreScore 拡張点は Score 拡張点を実行するための準備(事前計算など)を行う拡張点です。
実装は全てこのファイルに存在します。 https://github.com/kubernetes/kubernetes/blob/master/pkg/scheduler/framework/plugins/podtopologyspread/scoring.go
同様に PreScore からざっくり説明していきます。
PreScore の概要
PreScore では以下のpreScoreState
を計算します。
type preScoreState struct {
Constraints []topologySpreadConstraint
// IgnoredNodes is a set of node names which miss some Constraints[*].topologyKey.
IgnoredNodes sets.String
// TopologyPairToPodCounts is keyed with topologyPair, and valued with the number of matching pods.
TopologyPairToPodCounts map[topologyPair]*int64
// TopologyNormalizingWeight is the weight we give to the counts per topology.
// This allows the pod counts of smaller topologies to not be watered down by
// bigger ones.
TopologyNormalizingWeight []float64
}
Constraints
は PreFilter と同様に”その Pod のスケジュール中に考慮すべき Constraint”が格納されています。
IgnoredNodes
はその名の通り、無視すべき Node が格納されます。
現在スケジュールしている Pod において影響を与える全ての Constraints の topologyKey を持っていない Node がこのフィールドを通して無視されます。
TopologyPairToPodCounts
はtopologyPair
毎にtopologyPair
が示す Node 上に存在する「labelSelector
にマッチしている Pod の合計」が map で格納されます。
TopologyNormalizingWeight
は Constraint 毎の点数に差がつくのを防ぐために計算されるものです。この重み付けによって、Constraint 間の点数が正規化されています。
Score の概要
Pod Topology Spread ではドメイン間の Pod の数の差を常に縮めるようなスコアリングをします。
つまり、”labelSelector
にマッチしている Pod” の数が少ないドメインに属する Node に高い点数をつけるわけですね。
そして、Score では以下によってスコアが計算されます。
'existing matching num' * 'topology weight' + 'maxSkew' - 1
existing matching num
は 現在ドメインに存在している 「labelSelector
にマッチしている Pod」 の数です。
さて、気がつきましたか?この計算だと「labelSelector
にマッチしている Pod」の数」が多い Node に高い点数がついてしまいます。
Score では「Score では本来低い Score をつけなければいけない Node に高い Score を、高い Score をつけなければいけない Node に低い Score を」つけているというわけですね。
Normalized Score の概要
そして Normalized Score です。この Normalized Score の拡張点は全ての Node に対する Score 拡張点の結果を踏まえて、Score 全てを修正することができる拡張点です。
Pod Topology Spread では Score では「Score では本来低い Score をつけなければいけない Node に高い Score を、高い Score をつけなければいけない Node に低い Score を」つけていました。
それをこの Normalized Score で各 Node へのスコアs
を以下の式で正規化します。これによって先程の逆転現象も解決されます。
framework.MaxNodeScore * (maxScore + minScore - s) / maxScore
終わりに
ということで、今回は、実装をあまり交えずに、Pod Topology Spread Constraints の概要と仕組みを紹介しました。
何か質問や「これちがくね?」などあればTwitterまでお願いします。