チェシャ猫の消滅定理

数学にプログラミング、素敵なもの何もかも。

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-prioritynon-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 になりました。

以上から、keyeffect が同じで 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 です。

GHCJSHaskellソースコードJavaScript に変換するコンパイラで、GHC をフォークすることによって開発されています。

github.com

Miso は GHCJS 上で The Elm Architecture を実装するためのフレームワークです。Miso では事実上サーバサイドで書けない Elm と異なり、サーバサイドとの Model の共有や、初回アクセス時の HTML をサーバ側で構築するサーバサイドレンダリング (SSR) の仕組みが提供されています。

github.com

ccvanishing.hateblo.jp

JSaddleHaskell から JavaScript の関数を呼ぶための DSL です。Lens インタフェースに対応した JSM モナドが提供されており、GHCJS でコンパイルした場合は単に IO モナドに変換されます。

main :: IO ()
main = do
    -- console.log('Hello, JSaddle!');
    jsg "console" ^. js1 "log" (val "Hello, JSaddle!")

github.com

これら要素技術の簡単な解説に加えて、LT の後半ではライブコーディングのパフォーマンスを披露しました。

作成したのは、簡単なコメントが残せるゲストブックを模した、以下のようなアプリです。バックエンドとして Firebase Realtime DB を利用しているため、複数のブラウザから開いた場合でも書き込みはリアルタイムで同期されます。

f:id:y_taka_23:20190529193951p:plain

前掲のスライドは公開用にソースコードを補足してありますが、当日は文字通り、その場で段階的にコードを追記・変更することで機能が画面に反映されていく様子を紹介しました。

  1. まず動きのない HTML だけの View をレンダリングする
  2. ユーザからの入力に反応して Action を発行し、Update で Model に反映する
  3. Effect を利用してコメントを Realtime DB に保存する
  4. Subscription を利用して Realtime DB の変更を検知する

LT としてはあまり見ない形式ですが、Twitter 上で見る限り割と好評だったようです。こんな風に、実装の結果が目で見て分かりやすいのはフロントエンドの強みだなと改めて感じました。

(ちなみに @u_taka_23 ではなく @y_taka_23 です)

ところで今回のライブコーディング、ハンズオン形式のチュートリアルとして仕立て直して公開しようかなとも思っているのですが、それはまた別の話。

Docker Meetup Tokyo #28 で Scheduler のカスタマイズについて話してきました

先日行われた Docker Meetup Tokyo #28で、Kubernetes Scheduler の挙動をカスタマイズする方法について発表してきました。

なお Scheduler のカスタマイズについては、つい最近 Kubernetes Meetup Tokyo #16 でも発表しています。ドキュメント類へのリンクも含めてまとめたものが以下の記事です。

ccvanishing.hateblo.jp

両方のスライドを見比べて頂ければ分かる通り、内容としてはオーバラップしている部分がかなりあります。

ただし、前回はあくまでも 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

github.com

kube-batch はバッチ処理向けのスケジューラで、かつて kube-arbitrator と呼ばれていたプロジェクトが改名しました。たった 1 行だけの Wiki が味わい深いです。

PodGroup を指定することで複数の Pod を組にして扱う機能を備えているのが特徴で、この仕組みは Kubernetes機械学習用基盤として使用する kube-flow プロジェクトにも導入されるようです。

Poseidon

github.com

Poseidon は、グラフ理論を利用して複雑かつ効率的なスケジューリングを目指す新機軸のプロジェクトです。

元になっているのは Firmament Scheduler と呼ばれる汎用のタスクスケジューリングの仕組みで、配置の制約を最小費用流問題に帰着させて最適化を行うようです。Kubernetes の Pod と Node に対して Firmament を実装したものが Poseidon という関係になっています。

正直なところ、この記事を書いている時点ではまだ論文を充分読み込んでいないので何とも言えませんが、内容がきちんと把握できたら改めてどこかのイベントで発表したいと思います。

Scheduler Extender

github.com

スケジューリング処理の一部分を 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

github.com

Scheduling Framework のプロポーザルです。今回のスライドは基本的にこのプロポーザルに沿って説明しています。

上でも挙げたような特定用途のために作られたスケジューラは、基本的にスクラッチから開発されています。というのも kube-scheduler の実装は歴史的事情から抽象化が十分でなく、拡張性に限界があるためです。

そこで、Scheduling Framework では設計を刷新して新しい拡張点を定式化します。それぞれの拡張点では、Go のインタフェースとして実装されたプラグインを登録し処理を差し込めるようにするとともに、プラグイン同士が情報をやり取りできる仕組みが構築される予定です。

そう、あくまでも予定です。 プロポーザルには多数の拡張点が述べられていますが、現状で以下の二点のみ実装されています。

  • Reserve - Node が確定した後、Pod ごとの goroutine が生成される前
  • Pre-Bind - Pod ごとの goroutine に入った後、API Server に Binding が登録される前

この二点は、Pod を配置する Node に前もって何かを準備する処理を想定したもので、典型的な用途はネットワークストレージをボリュームとして確保するというものです。

従来、ボリュームの確保とバインドは assumeVolumebindVolume という専用の関数で行われています。一方 GPU などボリューム以外のネットワークリソースについてはユーザが Bind Extender という形で各自実装する必要があります。

そこで、assumeVolume に相当するものとして Reserve プラグインを、bindVolume に相当するものとして Pre-Bind プラグインを実装し、リソース関連の処理を統合して抽象化することが Scheduling Framework の第一弾として意図されていました。が、今のところ拡張点が準備されただけで止まっており、assumeVolumebindVolume もそのまま残っています。まあ、今後の展開を見守りましょう……。

まとめ

本記事では、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-readyunreachable に 300 秒の tolerationSeconds が設定されます。要するに何も設定していない場合は Node の障害から最大 300 秒待って Pod が削除される、ということです。

#70298: critical-pod アノテーションが非推奨に

Pod に対するクリティカルアノテーションが非推奨になりました。アノテーションの代わりに Pod の優先度を使用すべきです。

DNS や Metrics Server といった死なれるとクラスタ全体の動作に影響するような Pod のために、従来 scheduler.alpha.kubernetes.io/critical-pod というアノテーションが用意されていましたが、今回から非推奨になりました。

代わりに、デフォルトで定義されている優先度クラス system-cluster-criticalsystem-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 内でしか使えないことに注意が必要です。

また、この件とは直接関係しませんが、優先度による Preemption の動作原理については半年ほど前に書いた記事があるのでよければこちらもご笑覧ください。

ccvanishing.hateblo.jp

#70040: 要対応

1.13 では kube-scheduler の設定ファイルの apiVersioncomponentconfig/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 を呼び出す際、ソースコードの内部に埋め込む形になるのが普通です。

例えば HaskellJavaScriptコンパイルする 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 となって観測されます。具体的には下記のサンプルレポジトリを参照してください。

github.com

なお、実際に JS 連携する部分をコーディングする際には、boiyaa さんが書かれている詳しい記事も参考になると思います。

boiyaa さんの記事は Elm v1.18 時点で書かれているので起動時に Html.programWithFlags を使用していますが、Elm v1.19 ではここに相当する関数は Browser.element に変更になっています。

動作サンプル

実際に触って動かせるデモもデプロイしておきました。先に挙げたソースコードと合わせて、よかったら参考にしてみてください。

f:id:y_taka_23:20181124221326p:plain デモページ

プレゼンの際にお見せしたデモは単にテキストを置いただけの画面でしたが、公開版はちょっと凝った CSS を付けてみました。カウンタ自体の Elm 側のロジックは非常にシンプルで行数も少ないので、GitHub からは Elm ではなく CSS のレポジトリ扱いされてしまっていますが、それはまた別の話。

猫でもわかる Vertical Pod Autoscaler

先日行われた Kubernetes Meetup Tokyo #13 で、Vertical Pod Autoscaler (VPA) について発表してきました。

VPA は、各コンテナの Resource Request の値を自動的に調整してくれるコンポーネント群です。必要とするリソース(CPU、メモリ)量があらかじめ推測しにくいアプリケーションに対して、実績に基づいてそれらしい値を決めたい場合に効果を発揮します。

本記事ではスライドの補足として、VPA が動作する流れをクラスタ上での実際の挙動を通じて確認し、また内部実装についても踏み込んで解説します。

なお、本記事中で引用している仕様やソースコードは執筆時点で最新のコミット ab9c27e を基準にしています。

github.com

はじめに

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.yamlreplicas を 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 は少し説明が必要かもしれません。可能な値は AutoRecreateInitialOff で、それぞれ次のような挙動になります。

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 の計算式は

 \displaystyle
  scaledResource = originalResource \left(1 +\frac{0.001}{N}\right)^{-2}

Upper Bound の計算式は

 \displaystyle
  scaledResource = originalResource \left(1 +\frac{1}{N}\right)

で与えられます。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

KubernetesAPI にはいわゆる認証・認可の後にこの 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 な変更という問題も深入りすると実はなかなか興味深いのですが、それはまた別の話。