Kubernetes 1.15: SIG Scheduling の変更内容
はじめに
本記事では、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 が認識されていないように見えましたが、今回は深追いしていません。
Fun Fun Functional (1) で Haskell と Firebase を使ってライブコーディングしてきました
先日行われた Fun Fun Functional (1) で、Haskell と Firebase を使った Web アプリの作り方について発表してきました。
使用した要素技術は、GHCJS 上のフレームワーク Miso と、Fireabse SDK を呼び出すための DSL である JSaddle です。
GHCJS は Haskell のソースコードを JavaScript に変換するコンパイラで、GHC をフォークすることによって開発されています。
Miso は GHCJS 上で The Elm Architecture を実装するためのフレームワークです。Miso では事実上サーバサイドで書けない Elm と異なり、サーバサイドとの Model の共有や、初回アクセス時の HTML をサーバ側で構築するサーバサイドレンダリング (SSR) の仕組みが提供されています。
JSaddle は Haskell から JavaScript の関数を呼ぶための DSL です。Lens インタフェースに対応した JSM
モナドが提供されており、GHCJS でコンパイルした場合は単に IO
モナドに変換されます。
main :: IO () main = do -- console.log('Hello, JSaddle!'); jsg "console" ^. js1 "log" (val "Hello, JSaddle!")
これら要素技術の簡単な解説に加えて、LT の後半ではライブコーディングのパフォーマンスを披露しました。
作成したのは、簡単なコメントが残せるゲストブックを模した、以下のようなアプリです。バックエンドとして Firebase Realtime DB を利用しているため、複数のブラウザから開いた場合でも書き込みはリアルタイムで同期されます。
前掲のスライドは公開用にソースコードを補足してありますが、当日は文字通り、その場で段階的にコードを追記・変更することで機能が画面に反映されていく様子を紹介しました。
- まず動きのない HTML だけの View をレンダリングする
- ユーザからの入力に反応して Action を発行し、Update で Model に反映する
- Effect を利用してコメントを Realtime DB に保存する
- Subscription を利用して Realtime DB の変更を検知する
LT としてはあまり見ない形式ですが、Twitter 上で見る限り割と好評だったようです。こんな風に、実装の結果が目で見て分かりやすいのはフロントエンドの強みだなと改めて感じました。
(ちなみに @u_taka_23 ではなく @y_taka_23 です)最後は @u_taka_23 さんです!
— オプトテクノロジーズ (@OptTechnologies) May 27, 2019
なんとライブコーディングが始まりました...#FunFunFunctional pic.twitter.com/Yej8v4pG7a
チェシャ猫さんのライブコーディング素晴らしかった #miso #ghcjs #FunFunFunctional
— Yoshihiro503 (@yoshihiro503) May 27, 2019
ところで今回のライブコーディング、ハンズオン形式のチュートリアルとして仕立て直して公開しようかなとも思っているのですが、それはまた別の話。
Docker Meetup Tokyo #28 で Scheduler のカスタマイズについて話してきました
先日行われた Docker Meetup Tokyo #28で、Kubernetes Scheduler の挙動をカスタマイズする方法について発表してきました。
なお Scheduler のカスタマイズについては、つい最近 Kubernetes Meetup Tokyo #16 でも発表しています。ドキュメント類へのリンクも含めてまとめたものが以下の記事です。
両方のスライドを見比べて頂ければ分かる通り、内容としてはオーバラップしている部分がかなりあります。
ただし、前回はあくまでも Scheduling Framework の解説であったのに対し、今回は Scheduler のカスタマイズ全体を俯瞰する形で差別化を図っています。時間的にも前回 5 分に対して今回 9 分とやや余裕があったため、Amazon ECS の binpack 配置戦略やポリシの記述方法など、前回端折った具体的な設定についても少し触れてみました。
ちなみに、スライド中で挙げた 3 種類のカスタマイズ方法それぞれについて、ハンズオン形式で実装しながら学べるチュートリアルを作成するプランもあるのですが、それはまた別の話。
Kubernetes Meetup Tokyo #16 で Scheduling Framework について話してきました
先日行われた Kubernetes Meetup Tokyo #16 で、現在 Scheduling SIG で進められているプロジェクト Scheduling Framework について発表してきました。
Kubernetes では、Pod をどの Node に配置するかを決める手続きをスケジューリングと呼びます。
古典的な Kubernetes の用途、すなわち通常の long-running なサーバ群の管理においては、Pod のスケジューリングは比較的シンプルな問題でした。すなわち、Node の障害時でも可用性が保てるように Pod を複数の Node に散らし、一度立ち上がった Pod は基本的に動き続ける、というシナリオです。
しかし、Kubernetes は既にコンテナスケジューラのデファクトを獲得し、様々な性質を持ったアプリケーションがデプロイされる基盤となりました。この流れの中で、デフォルトの kube-scheduler ではカバーできないような複雑なスケジューリングの需要も生まれています。
この問題を解決するためのプロジェクトが Scheduling Framework です。本記事では、スライドに登場した用語や概念について、Scheduling SIG その他から提供されているドキュメントやサンプルを紹介します。
Scheduling SIG によるサブプロジェクト
kube-batch
kube-batch はバッチ処理向けのスケジューラで、かつて kube-arbitrator と呼ばれていたプロジェクトが改名しました。たった 1 行だけの Wiki が味わい深いです。
PodGroup
を指定することで複数の Pod を組にして扱う機能を備えているのが特徴で、この仕組みは Kubernetes を機械学習用基盤として使用する kube-flow プロジェクトにも導入されるようです。
Poseidon
Poseidon は、グラフ理論を利用して複雑かつ効率的なスケジューリングを目指す新機軸のプロジェクトです。
元になっているのは Firmament Scheduler と呼ばれる汎用のタスクスケジューリングの仕組みで、配置の制約を最小費用流問題に帰着させて最適化を行うようです。Kubernetes の Pod と Node に対して Firmament を実装したものが Poseidon という関係になっています。
正直なところ、この記事を書いている時点ではまだ論文を充分読み込んでいないので何とも言えませんが、内容がきちんと把握できたら改めてどこかのイベントで発表したいと思います。
Scheduler Extender
スケジューリング処理の一部分を JSON Webhook で外部サーバに委譲するための仕組みが Extender です。
Extender が設定できる処理は以下の 4 か所で、Scheduler 起動時の設定ファイルに外部サーバのエンドポイントなどを記述することで有効になります。
- Filter - 後段の順序付けフェイズに候補として残す Node を絞ることができます。リクエストはデフォルトのフィルタリングプロセスを通過した Pod、レスポンスはフィルタリング後の Pod です。
- Prioritize - 最良の Node を選択するためのスコアリング関数を追加できます。引数はフィルタリングで残った Pod、レスポンスは Node ごとのスコアです。
- Preempt - Pod の Preemption が発生する際に、犠牲となる Pod を消極的に選択することができます。リクエストは Node ごとに犠牲となる予定の Pod、レスポンスは実際に犠牲にする Pod です。
- Bind - Pod を Node に配置する前の準備などを行うことができます。ただしその場合、Pod と Node を結びつけている
Binding
リソースの作成まで含めて自分で行う必要があります。Pod の情報と Node 名がリクエストされます。
なお、上記のドキュメントは古いためか Preempt Extender の記載がありませんが、ソースコードを見ると実装されていて実際に動きます。
もし Extender を使用したい場合は、ドキュメントよりもむしろ、構造体の定義および @everpeace さんによる実装サンプルを参考にするとよいでしょう。
Scheduling Framework
Scheduling Framework のプロポーザルです。今回のスライドは基本的にこのプロポーザルに沿って説明しています。
上でも挙げたような特定用途のために作られたスケジューラは、基本的にスクラッチから開発されています。というのも kube-scheduler の実装は歴史的事情から抽象化が十分でなく、拡張性に限界があるためです。
そこで、Scheduling Framework では設計を刷新して新しい拡張点を定式化します。それぞれの拡張点では、Go のインタフェースとして実装されたプラグインを登録し処理を差し込めるようにするとともに、プラグイン同士が情報をやり取りできる仕組みが構築される予定です。
そう、あくまでも予定です。 プロポーザルには多数の拡張点が述べられていますが、現状で以下の二点のみ実装されています。
- Reserve - Node が確定した後、Pod ごとの goroutine が生成される前
- Pre-Bind - Pod ごとの goroutine に入った後、API Server に Binding が登録される前
この二点は、Pod を配置する Node に前もって何かを準備する処理を想定したもので、典型的な用途はネットワークストレージをボリュームとして確保するというものです。
従来、ボリュームの確保とバインドは assumeVolume
と bindVolume
という専用の関数で行われています。一方 GPU などボリューム以外のネットワークリソースについてはユーザが Bind Extender という形で各自実装する必要があります。
そこで、assumeVolume
に相当するものとして Reserve プラグインを、bindVolume
に相当するものとして Pre-Bind プラグインを実装し、リソース関連の処理を統合して抽象化することが Scheduling Framework の第一弾として意図されていました。が、今のところ拡張点が準備されただけで止まっており、assumeVolume
も bindVolume
もそのまま残っています。まあ、今後の展開を見守りましょう……。
まとめ
本記事では、Kubernetes スケジューラの再設計を目的としたプロジェクト Scheduling Framework と、その周辺トピックについて紹介しました。Scheduling SIG のマンパワー不足もあって、当初の予定ほど華々しい進展が見られないのは残念ですが、それはまた別の話。
Kubernetes 1.13: SIG Scheduling の変更内容
はじめに
本記事では、Kubernetes 1.13 の CHANGELOG からスケジューリングに関する内容をまとめました。
主な変更点
1.13 における SIG Scheduling の取り組みは主に安定性に焦点を当てており、いくつかの大きな機能の導入は次のバージョンまで延期することになりました。特記すべき変更として次に挙げる 2 点があります。
#69824: Taint based Eviction の有効化
TaintBasedEvictions
がベータに移行し、デフォルトで有効になりました。この機能が有効になっている場合、Node には自動的に条件 Taint が付加され、Pod は必要であれば Toleration を使用することができます。
Taint based eviction は、Node に問題が発生した際、その内容に応じて Node Controller が以下のような Taint を自動的に付加する仕組みです。
node.kubernetes.io/not-ready
node.kubernetes.io/unreachable
node.kubernetes.io/out-of-disk
node.kubernetes.io/memory-pressure
node.kubernetes.io/disk-pressure
node.kubernetes.io/network-unavailable
node.kubernetes.io/unschedulable
node.cloudprovider.kubernetes.io/uninitialized
今まで Pod のスケジューリングには「Not Ready な Node を避ける」といったロジックが入っていました。1.13 からこの TaintBasedEvictions
がデフォルトで有効になったことにより、障害時の Pod 退避は Taint による管理に統一されます。
Taint と Tolaration によるスケジューリングに統一されることで、Node 障害時の挙動をユーザがより柔軟にコントロールできるようになります。例えば Pod に tolerationSeconds
を指定することで「Node に問題 X が発生した際は n 秒以内に回復しなければ移動」といった挙動の調整が可能です。
tolerations: - key: "node.kubernetes.io/unreachable" operator: "Exists" effect: "NoExecute" tolerationSeconds: 6000
ちなみに、tolerationSeconds
が設定されていない場合、Admission Control により not-ready
と unreachable
に 300 秒の tolerationSeconds
が設定されます。要するに何も設定していない場合は Node の障害から最大 300 秒待って Pod が削除される、ということです。
#70298: critical-pod
アノテーションが非推奨に
Pod に対するクリティカルアノテーションが非推奨になりました。アノテーションの代わりに Pod の優先度を使用すべきです。
DNS や Metrics Server といった死なれるとクラスタ全体の動作に影響するような Pod のために、従来 scheduler.alpha.kubernetes.io/critical-pod
というアノテーションが用意されていましたが、今回から非推奨になりました。
代わりに、デフォルトで定義されている優先度クラス system-cluster-critical
と system-node-critical
を使用します。両者の定義は以下のようになっており、Node の移動が許容できるかどうかで用途が分かれています。
Name: system-cluster-critical Value: 2000000000 GlobalDefault: false Description: Used for system critical pods that must run in the cluster, but can be moved to another node if necessary. Annotations: <none> Events: <none> --- Name: system-node-critical Value: 2000001000 GlobalDefault: false Description: Used for system critical pods that must not be moved from their current node. Annotations: <none> Events: <none>
ただしこれらの優先度クラスは、1.11 以降 kube-system
Namespace 内でしか使えないことに注意が必要です。
- https://kubernetes.io/docs/tasks/administer-cluster/guaranteed-scheduling-critical-addon-pods/
- https://kubernetes.io/docs/concepts/configuration/pod-priority-preemption/
また、この件とは直接関係しませんが、優先度による Preemption の動作原理については半年ほど前に書いた記事があるのでよければこちらもご笑覧ください。
#70040: 要対応
1.13 では kube-scheduler の設定ファイルの
apiVersion
がcomponentconfig/v1alpha1
ではなくkubescheduler.config.k8s.io/v1alpha1
になります。
API グループ componentconfig
は解体されつつあり、Scheduler 以外にも例えば kubeproxy.config.k8s.io
が 1.9 から導入されています。
しかし、そもそもこの設定ファイルの書式はドキュメントに記載されていないし、サンプルファイルのようなものも提供されていません。対応する構造体が以下に定義されているので、ファイルの記載項目を確認することは一応可能です。
SIG Scheduling リリースノート
メトリクス追加を除き、内部ロジックの修正のみです。
#59529
ボリュームをスケジューリングする操作にメトリクスが追加されました。
以下のメトリクスが登録されるようになっています。
binder_cache_requests_total
scheduling_duration_seconds
scheduling_stage_error_total
#65350
Toleration を含む大量の Pod を処理する際のメモリ使用量とパフォーマンスが向上しました。
#69758
ゾーン内の Node がすべて削除された際、スケジューラが無限ループに陥るバグを修正しました。
#71212
Pod のバインディングでエラーが発生した際、古いキャッシュが使用されないよう削除するようにしました。
#71063
スケジューラ内部のキャッシュが不整合になった際、panic になる挙動を修正しました。
#71085
kube-scheduler のリーダ選出がデッドロックに陥った際、unhealthy を報告するようになりました。
#70898
必要のない Pod まで preemption してしまう潜在的なバグを修正しました。
まとめ
ユーザ側にとってさほど大きな変更点はありませんが、これまで Pod の優先度を導入しないまま運用していた場合、critical-pod
アノテーションの件で影響を受ける可能性があります。まだ非推奨になっただけで廃止ではありませんが、早めに変更しておきましょう。
なお、このところ機能追加という意味では SIG Scheduling はやや低調で、実際マンパワーが足りていない印象がありますが、水面下では興味深い動きもいくつか見られます。
- スケジューラに拡張点を設けてカスタマイズ可能にする Scheduling Framework
- 複数の Pod を同時に All or Nothing でスケジューリングする coscheduling(旧称 Gang Scheduling)
あたりが目下のところ予定されている大きな機能追加ですが、それはまた別の話。
We Are JavaScripters! @26th で Elm と Firebase の連携について話してきました
先日行われた We Are JavaScripters! @19th で Elm と JavaScript ライブラリの連携について発表してきました。
Elm の初心者向けの解説としてよく Msg, Model, update からなるアーキテクチャが挙げられていますが、今回の発表ではもう一歩だけ進んで、Cmd と Sub を使って Elm から JavaScript のライブラリを呼ぶ方法について解説しました。
サーバとしての JS ライブラリ
他の AltJS では JavaScript を呼び出す際、ソースコードの内部に埋め込む形になるのが普通です。
例えば Haskell を JavaScript にコンパイルする GHCJS の場合、JSaddle という DSL を利用して次のように呼び出すことになります。
store :: Int -> IO () store n = runJSaddle () $ do ref <- jsg "firebase" ^. js0 "database" ^. js1 "ref" (val "/counter") ref ^. js1 "set" (val n) return ()
Firebase SDK を呼び出して Realtime DB に値をセットする部分です。JavaScript 側の関数を文字列で指定することで、直接 Haskell のソースコード内に IO
アクションとして JS の呼び出しが定義されていることがわかります。この場合、Haskell とは別に JavaScript 側を自分で実装する必要はありません。
一方、Elm で同様の呼び出しを実装する場合、「Port」「Command」「JavaScript 側での subscribe」という 3 つの部分に分割されます。
まず、JavaScript 側へのインタフェースとなる Port を次のように定義します。
port module RealtimeCounter.Port exposing (store) import Json.Encode as E port store : E.Value -> Cmd msg
JSON を受け取って、Cmd を発生させる関数 store
が定義されています。port
に指定された関数は型シグネチャだけが存在し、中身は記述しません。
次に、実際に update
関数の中でこの Cmd を発生させるために、Elm の Int
値を JSON エンコードしてこの port に流し込む関数を定義します。
import Json.Encode as E storeCount : Int -> Cmd msg storeCount = store << E.int
最後に、Elm 側から発生した Cmd を受け取るための JavaScript を実装します。
const { Elm } = require('./Main.elm'); const app = Elm.Main.init({ node: document.getElementById('app') }); app.ports.store.subscribe((count) => { firebase.database().ref('count').set(count); });
上に挙げたコールバックによる実装を見ても分かる通り、JavaScript 側はあたかも Web サーバのコントローラのように subscribe
で待ち受けており、Elm から Cmd によって JSON が渡されると実際に Realtime DB に値をセットします。
逆に、JavaScript 側から Elm 側に値を戻す必要がある場合には、JavaScript 側で send
関数を使用すると Elm 側からは Sub となって観測されます。具体的には下記のサンプルレポジトリを参照してください。
なお、実際に JS 連携する部分をコーディングする際には、boiyaa さんが書かれている詳しい記事も参考になると思います。
boiyaa さんの記事は Elm v1.18 時点で書かれているので起動時に Html.programWithFlags
を使用していますが、Elm v1.19 ではここに相当する関数は Browser.element
に変更になっています。
動作サンプル
実際に触って動かせるデモもデプロイしておきました。先に挙げたソースコードと合わせて、よかったら参考にしてみてください。
プレゼンの際にお見せしたデモは単にテキストを置いただけの画面でしたが、公開版はちょっと凝った CSS を付けてみました。カウンタ自体の Elm 側のロジックは非常にシンプルで行数も少ないので、GitHub からは Elm ではなく CSS のレポジトリ扱いされてしまっていますが、それはまた別の話。
猫でもわかる Vertical Pod Autoscaler
先日行われた Kubernetes Meetup Tokyo #13 で、Vertical Pod Autoscaler (VPA) について発表してきました。
VPA は、各コンテナの Resource Request の値を自動的に調整してくれるコンポーネント群です。必要とするリソース(CPU、メモリ)量があらかじめ推測しにくいアプリケーションに対して、実績に基づいてそれらしい値を決めたい場合に効果を発揮します。
本記事ではスライドの補足として、VPA が動作する流れをクラスタ上での実際の挙動を通じて確認し、また内部実装についても踏み込んで解説します。
なお、本記事中で引用している仕様やソースコードは執筆時点で最新のコミット ab9c27e
を基準にしています。
はじめに
Kubernetes の Pod には、コンテナごとにリソース使用量を指定する機能があります。指定できる項目は Request と Limit の二種類がありますが、今回のテーマである VPA に関係するのは Request の方です。
Kubernetes Scheduler は、Node の利用可能リソースと Pod の Resource Request を比較してどの Node に Pod を配置するか決定します。
Managing Compute Resources for Containers - Kubernetes
重要なのは、Request の値はあくまでもユーザが指定した値であり、実際のリソース使用量とは無関係であるという点です。そのため、Request が小さすぎると実際の動作時にリソースが不足してアプリケーションの挙動に悪影響を与える可能性がある一方、大きすぎるとリソースが余っているのにもかかわらず Pod が配置されないスペースが増えるため、集積度が下がり無駄なコストを抱えることになります。
この Request と実績値の乖離という問題を解決するのが VPA です。VPA は、リソース使用量の実績値をモニタして、過去の履歴から算出した推奨値を自動で Request として設定してくれます。結果として、Scheduler はその Pod をより実態に合った Node に配置することができ、クラスタ全体のリソースを有効活用することができます。
コンポーネント群とその役割
ここからは、実際にクラスタ上に VPA をデプロイしtて、Resource Request が変更される際の各コンポーネントの挙動を確認してみましょう。手元のクラスタが必要な条件を満たしていることを確認しておいてください。
なお、VPA は amd64
用のバイナリしか提供していません。Raspberry Pi で試したい場合、ARM 用の差分をフォークしたので参考にしてください。対応した Docker イメージも Docker Hub に push してあります。
起動の準備
VPA は、次に挙げた三つのコンポーネントがそれぞれの役割を分担することで成り立っています。
- Recommender : 実際のリソース使用量データから Request の推奨値を算出する
- Updater : 推奨値に合致しない Pod を見つけて evict する
- Admission Controller : evict された Pod が再作成される際に Request の値を差し替える
これらのコンポーネントを作成するための YAML は deploy
ディレクトリ以下にありますが、証明書の作成などの作業とひとまとめにしたスクリプト vpa-up.sh
が提供されているのでこれを使用します。
$ mkdir -p $GOPATH/src/k8s.io $ cd $GOPATH/src/k8s.io $ git clone https://github.com/kubernetes/autoscaler.git $ cd autoscaler/vertical-pod-autoscaler $ git checkout -b ab9c27e
ただし今回は、起動スクリプト実行前にちょっとした工夫を加えます。コンポーネント群を生成する *-deployment.yaml
の replicas
を 0 に書き換えておきましょう。
$ find deploy -name *-deployment.yaml | xargs sed -i.bak "s/replicas: 1/replicas: 0/" $ ./hack/vpa-up.sh
こうすることで、必要な準備は整えた上で、コンポーネント群はまだ起動していない状態をつくることができます。以下、各コンポーネントをひとつづつ起動し、それぞれのコンポーネントの役割を確認します。
VPA CustomResource
VPA の設定および現在の状態は CustomResource として保持されます。
VPA の作成
先ほど実行した vpa-up.sh
によって VPA 用の CustomResourceDefinition が作成されているはずなので確認してみましょう。
$ kubectl get crd | grep verticalpodautoscaler verticalpodautoscalercheckpoints.poc.autoscaling.k8s.io 2018-10-01T08:17:20Z verticalpodautoscalers.poc.autoscaling.k8s.io 2018-10-01T08:17:20Z
二種類の CustomResource が作成されていますが、VPA の状態を保持するのは verticalpodautoscalers
です。
提供されているサンプル hamster.yaml
から Deployment とそれを管理する VPA を作成します。
$ kubectl create -f examples/hamster.yaml verticalpodautoscaler.poc.autoscaling.k8s.io/hamster-vpa created deployment.extensions/hamster created
Pod が 2 個作成されますが、この段階では Pod の Resource Request は hamster.yaml
で定義されている通り CPU 100m、メモリ 50 Mi です。
$ kubectl get pods | grep hamster hamster-6db596f5b4-46g4g 1/1 Running 0 6s hamster-6db596f5b4-9hlw2 1/1 Running 0 7s $ kubectl describe pods/hamster-6db596f5b4-46g4g ... Requests: cpu: 100m memory: 50Mi ...
同時に作成された VPA についても確認しておきましょう。出力に Status
の項目がないことに注意してください。Status
を埋める役割を担っているのは Recommender ですが、この段階ではまだ起動していないためです。
$ kubectl describe vpa/hamster-vpa ...
以上の状態はいわば「初期状態」です。必要なコンポーネント群がまだ起動していないので、このまま待っても状態は変化しません。ここから一つずつコンポーネントを立ち上げていって、VPA の変化を観察します。
VPA の設定項目
今回のサンプルでは最低限の項目のみ定義されていますが、可能な設定をすべて明示すると以下のようになります。
apiVersion: "poc.autoscaling.k8s.io/v1alpha1" kind: VerticalPodAutoscaler metadata: name: my-app-vpa spec: selector: matchLabels: app: my-app updatePolicy: updateMode: Auto resourcePolicy: containerPolicies: - containerName: my-app mode: Auto minAllowed: cpu: 200m memory: 100Mi maxAllowed: cpu: 1000m memory: 500Mi
各項目はおおむね見た通りですが、updatePolicy
は少し説明が必要かもしれません。可能な値は Auto
、Recreate
、Initial
、Off
で、それぞれ次のような挙動になります。
Auto
はデフォルト値です。現時点では Kubernetes が Pod を起動させたまま Resource Request の値を変更する機能を提供していないため、暫定的に Recreate
と同じ挙動になります。
Recreate
の場合、Resouce Request が推奨値のレンジに収まらない Pod が存在したとき、その Pod は evict されます。
Initial
の場合、Pod が最初に作成されるときのみ Resource Request の変更を行いますが、すでに起動状態の Pod を能動的に evict しようとはしません。
Off
の場合、Resource Request の推奨値の算出だけを行うのみで Pod には影響を与えません。
なお resourcePolicies[].mode
には Auto
もしくは Off
のみが指定できます。また resourcePolicies[].containerName
を *
とすると、その Pod に属するコンテナの内 containerName
が指定されていないものすべてに適用されるデフォルト値になります。
Recommender
Recommender は Metrics Server 経由で Pod のリソース使用実績を取得し、Resource Request の推奨値を算出して VPA に保存します。
Recommender の起動
まず、先ほど 0 に差し替えていた replicas
を 1 に戻して Recommender を起動させます。
$ mv deploy/recommender-deployment.yaml.bak deploy/recommender-deployment.yaml $ kubectl apply -f deploy/recommender-deployment.yaml serviceaccount/vpa-recommender configured deployment.extensions/vpa-recommender configured $ kubectl -n kube-system get pods | grep recommender vpa-recommender-6d8cddc856-rw7m2 1/1 Running 0 6s
これで Recommender が立ち上がりました。数分待つと、算出された推奨値が VPA の Status
として書き込まれているのが確認できます。
$ kubectl describe vpa/hamster-vpa ... Status: Conditions: Last Transition Time: 2018-10-01T08:52:56Z Status: True Type: RecommendationProvided Recommendation: Container Recommendations: Container Name: hamster Lower Bound: Cpu: 115m Memory: 53677237 Target: Cpu: 688m Memory: 319572800 Upper Bound: Cpu: 991408m Memory: 460504404800 ...
Recommender は Target、Lower Bound、Upper Bound の三種類の値を提示します。もし現に起動している Pod の Resource Request がこの Lower Bound を割っている場合、その Pod が実際に動作するためには想定よりも多くのリソースが必要です。また Upper Bound を超えている場合、Node の選択時に必要以上に余裕を見ていることでクラスタ全体のリソースに無駄が生じていることになります。
ただし、Recommender は推奨値を算出して VPA の Status
に記録するだけで、実際の調整作業は行いません。例えば先ほど作成した Pod の Request は CPU 100m、メモリ 50 Mi だったので推奨値の範囲からは外れていますが、しばらく放置しても再作成されたりはしないはずです。
推奨値を算出するアルゴリズム
アルゴリズムについて解説する前に、もう一度 VPA の状態を確認してみましょう。
$ kubectl describe vpa/hamster-vpa ... Status: Conditions: Last Transition Time: 2018-10-01T08:52:56Z Status: True Type: RecommendationProvided Recommendation: Container Recommendations: Container Name: hamster Lower Bound: Cpu: 529m Memory: 237494649 Target: Cpu: 712m Memory: 319572800 Upper Bound: Cpu: 114632m Memory: 51451220800 ...
先ほどと値が変わっています。もちろん、Recommender はリアルタイムの実績値を使用しているのでタイミングによって多少のブレはありますが、Lower Bound と Upper Bound が Target に近づいている、言い換えれば許容される値の幅が狭くなっているのが特徴的です。
推奨値の算出ロジックは、estimator.go
に定義された ResourceEstimator
インタフェースとして抽象化されています。
type ResourceEstimator interface { GetResourceEstimation(s *model.AggregateContainerState) model.Resources
内部で使用されている ResourceEstimator
の実装はいわゆる Decorator パターンになっていて、ベースとなる Estimator を次々にラップすることで算出ロジックを組み立てていきます。
例として、実装の一つである minResourceEstimator
を見てみましょう。
type minResourcesEstimator struct { minResources model.Resources baseEstimator ResourceEstimator } func (e *minResourcesEstimator) GetResourceEstimation(s *model.AggregateContainerState) model.Resources { originalResources := e.baseEstimator.GetResourceEstimation(s) newResources := make(model.Resources) for resource, resourceAmount := range originalResources { if resourceAmount < e.minResources[resource] { resourceAmount = e.minResources[resource] } newResources[resource] = resourceAmount } return newResources }
構造体の内側にベースとなる baseEstimator
を保持していて、ベースが返す値がもし許容される最小値 minResources
より大きい場合はそのまま、小さい場合には切り上げた値を返すようになっています。
実際に Lower Bound、Target、Upper Bound を算出している Estimator は、recommender.go
内の CreatePodResourceRecommender
関数で構築されています。過去データのパーセンタイル値をベースとして、最小値と安全係数を加味した Estimator です。
推奨値 | リソース | 基準パーセンタイル | 最小値 | 安全係数 |
---|---|---|---|---|
Lower Bound | CPU | 50% | 25m | *1.15 |
memory | 50% | 250Mi | *1.15 | |
Target | CPU | 90% | 25m | *1.15 |
memory | 90% | 250Mi | *1.15 | |
Upper Bound | CPU | 95% | 25m | *1.15 |
memory | 95% | 250Mi | *1.15 |
Lower Bound と Upper Bound については、ここからさらに「信頼区間」を加味するためのスケーリング操作が入ります。
Lower Bound の計算式は
Upper Bound の計算式は
で与えられます。N は算出に使用する過去データのサンプルサイズを表すパラメータで、デフォルトのサンプリング間隔(1 回/分)の場合はデータを使用する期間の日数に一致します。
計算式から、使用する過去データの期間が長ければ長いほど scaledResource は originalResource に近くなり、また Upper Bound よりも Lower Bound の方が急激に収束することがわかります。このことは、先ほど VPA をしばらく放置した際、値の幅が狭くなっていた観察結果とも一致します。
なお、このサンプリング間隔は Recommender 起動の際に --recommender-interval
オプションで指定できますが、なぜか無駄に細かくナノ秒単位で指定する仕様になっています。
Updater
Updater は、Recommender が VPA に書き込んだ推奨値と実際に起動中の Pod に指定されている Resource Request を比較し、推奨値のレンジから外れている場合はその Pod を evict します。
Updater の起動
Recommender に引き続き、Updater も起動させてみましょう。
$ mv deploy/updater-deployment.yaml.bak deploy/updater-deployment.yaml $ kubectl apply -f deploy/updater-deployment.yaml serviceaccount/vpa-updater configured deployment.extensions/vpa-updater configured $ kubectl -n kube-system get pods | grep updater vpa-updater-664996d6dd-sjrz4 1/1 Running 0 6s
これで Recommender + Updater が立ち上がった状態になりました。数分待つと Pod が evict され、Deployment の作用で再作成されます。しかし、この新しい Pod の Resource Request は元の Pod と同様、CPU 100m、メモリ 50Mi のままです。
$ kubectl get pods | grep hamster hamster-6db596f5b4-9hlw2 1/1 Running 0 12m hamster-6db596f5b4-wls85 1/1 Running 0 4s $ kubectl describe pods/hamster-6db596f5b4-wls85 ... Requests: cpu: 100m memory: 50Mi ...
Updater の担当はあくまでも Pod を evict することまでであり、Resource Request の値を変更することはありません。したがって当然ながら Pod は Deployment で定義されている通りの設定で再作成されます。すると新しい Pod もやはり推奨値のレンジからは外れているので、結果として evict と再作成が繰り返されるはずです。
ちなみに、Updater が delete ではなく evict API を使用するという事実は地味ですが有用です。というのも、あらかじめ Pod Disruption Budget を指定しておくことで、すべての Pod が同時に再作成されて一時的にサービス不能になる事態を避けることができるからです。
evict される Pod の割合
ところで、今回は特に Pod Disruption Budget が設定されていないにもかかわらず、evict される Pod は必ず一個ずつであることに気づいたかもしれません。これは、Updater 自身が evict する Pod を制限する仕組みを持っているためです。
Pod を evict できるかどうかの判定は、pods_eviction_restriction.go
内で定義されたインタフェース PodsEvictionRestriction
によって行われます。インタフェースは切られていますが実装は事実上podsEvictionRestrictionImpl
のみで、判定を行う関数 canEvict
は以下のような内容になっています。
func (e *podsEvictionRestrictionImpl) CanEvict(pod *apiv1.Pod) bool { ... shouldBeAlive := singleGroupStats.configured - singleGroupStats.evictionTolerance if singleGroupStats.running-singleGroupStats.evicted > shouldBeAlive { return true } ... }
ここで、configured
は設定上存在する Pod の個数、例えば今回の場合なら Deployment に指定された replicas
の値が格納されています。条件判定の内容から、evictionTolerance
は evict したくない Pod の個数であるらしいことが見て取れます。
evictionTolerance
の値を実際に計算しているのは、同じファイル内の NewPodsEvictionRestriction
関数です。
func (f *podsEvictionRestrictionFactoryImpl) NewPodsEvictionRestriction(pods []*apiv1.Pod) PodsEvictionRestriction { ... singleGroup.evictionTolerance = int(float64(configured) * f.evictionToleranceFraction) ... }
この evictionToleranceFraction
のデフォルト値は 0.5 なので、 Updater は一度に replicas
の半分まで evict しようとします。今回、2 個ある Pod が 1 個ずつ evict されていたのはこのロジックが原因です。
なお evictionToleranceFraction
の値は Updaer 起動の際に --eviction-tolerance
オプションで指定できますが、VPA ごとに Pod の用途に合わせて設定することはできないため注意しましょう。
Admission Controller
Admission Controller は、Updater により evict された Pod が再作成される際、API Server への Pod 作成リクエストに割り込んで Resource Request の値を書き換えます。
Admission Controller の起動
最後のコンポーネントとして、Admission Controller を起動させます。
$ mv deploy/admission-controller-deployment.yaml.bak deploy/admission-controller-deployment.yaml $ kubectl apply -f deploy/admission-controller-deployment.yaml deployment.extensions/vpa-admission-controller configured service/vpa-webhook configured $ kubectl -n kube-system get pods | grep admission-controller vpa-admission-controller-76744566cc-t6m9d 1/1 Running 0 9s
これで Recommender + Updater + Admission Controller の全コンポーネントが揃った状態になりました。先ほど Updater のみ起動した状態では元と同じ設定で Pod が再作成されるだけでしたが、Admission Controller の起動によって状況が変わったことを確認しましょう。
$ kubectl get pods | grep hamster hamster-6db596f5b4-7xm84 1/1 Running 0 1m hamster-6db596f5b4-9dn55 1/1 Running 0 22s $ kubectl describe pods/hamster-6db596f5b4-9dn55 ... Requests: cpu: 712m memory: 319572800...
ここで hamster-6db596f5b4-9dn55
は Admission Controller 起動後に作成された Pod です。Resource Request の値が VPA に保持された Target と同じ値に差し替えられていることがわかります。
以上で VPA が推奨値を算出して Pod に適用する一連の流れが確認できました。
Request を差し替える仕組み
ところで、なぜ Pod の Resource Request が差し替えられたのでしょうか? Pod を再作成している Deployment と、そこから作られる ReplicaSet の状況を確認してみましょう。
$ kubectl describe deploy/hamster ... Requests: cpu: 100m memory: 50Mi ... $ kubectl get rs | hamster hamster-78568d5fbc 2 2 2 29m $ kubectl describe rs/hamster-78568d5fbc ... Requests: cpu: 100m memory: 50Mi ...
Deployment と ReplicaSet はいずれも元の設定である CPU 100m、メモリ 50 Mi から変化していません。すなわち他には影響を与えず、Pod を作成するリクエストだけが差し替えられていることがわかります。
ここまで VPA の動作に必要な特定のコンポーネントを Admission Controller と呼んできましたが、実はこれは固有名詞ではありません。もっと広く Kubernetes API へのリクエストに割り込んで特殊な操作を行う機能の総称です。
Using Admission Controllers - Kubernetes
Kubernetes の API にはいわゆる認証・認可の後にこの Admission Control のフェイズがあり、リクエストの内容が特定の条件を満たさない場合にエラーを返したり、あるいはリクエストの一部を書き換えたりすることができます。
Admission Contoller はあらかじめ API Server に組み込まれているものの他に、JSON Webhook の形でユーザが定義することもできます。
Dynamic Admission Control - Kubernetes
VPA の Admission Controller は、その名の通りこの仕組みを利用しています。先ほど立ち上げた VPA Admission Controller の実体は Webhook を提供する HTTP サーバで、API Server は Pod 作成リクエストを受け取った際、この Webhook サーバに問い合わせることでリクエスト内容の書き換えを行います。
それでは VPA Admission Controller の実装を確認してみましょう。
まず Admission Controller は、API Server に対して自分自身を Webhook サーバとして登録し、Pod の作成リクエストがあったとき自分に問い合わせが来るようにします。ソースコード上では config.go
内の selfRegistration
関数が相当します。
func selfRegistration(clientset *kubernetes.Clientset, caCert []byte) { ... Webhooks: []v1beta1.Webhook{ { Name: "vpa.k8s.io", Rules: []v1beta1.RuleWithOperations{ { Operations: []v1beta1.OperationType{v1beta1.Create}, Rule: v1beta1.Rule{ APIGroups: []string{""}, APIVersions: []string{"v1"}, Resources: []string{"pods"}, }, }, ... ClientConfig: v1beta1.WebhookClientConfig{ Service: &v1beta1.ServiceReference{ Namespace: "kube-system", Name: "vpa-webhook", }, CABundle: caCert, }, }, }, } }
VAP Admission Controller がこの設定を API Server に登録しておくことで、API Server は Pod に対する Create リクエストを受け取った際、Namespace kube-system
内の Service vpa-webhook
にアクセスするようになります。
そしてアクセスされた VAP Admission Controller 側では、recommendation_provider.go
内の getContainersResources
関数によって、VPA が保持している Target が Resource Request の値として設定されます。
func getContainersResources(pod *v1.Pod, podRecommendation vpa_types.RecommendedPodResources) []ContainerResources { resources := make([]ContainerResources, len(pod.Spec.Containers)) for i, container := range pod.Spec.Containers { resources[i] = newContainerResources() ... resources[i].Requests = recommendation.Target } return resources }
ここで取得した resources
を元にして、最終的には server.go
内の getPatchesForPodResourceRequest
関数が JSON Patch を組み立てて API Server に送り返す流れになっています。
まとめ
本記事では、Kubernetes Pod の Resource Request を実績値から判断し、自動で変更する仕組みである Vertical Pod Autoscaler (VPA) の実装について解説しました。
VPA を使用することで、リソースの使用実績が Pod の配置にフィードバックされるため、クラスタのリソースを有効に活用することができます。
より具体的には、実績から推奨値を算出する Recommender、推奨値に合わない Pod を evict する Updater、Pod の再作成時にリクエストを書き換える Admission Controller の三つのコンポーネントがそれぞれの役割を果たすことで、最終的に Pod の Resource Request が更新されます。現時点では稼働中 Pod の Resource Request を動かしたまま(In-Place で)変更することができないという制限のため、一度 evict する手法が取られています。
ところで、この稼働中の In-Place な変更という問題も深入りすると実はなかなか興味深いのですが、それはまた別の話。