はじめに
本記事では、Kubernetes 1.15 のリリースノート からスケジューリングに関する内容をまとめました。
なお、SIG Scheduling の変更内容については既に他の方から翻訳記事が出ていますが、本記事は後発ということもあり、すべての機能を実際に触ってみた上でサンプルコードを添えて解説していきます。
1.15 の新着情報 (1.15 What’s New)
今回、完全な変更ログは https://relnotes.k8s.io/ で、絞り込み可能なフォーマットで公開されています。確認とフィードバックお願いします!
特筆すべき機能アップデート (Additional Notable Feature Updates)
Scheduler のプラグインを作るための Scheduling Framework が新しく Alpha 版になりました。
Scheduler Framework は、Scheduler の各点に拡張できるポイントを設け、カスタム処理をプラグインとして差し込むための仕組みです。
Scheduler を拡張する仕組みとしては他に JSON Webhook を使用する Scheduler Extender もありますが、プラグインは本体と同時にコンパイルするため通信によるオーバヘッドを避けることができます。
今回の変更点を述べるためには全体像を説明する必要がありますが、ここに書くにはボリュームが多くなりそうなので別記事を立てる予定です。
(TODO: 別記事を書いたらリンク貼る)
既知の問題点 (Known Issues)
1.15 でオプション
--log-file
を指定すると問題が発生することが分かっています。一つのファイルにログが複数回出力される現象です。この問題の振る舞いや詳細、あるいは修正のための予備調査などは ここ に解説されています。
Scheduler もこの問題の影響を受けます。実際、kube-scheduler 起動時に --log-file=kube-scheduler.log
および --logtostderr=false
を指定した場合、以下のようなログファイルが出力されることを確認しました。*1
Log file created at: 2019/07/01 21:00:28 Running on machine: custom-scheduler-767dbc9c6-blhwk Binary: Built with gc go1.12.5 for linux/amd64 Log line format: [IWEF]mmdd hh:mm:ss.uuuuuu threadid file:line] msg Log file created at: 2019/07/01 21:00:29 Running on machine: custom-scheduler-767dbc9c6-blhwk Binary: Built with gc go1.12.5 for linux/amd64 Log line format: [IWEF]mmdd hh:mm:ss.uuuuuu threadid file:line] msg (snip)
通常の運用で Scheduler のログをファイルに書き出す必然性はまずないので実際には問題にはならないと思いますが、一応注意が必要です。
メトリクスの変更 (Metrics Changes)
追加されたメトリクス (Added metrics)
Scheduler について、それぞれのキュー内にある Pending 状態の Pod の個数を記録するメトリクスが追加されました。(#75501, @Huang-Wei)
追加されているメトリクスは以下の 3 種類です。
scheduler_pending_pods_num{queue="active"}
scheduler_pending_pods_num{queue="backoff"}
scheduler_pending_pods_num{queue="unschedulable"}
非推奨または変更されたメトリクス (Deprecated/changed metrics)
Scheduling Framework に Reserve、Pre-bind、Permit、Post-bind、Queue sort および Un-reserve の拡張点が実装されました。(#77567, @wgliang) (#77559, @ahg-g) (#77529, @draveness) (#77598, @danielqsj) (#77501, @JieJhih) (#77457, @danielqsj)
Scheduling Framework の進捗についてです。直接的にメトリクスに関連する話題ではないはずですが、なぜこの位置に置かれているのかよくわかりません。
ここに述べられている通り、v1.14 時点ですでに実装されていた Reserve と Pre-bind に加えて、さらに 4 つの拡張点が追加されました。これに伴い、設定項目として有効にする拡張点が選択できるようになるなど、前回まで場当たり的だった実装のリファクタリングも行われています。
ただし、Permit プラグインについては「戻り値として Wait を返すことで Bind 前の Pod を待機させる機能」は実装されているものの、その待機状態を解除する方法がまだ提供されていないようです。詳細は先にも挙げた詳細記事を参照してください。
(TODO: 別記事へのリンク)
特筆すべき機能 (Notable Features)
Preemption を行わない Pod の優先度 (NonPreemptingPriority) が作成できるようになりました。PriorityClass においてこれが指定された場合、対象の Pod は 優先度の低い Pod に対してキュー内では優先されますが、実行中の Pod を Preemption することはありません。(#74614, @denkensk)
実際に PriorityClass で指定する属性は preemptionPolicy
で、値としては PreemptLowerPriority
もしくは Never
が指定可能です。デフォルトは前者になります。Preemption する側 の Pod に指定する点に注意してください。
なお、Pod の Priority と Preemption の一般論については以下に解説があります。
この機能は Alpha 版なので、使用するには FeatureGates の有効化が必要です。kube-scheduler と apiserver の実行時引数として --feature-gates "NonPreemptingPolicy=true"
を与えておいてください。
実験してみましょう。以下、話を簡単にするためにシングルノードの Kubernetes クラスタを仮定します。
まず、以下のような 3 つの PriorityClass を用意します。low-priority
が Preemption される側の Pod 用、preempting-priority
と non-preempting-priority
が Preemption する側の Pod 用です。
priority.yaml
apiVersion: scheduling.k8s.io/v1 kind: PriorityClass metadata: name: low-priority value: 100 description: "The priroity for preempted Pods." --- apiVersion: scheduling.k8s.io/v1 kind: PriorityClass metadata: name: preempting-priority value: 100000 description: "The priroity for preempting Pods." --- apiVersion: scheduling.k8s.io/v1 kind: PriorityClass metadata: name: non-preempting-priority value: 100000 preemptionPolicy: Never description: "The priroity for non-preempting Pods."
さらに、各 PriorityClass に対応した Pod を定義します。ここで resources.requests.memory: 5Gi
が指定されていますが、これは Node の利用可能メモリが 8Gi
程度だったためです。実験に使用している Node の性能を鑑みて「1 個なら置けるが 2 個は無理」という値にしてください。
low-priority-pod.yaml
apiVersion: v1 kind: Pod metadata: name: low-priority-pod spec: containers: - name: low-priority-pod image: k8s.gcr.io/pause:2.0 resources: requests: memory: 5Gi priorityClassName: low-priority
preempting-pod.yaml
apiVersion: v1 kind: Pod metadata: name: preempting-pod spec: containers: - name: preempting-pod image: k8s.gcr.io/pause:2.0 resources: requests: memory: 5Gi priorityClassName: preempting-priority
non-preempting-pod.yaml
apiVersion: v1 kind: Pod metadata: name: non-preempting-pod spec: containers: - name: non-preempting-pod image: k8s.gcr.io/pause:2.0 resources: requests: memory: 5Gi priorityClassName: non-preempting-priority
それでは実験です。Pod に先立って PriorityClass を作成しておきます。
$ kubectl create -f priority.yaml priorityclass.scheduling.k8s.io/low-priority created priorityclass.scheduling.k8s.io/preempting-priority created priorityclass.scheduling.k8s.io/non-preempting-priority created
まず、low-priority-pod
を作成後に preempting-pod
を作成します。
$ kubectl create -f low-priority-pod.yaml pod/low-priority-pod created $ kubectl get pods NAME READY STATUS RESTARTS AGE low-priority-pod 1/1 Running 0 14s $ kubectl create -f preempting-pod.yaml pod/preempting-pod created (wait for a while...) $ kubectl get pods NAME READY STATUS RESTARTS AGE preempting-pod 1/1 Running 0 70s
Node のメモリが不足したことにより、優先度が低い low-priority-pod
が Preemption されて終了したことが分かります。
次に、preempting-pod
を削除したうえで再度 low-priority-pod
を準備し、今度は non-preempting-pod
を作成してみます。
$ kubectl delete -f preempting-pod.yaml pod "preempting-pod" deleted $ kubectl create -f low-priority-pod.yaml pod/low-priority-pod created $ kubectl create -f non-preempting-pod.yaml pod/non-preempting-pod created (wait for a while...) $ kubectl get pods NAME READY STATUS RESTARTS AGE low-priority-pod 1/1 Running 0 48s non-preempting-pod 0/1 Pending 0 25s
今度は low-priority-pod
は Running 状態のまま、優先度の高い non-preempting-pod
が Pending になっています。これが preemptingPolicy: Never
の効果です。
その他の特筆すべき変更 (Other notable changes)
重複 Toleration の扱い
Best Effort の Pod に対して同じ key と effect を持った Toleration が複数指定されている場合、マージされて最後に指定された Toleration の値が有効になります。(#75985, @ravisantoshgudimetla)
気が付かないとわかりづらいですが、これは Admission Control で呼び出されるロジックのバグ修正です。
Admission Control のひとつ PodTolerationRestriction
は、Namespace のラベルとしてホワイトリストされた以外の Toleration が付いた Pod の作成を拒否します。
その過程で Toleration を走査するのですが、Pod の QoS が Best Effort 以外の場合には、以前から「メモリが枯渇気味の Node にも配置を避けない」という Toleration を付加する処理が行われており、ここでマージが行われていました。今回、Best Effort の場合でも同じマージ関数を通すための修正です。
なお Taint と Toleration の一般論については以下に解説があります。
実験してみましょう。今回もシングルノードクラスタを仮定します。また PodTolerationRestriction
はデフォルト無効なので、apiserver に --enable-admission-plugins=PodTolerationRestriction
を指定して有効にしておいてください。
まず、Node に Taint を付加します。この Node には mykey = myvalue
の Toleration を持つ Pod 以外は新たに配置されなくなります。
$ kubectl get nodes NAME STATUS ROLES AGE VERSION kind-control-plane Ready master 64m v1.15.0 $ kubectl taint node kind-control-plane mykey=myvalue:NoSchedule node/kind-control-plane tainted
ここで、以下の Pod を考えます。
tolerant-pod.yaml
apiVersion: v1 kind: Pod metadata: name: tolerant-pod spec: containers: - name: tolerant-pod image: k8s.gcr.io/pause:2.0 tolerations: - key: mykey value: myvalue effect: NoSchedule - key: mykey value: dummy effect: NoSchedule
この Pod は mykey = myvalue
なる Toleration を持つため、一見すると配置可能なように見えます。試してみましょう。
$ kubectl create -f tolerant-pod.yaml pod/tolerant-pod created $ kubectl get pods NAME READY STATUS RESTARTS AGE tolerant-pod 0/1 Pending 0 38s
予想に反して、Pod は配置されず Pending のままになってしまいました。
では今度はふたつの Toleration の順序だけを入れ替えてみます。
tolerant-pod.yaml (edited)
apiVersion: v1 kind: Pod metadata: name: tolerant-pod spec: containers: - name: tolerant-pod image: k8s.gcr.io/pause:2.0 tolerations: - key: mykey value: dummy effect: NoSchedule - key: mykey value: myvalue effect: NoSchedule
同様に Pod を作成してみると、
$ kubectl delete -f tolerant-pod.yaml pod "tolerant-pod" deleted $ kubectl create -f tolerant-pod.yaml pod/tolerant-pod created $ kubectl get pods NAME READY STATUS RESTARTS AGE tolerant-pod 1/1 Running 0 18s
無事に Running になりました。
以上から、key
と effect
が同じで value
が異なる 2 種類の Toleration が同時に指定されているとき、後から指定されたものが有効になっていることが分かります。
PodAffinity のパフォーマンス改善
PodAffinity 指定時、Required および Preferred の両指定とも 2 倍のパフォーマンス向上を達成しました。(#76243, @Huang-Wei)
Pod の Affinity に基づいて各 Node をスコア付けする部分に手が入っています。
アルゴリズムを抜本的に変更したわけではなく、Node を走査する際に、今までループの外側で手動で取っていたロックを sync/atomic
パッケージ (GoDoc) に置き換えたようです。アトミックな加算 AddInt64 の使用に伴い、Node に対するスコアが int で保持されるようになっています。
なお Pod の Affinity については以下に解説があります。
競合状態の防止
高優先度の Pod に NominatedNodeName が登録されている際、その Node に対して低優先度の Pod をスケジュールしないようにすることで、競合状態が発生する問題を修正しました。(#77990. @Huang-Wei)
この変更ログの書き方はややミスリードで、今回、実際に入った変更は Pod のスケジューリングのロジックではありません。
NominatedNodeName は、Preemption 発生時にどの Node に対して Preemption を行ったかを記録する情報です。Preemption した側の Pod に付与されますが、必ずしも結果的にその Node に最終的に配置されることは保証していません。例えば、Preemption された低優先度 Pod の終了を待っているうちに他の Node に空きが出たり、逆に Preemption した Pod よりさらに高優先度の Pod が到着したりする状況も考えられます。
スケジューラは内部に Pod と NominatedNodeName の対応を保持しており、キュー内の Pod を操作すると同時にその対応を更新します。そして Premption 直後の特定のタイミングでは、Preemption した側の Pod の NominatedNodeName が空欄になった状態で更新関数が呼ばれるケースがあるようです。
今回の修正は、このような場合に NominatedNodeName が空欄で上書きされて消えてしまい、空いた Node に低優先度の Pod が横から入ることを防ぐためのものです。
まとめ
以上、Kubernetes v1.15.0 におけるリリースノートとその解説でした。押さえておくべき重要事項は次の 2 点です。
- Scheduling Framework に新しい拡張点が設定された
- Preemption は発生させない (ただしキュー内の順番としては優先される) PriorityClass の設定が可能になった
ところで、PriorityClass はもちろん Preemption 関連ですが、実は Scheduling Framework も Preemption と無関係ではありません。新しく実装された Queue sort プラグインを利用することで今まで難しかった Preemption の制御が可能になるのですが、それはまた別の話。
*1:なお 1.15.0 において --log-dir と --alsologtostderr が認識されていないように見えましたが、今回は深追いしていません。