CloudNative Days Tokyo 2021での登壇資料です。 https://event.cloudnativedays.jp/cndt2021/talks/1184
GitHub repository https://github.com/sanposhiho/mini-kube-scheduler
自作して学ぶKubernetes scheduler入門 Kensei Nakada @sanposhiho 1
View Slide
自己紹介 中田 健誠 / Kyoto Uni (4th year) 普段はバックエンドエンジニアをしてます Twitter: さんぽし (@sanpo_shiho) GitHub: @sanposhiho Google Summer of Code 2021 @ Kubernetes member of Kubernetes / Kubernetes SIGs
もくじ 0. Kubernetes schedulerの簡単な概要を知る 1. 今回の自作に関して 2. 適当なスケジュールをするSchedulerを作る 🌟 3. Filter/Score Pluginを実装する 🌟 4. CycleStateについて知る 5. Binding Cycle の並列化について知る 6. Scheduling Cache について知る 7. Scheduling Queue をざっくり実装する 🌟 8. Event Handlerについて再度考える 🌟 🌟: 実装する章 3
このトークの目指すところ 内部実装の概要をつかむことを目標とします → Schedulerが実際に内部的に行っていることを、Schedulerが自作されていく様子を見ながら、機能ごとに段階的に学んでいきます
Kubernetes schedulerの簡単な概要を知る 5
Kubernetes scheduler PodをどのNodeに割り当てるかを決定するコンポーネント 様々なリソースの状況からPodに最適なNodeを決定する 6
Scheduling Framework 公式Doc: 「Scheduling Framework | Kubernetes」より引用 7
Scheduling Framework Scheduling Framework (の Scheduling Cycle) のうち、 ● Filter (+ PreFilter) ● Score (+ PreScore + Normalized Score ) の結果が主にNodeの決定に影響している ※ Extenderについては今回は割愛して説明します。 8
Scheduling Framework Filter Podを実行できない(したくない)Nodeを候補から除外する 例えば… ・リソース不足でPodが実行できないNode ・nodeSelectorの条件に一致しないNode を除外する 9
Scheduling Framework Score 残った候補のNodeをスコアリングする。 例えば… ・全体のNodeのリソースの使用量のバランスがちょうど良くなるNodeを優先 ・Podを実行するコンテナイメージをすでに持っているNodeを優先 10
11
12× × 何かしらの観点でNodeをフィルタリングする
13× × 何かしらの観点でNodeをフィルタリングする × × × × もしこの段階で候補のNodeが全滅した場合 → Preemption(Podの追い出し) を試みる → 諦める (Unscheduled)
14何かしらの観点でNodeをスコアリングする
15Score B プラグインのWeightが1 → 10 → 50 Score A プラグインのWeightが10 → 300 → 500
16→ 10 → 50 Node1 が最終的に選ばれる → 300 → 500 350 510
Scheduling Framework 公式Doc: 「Scheduling Framework | Kubernetes」より引用 17
仕組みざっくりまとめ Scheduler は Scheduling Framework の流れに沿って動作 ● Filter で実行できない/したくないNodeを弾き、 ● ScoreでNodeをスコアリングし、 ● 最後にPlugin Weightを加味して、 ● どのNodeでPodを実行するか決定する ● その後Binding CycleでPodをそのNodeにBindする 18
今回の自作Schedulerに関して 19
自作Schedulerに関して Kubernetes Schedulerのversion:v1.22.0 リポジトリを公開しています → https://github.com/sanposhiho/mini-kube-scheduler/ この発表の流れに沿ってブランチを切っています
自作Schedulerに関して kubernetes-sigs/kube-scheduler-simulator の仕組みを流用しており、 コマンドライン上からSchedulerの振る舞いを確認することができます。
自作Schedulerに関して scenario という形で試したいシナリオを定義することができます。 シナリオを変更したい場合は、 /sched.go のscenario関数の中身を変更してください make start でその時点のSchedulerでシナリオを実行することができます。
自作Schedulerに関して
自作Schedulerに関して 基本的には発表を見ていただくだけでなんとなく要点の実装がわかるように解説していきます。 時間に余裕がある方は、適宜一時停止しながら、 実際のコードを見たり、変更してみたり、実際に手元で動作させてみたりしてみると理解が深まると思います。
適当なスケジュールをするSchedulerを作る 25ブランチ: initial-random-scheduler
適当なScheduler 26全てのNodeからランダムに選んだNodeに対してPodをBindする
やらなければいけない事 1. ScheduleされていないPodを見つける 2. そのPodのScheduleを始める 3. 全てのNodeを取得する 4. ランダムにNodeを一つ選ぶ 5. 選んだNodeに対してPodをBindする 6. 1に戻る
ScheduleされていないPodを見つける Scheduler は内部のQueueにスケジュールされていないPodを持っている ↓ “ScheduleされていないPodを見つけてQueueに貯める仕組み” が必要
ScheduleされていないPodを見つける Queue はとりあえず Slice でシンプルに実装 ● Queue に Pod を追加するメソッドを実装 ● Queue からPodを取り出すメソッドを実装
すごいシンプルなQueue /minisched/queue/queue.go
Queue に Pod を追加するメソッド Slice にappendしてるだけの関数 /minisched/queue/queue.go
Queue の先頭を取り出すメソッド /minisched/queue/queue.go
ScheduleされていないPodを見つける EventHandler → リソースの変更をトリガーにあらかじめ登録した関数を実行してくれる仕組み 1. EventHandlerを用いて新しいPodの作成を検知する 2. 新たなPodをQueueに入れる
/minisched/eventhandler.go スケジュールされてないPodのみ をFilter 追加されたときのみ この関数を実行 →
Pod が作られたときに呼ばれるメソッド /minisched/eventhandler.go
やらなければいけない事 (再掲) 1. ScheduleされていないPodを見つける 2. そのPodのScheduleを始める 3. 全てのNodeを取得する 4. ランダムにNodeを一つ選ぶ 5. 選んだNodeに対してPodをBindする 6. 1に戻る
PodのScheduleを始める sched.scheduleOne を実行し続ける /minisched/minisched.go
PodのScheduleを始める scheduleOne がスケジューラーのメインロジック Queue の先頭のPodを取り出す /minisched/minisched.go
全てのNodeを取得する API からNodeを全て取得 /minisched/minisched.go
ランダムにNodeを一つ選ぶ /minisched/minisched.go
選んだNodeに対してPodをBindする /minisched/minisched.go
選んだNodeに対してPodをBindする Bind しているメソッド本体 /minisched/minisched.go
適当な Scheduler おさらい 1. EventHandlerを用いて新しいPodの作成を検知する 2. 新たなPodが作成されたら、それをQueueに入れる
適当な Scheduler おさらい 1. Queue のPodを取り出す 2. そのPodのScheduleを始める 3. 全てのNodeを取得する 4. ランダムにNodeを一つ選ぶ 5. 選んだNodeに対してPodをBindする 6. 1に戻る
動くか試そう node0 ~ node9 を作った後に pod1を作成 ↓動いた 👌
Filter/Score Pluginを作る 46ブランチ: filter-plugin, score-plugin
Scheduling Framework 公式Doc: 「Scheduling Framework | Kubernetes」より引用 47
適当な Scheduler += Filter/Score Plugin 1. Queue のPodを取り出す 2. そのPodのScheduleを始める 3. 全てのNodeを取得する 4. Filter Plugin で Node の候補を絞る 5. Score Plugin で 残った候補をスコアリングする 6. 残った候補の中からスコアが高いNodeを選ぶ 7. 選んだNodeに対してPodをBindする 8. 1に戻る
例として使用するFilter Plugin NodeUnschedulable Plugin Nodeの.Spec.Unschedulable を見て、trueのNodeを候補から外す (実際にSchedulerに標準で搭載されているPluginです)
Filter Pluginの実行 RunFilterPlugins というメソッドでFilter Pluginを実行。 エラーが返った場合はスケジュールをその時点で諦める /minisched/minisched.go
/minisched/minisched.go
ループでFilter PluginsをNode一つ一つに対して実行 全てのFilter PluginをpassしたNodeのみがfeasibleNodeとして 返される /minisched/minisched.go
Filter Pluginの実行 /minisched/minisched.go ひとつも候補がない場合はerrorを返す
動くか試そう Unschedulable: true のNodeを10個(node0 ~ node9)作成
動くか試そう Unschedulableの指定がないNodeを1個(node10)作成
動くか試そう ↓動いた 👌 Unschedulableの指定がないnode10にbindされている
例として使用するScore Plugin NodeNumber Plugin Nodeの名前の最後の数字がPodの名前の最後の数字と一致するNodeに高い点数をつける 例: pod1と言うPodのスケジュール時にはnode1に高いスコアをつける
NodeNumber Pluginを実装する interfaceを満たす構造体を作成することでPluginを実装できる https://github.com/kubernetes/kubernetes/blob/master/pkg/scheduler/framework/interface.go#L413
/minisched/plugins/score/nodenumber/nodenumber.go Pod の名前の最後の数字を取得 例: pod1 → 1 Node の名前の最後の数字を取得 例: node1 → 1 一致していた場合10点を返す
Score Pluginの実行 RunScorePlugins というメソッドで実行 /minisched/minisched.go
Score Pluginの実行 /minisched/minisched.go ループでScore PluginsをNode一つ一つに対して実行 別の形に加工して返却
動くか試そう node0 ~ node9 を作った後に pod1 と pod3 を作成 (Nodeを作る際、先程のspec.Unschedulableの設定は消してます) ↓動いた 👌 (pod1はnode1へ、pod3はnode3へ)
動くか試そう ↓score pluginの実行後のスコアをlogに出してみると… pod3のスケジュール時はちゃんとnode3が一番スコアが高くなっている
適当な Scheduler += Filter/Score Plugin 1. Queue のPodを取り出す 2. そのPodのScheduleを始める 3. 全てのNodeを取得する 4. Filter Plugin で Node の候補を絞る 5. Score Plugin で 残った候補をスコアリングする 6. (残った候補の中から) ランダムにNodeを一つ選ぶ 7. 選んだNodeに対してPodをBindする 8. 1に戻る
CycleStateについて知る 65
CycleStateについて 一つの Pod の スケジュール でのデータを保持している。 → Podのスケジュール開始時に毎回新しく作成される Plugin(等)はCycleStateにデータを読み書きすることができる。
PreFilter/PreScore Pluginについて Filter/Score pluginの実行の準備をできる拡張点 PreFilter / PreScore において事前計算を行い、CycleStateに結果を保存。Filter /Score ではその結果を使用し、実行パフォーマンスをあげる
先程のScorePluginのinterface interfaceを満たす構造体を作成することでPluginを実装できる https://github.com/kubernetes/kubernetes/blob/master/pkg/scheduler/framework/interface.go#L413
CycleState を使ってみる NodeNumber pluginにPreScoreを実装してCycleStateを使ってみる実装があります → ブランチ: prescore-plugin
Binding Cycle の並列化について知る 70
Scheduling Framework (おさらい) 71## Scheduling Cycle PodをどのNodeで実行するかを決定する ## Binding Cycle Scheduling Cycle での決定を実際にクラスターに適応する
Scheduling Cycle 72他のPodのスケジュール結果が確定していないと正しくスケジュールできない → 並列に実行できず、Podを一つ一つスケジュールしていくしかない 並列に実行するとダメな例: 2つのPodが同時にスケジュールされ、”1つのPodしかリソース的に実行できないNode”に2つのPodがBindされる
Binding Cycle 73スケジュールの結果は確定しているのであとはBindするだけ → 並列に実行できる!!効率化!!
Binding Cycle の並列化に関係する仕組み ● Node を予約する仕組み ● Bindのタイミングを管理できる仕組み ○ Permit Plugin / wait on permit ○ 実装済(発表時間が足りず削った) → ブランチ: permit-plugin
Scheduling Cache について知る 75
全てのNodeを取得する (おさらい) API からNodeを全て取得 /minisched/minisched.go
Scheduling Cache について知る Schedulerは実際にはAPI からNodeを取得しているわけではない → Scheduling Cache + snapshot の仕組み Scheduling Cache に全てのNodeの最新の状態をおいておく
Scheduling Cache について知る EventHandlerでNodeの作成/更新/削除 を検知してCacheを更新する (おさらい) EventHandler → リソースの変更をトリガーにあらかじめ登録した関数を実行してくれる仕組み
Scheduling Cache によるメリット ● 毎回APIで問い合わせないので効率が良い ● snapshotと組み合わせることでPodのスケジュール中に参照するNodeの状態が変化しない ● Schedulerに都合が良い形でNodeのデータを加工できる
snapshot に関して Cacheと同様にNodeの状態を保持している ● スケジュール中はNodeを取得する際にsnapshotに問い合わせる ○ (= APIから取得しない) ○ (= scheduling cacheからも直接取得しない ) ● スケジュールの開始時にCacheのデータを用いて更新されるようになっている ○ 逆に言うとスケジュールの開始時以外は更新されない
snapshot に関して snapshotと組み合わせることでPodのスケジュール中に参照するNodeの状態が変化しない → もし、取得するNodeの状態がスケジュール中に変わってしまうと、同一スケジュール中のプラグインの結果などがブレてしまうことにつながる
Scheduling Cache によるメリット ● 毎回APIで問い合わせないので効率が良い (自明) ● snapshotと組み合わせることで一つのCycleを実行中にNodeの状態が変化しない ● Schedulerに都合が良い形でNodeのデータを加工できる
Node を予約する仕組みが必要な理由 Binding Cycle を並行に実行 → 次のPodのスケジュール時に前のPodのBindが終了していない場合におかしなことに
Node を予約する仕組みが必要な理由 次のPodのスケジュール時に前のPodのBindが終了していない場合におかしなことに → Pod がNodeを予約する仕組みを作り、Binding Cycleの開始前に予約を行う → 他のPodのスケジュールの際には、 予約されているPodもすでにBindされているPodかのようにして扱う
Scheduling Cache によるメリット ● 毎回APIで問い合わせないので効率が良い (自明) ● snapshotと組み合わせることで一つのCycleを実行中にNodeの状態が変化しない ● Schedulerに都合が良い形でNodeのデータを加工できる ← 👀
Node を予約する仕組みとCache Binding Cycle に進む前に、Node を予約する → Cache を操作し、PodがNodeにBindされていることにする (もし、Binding Cycleが失敗した場合はCacheを元に戻す)
Scheduling Queue をざっくり実装する 88ブランチ: scheduling-queue
すごいシンプルなQueue (再掲) /minisched/queue/queue.go
すごいシンプルなQueue スケジューリングに失敗した後のPodのことなどを考えていない → スケジューリングに失敗したPodはまた後でスケジューリングし直すために Queueに戻したい
Scheduling Queue をざっくり実装する /minisched/queue/queue.go
Scheduling Queueが持つ3つのQueueの種類 ● activeQ: Schedule待ちのPodのQueue ● podBackoffQ backoff中のPodのQueue。 ● unschedulableQ 一度スケジュールしようとして失敗したPodのQueue Unschedulableとなった原因のPluginなども保存
Queue への追加/Queueからの取り出し
Queue への追加/Queueからの取り出し 追加 ● Add: 新たにPodが作成された時 ● AddUnschedulableIfNotPresent: スケジュールに失敗した時 ○ この際にUnschedulableとなった原因のPluginなども保存 取り出し ● スケジューリング開始時にPopを使用してactiveQからPodが取り出される
スケジュールに失敗した時にPodを戻す スケジュールに失敗した際にはsched.ErrorFunc を実行する
unschedulableQにPodを追加 unschedulableの原因となったpluginの記録
backoffQ → activeQ
backoffQ → activeQ ● backoffの待ちが終了したPodをactiveQに移動
unschedulableQ → backoffQ/activeQ
unschedulableQ → backoffQ/activeQ ● 60秒以上unschedulableQに存在するPodはbackoffQ/activeQに移動 ● 特定のイベントの発行時にbackoffQ/activeQに移動
EventHandler について再度考える 101ブランチ: event-handler
unschedulableQ → backoffQ/activeQ 特定のイベントの発行時にbackoffQ/activeQへ移動 unschedulableQはPodがどのPluginのせいでスケジュールできなかった(Unschedulableになった)のかを記録している 例: Filterで候補0となりUnschedulableとなった場合は、Filterで一つでもNodeを除外したPluginがUnschedulableの原因となったPluginとして記録される
EnqueueExtensions EnqueueExtensions を満たすことで「プラグインのスケジュールの結果が変わり得るイベント」の定義ができる
NodeUnschedulable Plugin(おさらい) Nodeの.Spec.Unschedulable を見て、trueのNodeを候補から外す
NodeUnschedulable Pluginの例 ● Nodeの.spec.Unschedulableの変更 ● 新たなNodeの追加 → このプラグインのスケジュールの結果が変わり得る
EventHandler(おさらい) EventHandler → リソースの変更をトリガーにあらかじめ登録した関数を実行してくれる仕組み これまでの登場箇所 ● PodのQueueへの追加 ● Cacheの更新
EventHandler と Queue の更新 EnqueueExtensions.EventsToRegister の情報から、 EventHandlerにイベント+Queueの更新を行う関数を動的に登録する
EventHandlerの動的な登録
EventHandlerの動的な登録 /minisched/eventhandler.go 細かい…
EventHandlerの動的な登録 新しいPodをQueueに入れるEventHandlerの登録(前述) gvkMap: EventsToRegisterの情報が集められたmap
EventHandlerの動的な登録 新しいPodをQueueに入れるEventHandlerの登録(前述) 要約: 「イベントが実行された時にMoveAllToActiveOrBackoffQueueが実行されるように登録する関数
EventHandlerの動的な登録 新しいPodをQueueに入れるEventHandlerの登録(前述) 登録され得るリソースイベントから動的に設定
Queue の更新 MoveAllToActiveOrBackoffQueue メソッド(引数: 発生したイベント) unschedulableQのPodを見て、 引数のイベントが、unschedulableの原因となったpluginの「スケジュールの結果が変わりうるイベント」だった場合に unschedulableQ から backoffQ/activeQ へと移動させる
まとめ/終わりに 114
まとめ 本日のトークで実装の対象とした機能のみで ● Filter/Score pluginsによるスケジュール ● Scheduling QueueによるBindされていないPodの管理 ● EventHandlerによるQueueの操作 が実装され、最低限のスケジュールを行うことができるようになりました
Next step.... 時間の関係上今回触れなかった機能が多くあります ● Preemption ● Permit Plugin / wait on permit ● Reserve/UnReserve Plugin ● QueueSort Plugin ● Extender全般 ● PodsToActivate によるCycleStateを通したQueue内の操作 ● その他リソースの変更によるQueue内の操作 など
ご静聴ありがとうございました 質問などはTwitterに投げてくださっても○です↓ (「いや、これ違くね?」ってところもあれば、是非教えてください ) Twitter: さんぽし(@sanpo_shiho) GitHub: @sanposhiho 117