この記事の要約
Kubernetes環境におけるインスタンスを動的に管理し、コストの最適化も行ってくれるSpot OceanをEKSクラスタに適用し、その基本的な挙動について検証してみました。
今回の検証で確認できたこと
- クラスタのリソースが不足 → 自動スケールアウト
- 追加されるインスタンスサイズはスケジュール待ちpodの要求リソースに応じて自動選択
- クラスタに組み込まれるインスタンスの種類はSpotインスタンスなどの安価なもの
- podの再配置が可能な場合は、スケールアウト → より高スペックなインスタンスへpodを集約 → 不要インスタンスのスケールイン(削除)という流れでスケールアップのような挙動を取る
- クラスタに余剰リソースあり → 自動スケールイン
- podを自動で再配置し、積極的にインスタンスコストを節約
- 再配置(pod削除/再作成)が不都合なワークロードは個別に指定することで回避可能
- podの再配置が可能な場合は、スケールアウト → より低スペックなインスタンスへpodを集約 → 不要インスタンスのスケールイン(削除)という流れでスケールダウンのような挙動を取る
コスト削減効果について
- 今回の検証環境では68.95%のコスト削減効果でした
- 実環境での効果を保証するものではないのでご注意ください。
Spot by NetAppについて
Spot by NetAppはAWSで言うところのSpotインスタンス1を活用してインスタンスのコストを最適化するFinOpsなマネージドサービスです。
Spotインスタンス自体の解説とSpot by NetAppの活用例については以下をご覧ください。
ちなみにSpot by NetAppの利用料はインスタンス20台分までなんと無料!(宣伝)
Spot Oceanについて
Spot OceanはSpot製品ファミリーの中でKubernetes環境におけるコスト最適化を担うプロダクトです。
k8s上に払い出されるワークロード(pod/job等)に応じて自動でインスタンスを払い出したり、リソース配置を最適化することでインスタンス料金を抑える、というのが主な機能になります。
上記公式サイトでもServerless Infrastructure Container Engineと謳っていますが、k8sのノードを構成するクラウドインスタンスはSpot Oceanによって自動管理されるため、インスタンスやノードグループを作成・管理する必要はありません。
Spot Oceanの機能検証(本編)
Spot Oceanは新規・既存のいずれのクラスタにも適用できますが、今回は新規に検証用EKSクラスタを構築します。
EKSのクラスタを新規に構築し、Spot Oceanの管理下に登録するという操作は、ほぼSpotのコンソール上で完結させることができます。
こちらの記事に詳しく構築手順がまとまっていますので良ければご覧ください。
なお、以後の記載では以下のように用語を区別します。
- スケールアップ: 既存のインスタンスのスペックを増強することでk8sクラスタ内のリソース追加すること
- スケールアウト: インスタンス自体を増やすことでk8sクラスタ内のリソース追加すること
- スケールダウン: 既存のインスタンスのスペックを減少させることでk8sクラスタ内のリソース削減すること
- スケールイン: インスタンス自体を減らすことでk8sクラスタ内のリソース削減すること
初期状態の確認
まずはEKS構築完了時点のクラスタの状況を確認しておきます。
EKS構築時点ではノードを1台も作成しておりませんでしたが、早速Spot Oceanによって自動で1つのノードが作成されています。
そしてそのノード上にはKubernetesのシステム系のpod(kube-system)が乗っている状態です。
というか、spotのコントローラもkube-system内に配置されるのですね。
$ kubectl get node
NAME STATUS ROLES AGE VERSION
ip-172-31-47-243.ap-northeast-1.compute.internal Ready <none> 4m36s v1.18.9-eks-d1db3c
$ kubectl get pod -o wide -A
NAMESPACE NAME READY STATUS RESTARTS AGE IP NODE NOMINATED NODE READINESS GATES
kube-system aws-node-gltdq 1/1 Running 0 2m8s 172.31.47.243 ip-172-31-47-243.ap-northeast-1.compute.internal <none> <none>
kube-system coredns-5fc8d4cdcf-49g9n 1/1 Running 0 11m 172.31.35.160 ip-172-31-47-243.ap-northeast-1.compute.internal <none> <none>
kube-system coredns-5fc8d4cdcf-7jffg 1/1 Running 0 11m 172.31.32.166 ip-172-31-47-243.ap-northeast-1.compute.internal <none> <none>
kube-system kube-proxy-6wvk4 1/1 Running 0 2m8s 172.31.47.243 ip-172-31-47-243.ap-northeast-1.compute.internal <none> <none>
kube-system spotinst-kubernetes-cluster-controller-5d9796c866-mw8xp 1/1 Running 0 6m27s 172.31.36.247 ip-172-31-47-243.ap-northeast-1.compute.internal <none> <none>
はじめに払い出されたノードのインタンスタイプはc5.largeでした(2vCPU/4GiB)
coredns等のkube-system系のpodやSpotのコントローラ等が稼働するノードはどうしても必要になるので、c5.large * 1ノードがSpot Oceanにおける最小構成ということになるのでしょう。
①Podの作成 → 自動スケールアウト
つぎに現状のk8sクラスタの限界を超えるワークロードを作成し、Spot Oceanが自動でk8sノード(Spotインスタンス)を追加する様子を観察します。
現状払い出されているノードにおいて、k8sが使えるリソース分(Allocatable)としては1930ミリコアです。
$ kubectl describe node ip-172-31-47-243.ap-northeast-1.compute.internal | grep -A 7
Allocatable:
attachable-volumes-aws-ebs: 25
cpu: 1930m # k8sが使えるCPUリソースの量(1コア=1000ミリコア)
ephemeral-storage: 18242267924
hugepages-1Gi: 0
hugepages-2Mi: 0
memory: 3131976Ki
pods: 29
つまり以下のスペックを持つpodは現状スケジューリングできないことになります。
apiVersion: v1
kind: Pod
metadata:
name: nginx-2
spec:
containers:
- name: nginx
image: nginx:latest
resources:
requests:
cpu: 2000m # ノードのキャパシティを超えるリソース要求
実際にこのpodを作成するとk8sのスケジューラが払出し先となるノードを見つけることができず、podは一時的にPendingのステータスとなりました。
$ kubectl get pod nginx-2
NAME READY STATUS RESTARTS AGE
nginx-2 0/1 Pending 0 11s
$ kubectl describe pod nginx-2 | grep Events: -A 5
Events:
Type Reason Age From Message
---- ------ ---- ---- -------
Warning FailedScheduling 7m3s default-scheduler 0/1 nodes are available: 1 Insufficient cpu. preemption: 0/1 nodes are available: 1 No preemption victims found for incoming pod.
しかしそのまま何もせずに数分待っていると、podはRunningへと遷移しました。
$ kubectl get pod nginx-2
NAME READY STATUS RESTARTS AGE
nginx-2 1/1 Running 0 3m
$ kubectl describe pod nginx-2 | grep Events: -A 5
Events:
Type Reason Age From Message
---- ------ ---- ---- -------
Warning FailedScheduling 7m3s default-scheduler 0/1 nodes are available: 1 Insufficient cpu. preemption: 0/1 nodes are available: 1 No preemption victims found for incoming pod.
Normal Scheduled 5m8s default-scheduler Successfully assigned default/nginx-2 to ip-172-31-35-204.ap-northeast-1.compute.internal
Normal Pulling 5m8s kubelet Pulling image "nginx:latest"
Spot Oceanによってノードが自動追加されたことでクラスタのキャパシティが増え、podを払い出すことができた様です。
イベントログから、以下の一連のフローは約2分間の間に行われたようです。
- podのスケジューリング(リソース不足で失敗)
- Spot Oceanがスケジュール不可のpodを検知
- 追加インスタンスの払い出し
- EKSクラスタへのノード追加
- podの再スケジューリング(成功)
ちなみに今回追加されたノードのインスタンスタイプはc5."x"largeでした(4vCPU/8GiB)
②自動スケールイン(リソース配置の最適化)
そのままk8sクラスタの状況を観察していると、当初c5.largeのノードで稼働していたはずのpodが、先ほど追加されたc5."x"largeのノードへとどんどん移動していきます。
(c5.largeのノードから削除 → c5."x"largeのノード上に再作成)
$ kubectl get pod -A -o wide
NAMESPACE NAME READY STATUS RESTARTS AGE IP NODE NOMINATED NODE READINESS GATES
default nginx-2 1/1 Running 0 10m 172.31.40.158 ip-172-31-35-204.ap-northeast-1.compute.internal <none> <none>
kube-system aws-node-gfh6r 1/1 Running 0 9m12s 172.31.35.204 ip-172-31-35-204.ap-northeast-1.compute.internal <none> <none>
kube-system aws-node-gltdq 1/1 Running 0 24m 172.31.47.243 ip-172-31-47-243.ap-northeast-1.compute.internal <none> <none>
kube-system coredns-5fc8d4cdcf-7jffg 0/1 Terminating 0 34m 172.31.32.166 ip-172-31-47-243.ap-northeast-1.compute.internal <none> <none>
kube-system coredns-5fc8d4cdcf-sm4qj 0/1 Running 0 7s 172.31.39.80 ip-172-31-35-204.ap-northeast-1.compute.internal <none> <none>
kube-system coredns-5fc8d4cdcf-tjrp6 1/1 Running 0 47s 172.31.41.56 ip-172-31-35-204.ap-northeast-1.compute.internal <none> <none>
kube-system kube-proxy-6wvk4 1/1 Running 0 24m 172.31.47.243 ip-172-31-47-243.ap-northeast-1.compute.internal <none> <none>
kube-system kube-proxy-h5v8n 1/1 Running 0 9m12s 172.31.35.204 ip-172-31-35-204.ap-northeast-1.compute.internal <none> <none>
kube-system spotinst-kubernetes-cluster-controller-5d9796c866-mw8xp 0/1 Terminating 0 29m 172.31.36.247 ip-172-31-47-243.ap-northeast-1.compute.internal <none> <none>
kube-system spotinst-kubernetes-cluster-controller-5d9796c866-t97mx 1/1 Running 0 87s 172.31.32.220 ip-172-31-35-204.ap-northeast-1.compute.internal <none> <none>
眺めている間に全てのpodが移動完了し、空っぽになったc5.largeのノードならびにインスタンスは削除されました。
どうやら先ほど追加されたc5.xlargeのインスタンスだけでクラスタ内の全てのワークロードが賄えるとSpotが判断し、不要なc5.largeのインスタンス料金を削減しにかかった様です。
ここまでの流れをまとめると以下の様になります。
podのスケジューリング(リソース不足で失敗)
↓
c5."x"largeのインスタンスを追加(スケールアウト)
↓
podの再スケジューリング(c5."x"large上で起動)
↓
c5.largeのインスタンス上で稼働していた既存のpod群を、c5."x"largeのインスタンスへ移行
↓
c5.largeのインスタンスを削除(スケールイン)
スケールアウトとリソース配置の最適化をシームレスに実行したことで、擬似的にではありますがSpot Oceanの機能によって1つのノードをスケールアップした、という見方もできそうです。
③podの削除 → スケールダウン
①で作成したpodを削除してみます。
つまりEKS構築時点のリソース状況に戻ることになるので、インスタンス構成も初期状態(c5.large * 1)に自動で戻るのでしょうか?
$ kubectl delete pod nginx-2
pod "nginx-2" deleted
結果は、c"3".largeと初期状態とはまた別のインスタンスに移行される形でインスタンスがスケールダウンされました。
Spotコンソールでのログはこんな感じです。
Revert to lower cost node(安価なノードへの切り戻し) というイベントが記録されています。
この際のkubernetes側のログを取ることを失念してしまったのですが、以下の流れでスケールダウンが実施されたものと思われます。
podの削除
↓
c3.largeのインスタンスを追加(スケールアウト)
↓
c5."x"largeのインスタンス上で稼働していた既存のpod群を、c3.largeのインスタンスへ移行
↓
c5."x"largeのインスタンスを削除(スケールイン)
公式ドキュメントを読んでみる
実機を使った検証としては以上ですが公式ドキュメントにk8sノードのスケーリングに関する仕様がセクションがあったので、こちらを読んで理解を深めます(いまさら)
スケールアップ(スケールアウト)について
- スケジューリングできないpodを検知し、自動でノード追加を行う(上記で確認した通り)
- 追加するノードはスケジューリング不可なpodの合計リソースを計算し、最適なインスタンスタイプを選択する
- インスタンス追加はリソース不足だけでなくアンチアフィニティ等によってもトリガーされる
- 例として、podAntiAffinityによって3つのレプリカを別々のアベイラビリティゾーンに分散するようなスペックを持つDeploymentを作成した場合、Spot Oceanによって3つのノードが自動追加される
スケールダウンについて
- リソース効率の低いノードをプロアクティブに検出し、インスタンスを削除できるかどうかのシミュレートを行う
- スケールダウンできるかどうかのシミュレートにはPodDisruptionBudget(レプリカの同時停止許容数)が考慮される
- Spot Oceanが持つ"maxScaleDownPercentage"というパラメータで一度に削除できるノードの割合を制御できる
- シミュレートで削除可能と判断されたノードに対してdrain操作が行われる(新規podを該当ノードへスケジューリングされない様にしつつ、起動済みpodを他のノードに逃す)
- ノードからのpodのEvictionは120秒間の間に分散される
- 例えば削除対象のノードに10pod稼働していた場合、12秒間に1podずつEvict(もとのノードからpod削除)される
- Evictのリトライタイムアウトは1分。それを超えた場合、Podは別のノードに移行されることなく強制削除される。
- ノードからのpodのEvictionは120秒間の間に分散される
- 重要なワークロードが自動削除されることを防止するために特定の方法でスケールダウンを抑止することができる
- Podに特定のラベルをつける(spotinst.io/restrict-scale-down:true)
- このラベルがついているpodが乗っているインスタンスは削除されなくなる
- 上記のSpot独自のラベル以外にCluster Autoscalerのラベル(cluster-autoscaler.kubernetes.io/safe-to-evict: false)をつけても同じ動作となる
- 仮想ノードグループに対して"Restrict Scale Down"を設定
- SpotコンソールまたはAPIで設定
- 設定した仮想ノードグループ(=特定のインスタンスの集合)ではスケールダウンが無効になる
- Podに特定のラベルをつける(spotinst.io/restrict-scale-down:true)
おまけ:今回の検証環境におけるコスト削減効果
Spot Ocean含めSpot by NetAppの1番のバリューはインスタンスコストの節約ですので、今回の環境でどれだけコスト削減されたのかも確認しておきます。
今回の検証では半日程度のインスタンス稼働かつ、検証用ワークロードしかデプロイしておりませんので、あくまで参考情報となります。
まとめ
Spot Oceanにははじめて触れましたが、インスタンスを管理しなくて良い&料金の節約までしてくれる、というのは楽でいいですね。
一方でコストの最適化を図るために、インスタンスやその上で稼働しているワークロード(pod等)がかなりダイナミックに動く(Spot Oceanが動かしている)のも印象的でした。
本番系かつステートフルなワークロードを扱うクラスタでは特にスケールダウンの仕様や制御方法について学ぶ必要があると感じます。
その辺りはまた時間をみつけて検証してみたいと思います。
-
Spot by NetApp自体はAWS以外にもMicrosoft Azure, Google Cloudに対応しています。 ↩