生涯未熟

生涯未熟

プログラミングをちょこちょこと。

GKEの開発環境クラスタはとりあえず「使用率の最適化」やっとこう

GKE / k8s、まだまだ分からんことが多くて学び甲斐があるなぁとしみじみ思っている今日この頃。
今回はそんな中で掲題のコスト最適化のお話をします。

要約

  • 本番環境が載っていないクラスタは自動スケーリング プロファイルをoptimize-utilization(使用率の最適化)やっとくと良い
  • 特に短いスパンでのCronJobを動かしているとコスト改善の可能性大

前提

GKEを使ってサービス運用している時に気になるのはやはりコストですよね。私が関わっているサービスでもコストをもう少し下げようといった声が大きくなり、その中でも大きな割合が割かれているGKEに注目が集まりました。

こういう時、まず手を付けるのは開発環境周りですね。Develop, Staging環境などを一つのクラスタで管理しており、ここなら最悪ぶっ壊しても問題なく着手しやすいのでここからやっていきます。

やったこと

やったこととしてはめちゃくちゃ単純なのですが、

  • GKEクラスタの自動スケーリング プロファイルをbalancedからoptimize-utilization(使用率の最適化)に変更する
  • PDBを設定する

になります。

自動スケーリング プロファイルの変更

GKEクラスタの自動スケーリング プロファイルとは、クラスタ内ノードの削除に対する挙動に関する設定です。balancedとoptimize-utilizationの2種類があるのですが、ドキュメントを読むとoptimize-utilizationは

クラスタ内で余剰リソースを保持するよりも使用率の最適化を優先させます。
このプロファイルを有効にすると、クラスタ オートスケーラーはクラスタをより積極的にスケールダウンします。

とのことです。正直この説明だけでは具体的なところが分からず、しっくり来なかったので調べてみるとGoogleの方の登壇資料に以下のようなことが書かれていました。

ref: Google Kubernetes Engine (GKE) で実現する運用レスな世界

つまり、balancedは「10分毎にスケジュール、使用率50%以下のノードが削除対象」で、optimize-utilizationは「1分毎にスケジュール、使用率85%以下のノードが削除対象」となるようです。optimize-utilizationではより1ノードの凝集度が高まる、といったことがよくわかりますね。

スパイク発生時などに余剰リソースが求められる本番環境では使えませんが、開発環境では使えそうですね。

PDBを設定する

これはノードを削除するために必要な設定となります。設定しましょうで終わる話なのですが、一つハマったところとしてCronJobリソースでの扱いです。
CronJobでは成功/失敗のJobを残すことができ、 successfulJobsHistoryLimitfailedJobsHistoryLimit という設定値があります。ここに幾つのJobを残すか設定できるのですが、開発環境では成功履歴に関しては残す必要がなかったので successfulJobsHistoryLimit を0に設定しておりました。

こうした場合に、成功時のノードの様子を見てみると fluentbitgke-metrics-agent などのkube-systemが残っている状態でした。てっきり、これらがoptimize-utilizationを設定した時にノード削除してくれるものかと思ったらノードが残ったままに・・・
なんでじゃろ?と調べてみると、このkube-systemのnamespaceに配備されているものに関してもPDBを設定する必要がありました。

こちらのCloud Skills Boostにもkube-systemのPodに対してもPDBを設定しましょうと書かれており、完全に盲点でした。

www.cloudskillsboost.google

デフォルトでは、これらの Deployment のシステム Pod のほとんどは、クラスタ オートスケーラーによって完全にオフラインにされ再スケジュールされることを回避します。
このラボでは、kube-system Pod に Pod 停止予算を適用し、クラスタ オートスケーラーが Pod を別のノードに安全にリスケジュールできるようにします。こうすることで、クラスタをスケールダウンする余裕を確保できます。

ラボ内で解説されている中では以下のようなPDBを設定しています。

kubectl create poddisruptionbudget kube-dns-pdb --namespace=kube-system --selector k8s-app=kube-dns --max-unavailable 1
kubectl create poddisruptionbudget prometheus-pdb --namespace=kube-system --selector k8s-app=prometheus-to-sd --max-unavailable 1
kubectl create poddisruptionbudget kube-proxy-pdb --namespace=kube-system --selector component=kube-proxy --max-unavailable 1
kubectl create poddisruptionbudget metrics-agent-pdb --namespace=kube-system --selector k8s-app=gke-metrics-agent --max-unavailable 1
kubectl create poddisruptionbudget metrics-server-pdb --namespace=kube-system --selector k8s-app=metrics-server --max-unavailable 1
kubectl create poddisruptionbudget fluentd-pdb --namespace=kube-system --selector k8s-app=fluentd-gke --max-unavailable 1
kubectl create poddisruptionbudget backend-pdb --namespace=kube-system --selector k8s-app=glbc --max-unavailable 1
kubectl create poddisruptionbudget kube-dns-autoscaler-pdb --namespace=kube-system --selector k8s-app=kube-dns-autoscaler --max-unavailable 1
kubectl create poddisruptionbudget stackdriver-pdb --namespace=kube-system --selector app=stackdriver-metadata-agent --max-unavailable 1
kubectl create poddisruptionbudget event-pdb --namespace=kube-system --selector k8s-app=event-exporter --max-unavailable 1

これでCronJobが成功後に残ったノードも削除されるようになりました。

まとめ

単純なことを2つやっただけですが、結果コストは以下のように最適化されました。

具体的な数値は出せませんが、グラフだけを見ると約半分くらいに最適化されたことがわかりますね。

開発環境クラスタを持っている場合は、この設定をやっておくのが良いと思います✌