kube-scheduler
は Kubernetesにおけるデフォルトスケジューラで、 Pod
を Node
に Bind
する責務(+α)を担っています。
この文章は、(kube-scheduler
)のソースコード(v1.13.3
)を読み進めながら、「Pod
がどのような処理を経てNode
にBind
されるか」を理解する手助けすることを目的として書かれました。
また、2019-02-05 Kubernetes 読書会 #4の資料としても用られました。
kube-scheduler
のエントリーポイント(cmd/kube-scheduler/scheduler.go
)から順に読んでいくスタイルで解説しています。
ソースコードをいきなり読む前に
この文章では図をつかったわかりやすい解説ができていません。これまでに、自分でソースコードを読み解いて、解説スライド・記事を作ってくださっている方がいますので、そちらをまず読むと、大まかな流れがつかめて、理解が大変進みやすいです。
- Kubernetes: スケジューラの動作
- 猫でもわかる Pod Preemption
- schedulerとdevice-pluginをカスタムして機械学習ジョブクラスタを作っている話
-
The Kubernetes Scheduler
- kube-schedulerの仕様を TLA+ を使って書き下し&解説しています
- ちょっと範囲が広いですがWhat happens when ... Kubernetes edition!
-
kubectl run ...
と打ってから何が起きるかを一気通貫で概要を説明してくれています
-
対象ソースコード
-
kubernetes/kubernetes at v1.13.3 - Github
- Sourcegraph extension を入れておくと読みやすいです
- kubernetes/kubernetes - Sourcegraph (こちら直接でも可)
パッケージ構造
CLIコマンドと、スケジューラ自身のロジックはパッケージが別れています。
cmd/kube-scheduler
CLIに関するの実装のパッケージです。
├── app # CLI実装のほぼ全てはappパッケージ
│ ├── config # scheduler本体へ渡すconfigオブジェクト用
│ ├── options # kube-schedulerのcommand line flags/option用
│ └── server.go # cliの処理本体, flag -> configへの変換(default値の補完含), scheduler本体の起動
└── scheduler.go # kube-schedulerのエントリポイント
pkg/scheduler
scheduler本体のコードが入っているパッケージです。
├── algorithm # Algorithm(predicates, prioritiesを組み合わせてSchedule(),Preempt()する)インターフェース関連
| # predicatesやpriorityもこのパッケージで定義される. extenderのインターフェースもここで定義される.
|
├── algorithmprovider # predicates, prioritiesの組を提供するAlgorithmProviderの初期化/定義
|
├── api # schedulerが外部に提供している api (types)
|
├── apis # いわゆるComponent Config用(kube-scheduler用のConfig type)
|
├── cache # kubernetes apiに負荷をかけないために、schedulerは常にキャッシュとやり取りするが、
| # そのキャッシュ内に格納されるデータ群の定義
|
├── core # Algorithm, Extenderインターフェースの実装用のパッケージ
|
├── factory # Config Factory, そのFactoryからConfigを生成するロジック(DIっぽい)用
|
├── internal # kube-scheduler内部用のパッケージ
| # scheduleing queue, cache自体の実装
|
├── scheduler.go # kube-scheduler本体(スケジューリングループ含む)
|
└── volumebinder # 名前の通りvolumebinder. pkg/controller/volume/persistentvolume/SchedulerBinderのwrapper
cmd/kube-scheduler のブート処理
さて、まずは kube-scheduler
コマンドがどう実行されるかから始めていきましょう。
- cmd/kube-scheduler/app ディレクトリがほぼ全てです
-
app.NewSchedulerCommand()が
cobra.Command
本体を作る -
app.runCommand() で command line flags から
schedulerserverconfig.CompletedConfig
が作られてapp.Run()が呼ばれる。-
schedulerserverconfig.CompletedConfig
にComponentConfig
が含まれている - kube-schedulerの挙動を変えるための
SchedulerAlgorithmSource
もここに含まれる
-
-
app.Run()の中で様々な
informer
,informerFactory
が初期化される -
pkg.scheduler.New()
が呼ばれて、pkg.scheduler.Scheduler
が作られる(以降明記が無い限り単にScheduler
と表記) - その最後にLeaderElection及び
Scheduler.Run()
が呼ばれる事によってスケジューラーループが起動します-
scheduler.Config
(下記参照)内のscheduler.Config.StopEverything
channelからシグナルを受けるまで動き続けます
-
Scheduler
インスタンスの初期化
- 初期化は
pkg.scheduler.New()
で行われる -
Scheduler
インスタンスを初期化するための設定は3ステップ
1. scheduler.factory.configFactory
を作る
-
scheduler.factory.NewConfigFactory()
を呼んで、scheduler.factory.configFactory
を作る -
scheduler.factory.configFactory
はConfigurator
interfaceを実装している(主にscheduler.Config
(これが実質のScheduler
の入力)を生成する責務) - この
scheduler.factory.NewConfigFactory()
の中で多くの初期化作業が行われる-
scheduler queueの初期化
- 各種Informerへの各種イベントハンドラの登録(スケジューラ内のキャッシュ, scheduling queueの呼び出しをたくさん登録)
-
debuggerのactivate:
SIGUSR2
シグナルによって、kube-scheduler内のnode cache, scheduling queuのダンプ、scheduler内のcacheと実際の比較結果のダンプ(logに出力)ができる
-
scheduler queueの初期化
2. 指定されたAlgorithmSourceによってscheduler.Config
(これが実質のScheduler
の入力)を作る
-
AlgorithmProvider
の場合:configFactory.CreateFromProvider()
で作られる (Providerの初期化と中身は下参照) 参照 -
Policy from File or ConfigMap
の場合:initPolicyFromFile()
orinitPolicyFromConfigMap()
でPolicy
が作られた後CreateFromConfig()で作られる -
scheduler.Config
内には、スケジューラのメインループから呼ばれる主要な挙動(podをbindするnodeを探す、preemption時のvictimを探す等はAlgorithm
でinterface化されていて、 - kube-schedulerでは
GenericScheduler
のみが実装していてGenericScheduler
はCreateFromConfig()
->CreateFromKeys()
内で作られる - この
scheduler.Config
に多くのロジックを持ったコンポーネントがDIされており、factory.go
に多くのロジックが記述されていて少し読みにくい原因になっている
3. Scheduler
instanceを生成する
- 作られた
scheduler.Config
からScheduler
instanceを生成する
Algorithm Providerの初期化
- pkg/scheduler/algorithmprovider packageで定義
-
Algorithm Provider は
predicate
の集合、priority
の集合で定義される(双方ともに名前で指定するため, predefined(in-tree)なものしか指定できない) -
defaults.init()内で registerAlgorithmProvider(defaultPredicates(), defaultPriorities())が呼ばれて
"DefaultProvider"
と"ClusterAutoscalerProvider"
が作られる - "DefaultProvider":
- predicates =
defaultPredicates()
- priorities =
defaultPriority()
- predicates =
- "ClusterAutoscalerProvider":
- predicates =
defaultPredicates()
- priorities =
defaultPriority()
-LeastRequestedPriority
+MostRequestedPriority
(Greedy Bin Packingできるようになっている)
- predicates =
Scheduler
のメインループ ScheduleOne
まずはKubernetes: スケジューラの動作 - 処理の流れをざっと読むとよい。大きく4ステップ
1. NextPod() : podをpop
- podをqueueから取り出す
- 本体はconfigFactory.getNextPod()で、queueからpopしているだけ
- つまり、bind 候補 の pod の順序はすべてこの queue から pop される順序で決まる
- queueに関しては 下記 "Topics" の節参照
2. schedule(): nodeを探す
- popされたpodに対して割当ノードを探す処理
- 本体はgenericScheduler.Schedule()で主に3ステップ
2.1.findNodesThatFit
- findNodesThatFit -> podFitsOnNode: fitするnodeを計算
- 各nodeに対して並列にチェックする
- extenderはその後呼ばれる(なぜか逐次)
-
pkg.scheduler.core.equivalence
パッケージで定義されている equivalence class cache を使ってpredicateの評価を高速化しているようだが詳細は把握できていない- 名前から察するに pod全体を 同値類 に分割して管理したいように読める
- schedulerが 毎pod に対して predicate を評価するのではなくpodを同値類に分割して管理することで、schedulerが即断(キャッシュから)できるようにしたいのだと思われる
- equivalencePodに定義されているPodの属性群から32-bit FNV-1a hashを生成してClassのidとしている
2.2. PrioritizeNodes()
2.3 selectHost()
- selectHost(): 最大ScoreのNodeを選ぶ
- 最大Score Node は 1つとは限らないので、内部に
lastNodeindex
を保持してround-robbinでルーレット的に決める - 見つからなかったら
Pod.status.conditions
を変更する - 直接queueには戻さず、イベントハンドラ(un assingedなpodのupdate)経由でqueueに戻ってくる
3a. preempt()
: 見つからなければpreemptionに挑戦
-
schedule()
結果がFitError
の場合のみ - 下記がわかりやすい
- victimを選ぶ処理の本体は
genericScheduler.Preempt()
: 主に4つのmethodからなる
3.1 genericScheduler.Preempt()
主に4つのメソッドからなる
nodesWherePreemptionMightHelp
- Selectorにマッチしない, NotReadyなnode等、自明なnodeを除外する
selectNodesForPreemption->selectVictimsOnNode: 各nodeでvictimを選ぶ
- 一旦低優先度のpodを全部抜いて(victim候補にして)、fitするかチェック
- 抜きすぎかもしれないので、fitしなくなるギリギリまでvictimを順にreprieveする
- PDB違反あり→なしの順に救済
- 各stepでは低優先度から処理されるが、PriorityでしかSortしているだけなので安定性はない
- PDB違反が起きるかもしれないことに注意(ドキュメントにも明記あり)
processPreemptionWithExtenders
- ここで extenderが呼ばれる。ここまでのphaseで選択された
Node->Victims
のmapが渡される -
Node -> Victims
の map を返せばよいだけなので原理上自分でどのvictimを選ぶかは自由だが現実的でない - 別processでの観測結果で選ぶとデータ整合性が心配
pickOneNodeForPreemption
- 複数のnodeにvictim候補がいるので、最終的に一つのnodeに絞る
- いくつかの指標で辞書式順に選択する
- PDB違反数の少ない
- victim内の最大優先度が小さい
- victimの優先度合計が小さい
- victim pod数が少ない
- 配列の先頭
3.2 Nomination を行う & VictimをDeleteする
- victimが選ばれたら、
nomination
を行う - pod がこの node の victim を殺して、preemptor としてここにスケジュールされる予定ですよ、というのを記録する
- 具体的には
pod.status.NominatedNodeName
を更新する - api経由でpod statusを更新しても、event handlerの通知を待っていたら、その通知が遅れた場合(raceが起きる)、このnodeに別podがスケジュールされてしまう可能性があるため、scheduler queueの管理しているnomination情報を更新する(このscheduler loop内でqueueを更新することで、次のpodがpopされるときにはqueue内のnomination情報が最新になっていることを保証する)
- また、schedulerの再起動、複数scheduler環境の場合にnominationを伝えるためにも必要な処理
- 具体的には
- victimをdeleteする(victimとしてnominated podがある場合にはそのclearも行う)
- preemptionが起きた場合にはここでscheduler loop終了
3b. Bind処理
-
VolumeScheduling
が有効だったらAssumePodVolumes()する(PVCを全てPVにbindする)-
persistentvolume.volumeBinder.AssumePodVolumes()
で行われる - ここではvolume binding/privsioningの詳細には触れない
-
- すべてのPVCがboundされなかったらエラーとして、
pod.status.conditions
を更新して終了 - キャッシュ内のpodの
spec.nodeName
を assume() する - ここまでで選ばれたnode(変数名
suggestedHost
)にBindする準備が整ったが、実際のBind処理は複数のスケジューラが居た場合等、失敗することがある処理なので、非同期に行えるようにキャッシュ内のpod情報は楽観的にそのnodeにしておく - 非同期にbindする
- 失敗したらassumeを解除する
Selected Topics
SchedulingQueue
- unschedulable(nodeにassumeされていない) pod のスケジュール順はこの
SchedulingQueue
から popされる順で制御される - インターフェースになっているので原理的には自分で実装できる
- 現在2つが定義されている
-
FIFO
: 単純なFIFO PriorityQueue
-
-
PodPriority
feature gateが有効ならPriorityQueue
が使われるようになっている
PriorityQueue
- 名前の通り
PriorityClass
で定義されるPriority
の値に従ってスケジュールする優先度付きキュー - 同優先度内、優先度間でstarvation (eventually に schedule候補になれない)を防ぐために色々な工夫がされている
- よく出る問題: queueへのpodの出し入れが頻繁なクラスタ(クラスタが満杯でpending podが大量)の場合、scheduleに失敗したpodが戻ってきて、先頭の方に入ってしまうと後続をずっとblockしてしまう
- 優先度間:
- 一旦
ScheduleFailed
になったら実際にqueueに戻すのを(内部的にはPriorityQueue.activeQ
) backoff することで回避 - 付け焼き刃的で完全なstarvationは防げない
- 一旦
- 同優先度内: scheduleに失敗した時刻の古い順に並べ替えることでちゃんとぐるぐる廻る用にする
- nodeやpodの状態が変化しない限り、scheduleしてもしょうがないので、unschedulable pod は 一旦
PriorityQueue.unschedulableQ
というところに入って、node, pod の status changeに伴って一気にPriorityQueue.activeQ
に映るようになっている (例: configFactory.addNodeToCache()内)
- nodeやpodの状態が変化しない限り、scheduleしてもしょうがないので、unschedulable pod は 一旦
SchedulerExtender
-
ExtenderConfig
: Policy内で記述する - extener.go(extender を直接呼ぶコンポーネント)
- genericSchedulerから下記の箇所で呼ばれている
-
Schedule()
->findNodesThatFit()
内 -
Schedule()
->PrioritizeNodes()
内 - Preempt()内
-
-
Bind()
は一回しか呼べないのでconfigFactory.getBinderFunc()
で、もしBind()
できるextenderがあったらそれを返している
PercentageOfNodesToScore
- 大きなclusterでpod毎に全nodeを評価せずに適当な割合のnode数が見つかったら探すのを早期にやめる
- 定義: KubeSchedulerConfiguration.PercentageOfNodesToScore
- ->configFactory.percentageOfNodesToScore -> genericScheduler.percentageOfNodesToScore と引き回されて
-
genericScheduler.Schedule()
->findNodesThatFit
内 で使われる - fitするnodeが必要なだけ見つかったら、predicateの計算をやめてscoringに入る
- 最低100nodeは保証される
pkg.scheduler.internal.cache.Cache
- scheduler内で保持されるcacheのインターフェース
- 基本的にschedulerはこのキャッシュの保持する状態に対してschedule判断を行う
-
Pod
とNode
が対象-
Pod
には状態遷移が規定されている(Initial
,Assumed
,Expired
,Added
,Deleted
) - bindが非同期なので、
Assumed
は一定時間後にexpireするようになっている -
Initial
,Expired
,Deleted
な状態のpodはキャッシュ内に実際には存在しない
-
-
Node
はNodeTree
というデ
ータ構造で管理されている- と言っても再帰的な構造ではなく、単に
failure-domain/{zone, region}
ごとにリストを管理しているのみ
- と言っても再帰的な構造ではなく、単に
- 実装は
cache.schedulerCache
Selected Future Improvements
- Priority and preemption is promoted to GA in 1.14
factory.go
に埋まっていたevent handler群を移動- Non-preempting PriorityClass: PreemptionされないPriorityClass(内部で実装しようと思っていたのでとても嬉しい)
-
Coscheduling(aka gang scheduling)
- KEP: このKEPでBrian Grantさんが "Gang"という名前に反対して"CoScheduling"になっている
- まだ始まったばかりらしいですが in-place update of pod resourcesに向けた作業も始まっているようです
-
Scheduler Framework
- KubeCon 2018 Seattleでleadのお二人と話しましたが、まだ優先度が決まっていなくて具体的な開発スケジュールはないらしいです(Kubernetes Meetup Tokyo #16でチェシャ猫さん@y_taka_23のLTありです!)
Scheduler開発に興味のある方へ
-
sig-scheduling
- meeting notes/recordingに目を通すのがおすすめ
- sub projectもいくつかあります(どれもまだ開発段階)
- cluster-capacity: podの定義に対して、どのnodeがどのくらい空いてるか空いて無いかを分析してくれる
- descheduler: deflagしたり, affinityを回復したりするために敢えて podをde-scheduler(delete)する(名前の通りscheduleはせず単にnodeから抜くだけ)
- kube-batch: A batch scheduler of Kubernetes for ML/BigData/HPC workload
- poseidon: http://www.firmament.ioベース(グラフ理論ベース)のscheduler
-
sig/scheduling issues
- まだまだたくさんありますので是非!
-
PFN is hiring!!
- Engineer -> MLクラスターミドルウェア の職種を参照ください