Upgrade to Pro — share decks privately, control downloads, hide ads and more …

Cluster Autoscaler

bells17
March 29, 2022

Cluster Autoscaler

Kubernetes Meetup Tokyo #49で発表したセッション資料です
https://k8sjp.connpass.com/event/240993/

配信URL:
https://youtu.be/KOrantQgXkI?t=2258

bells17

March 29, 2022
Tweet

More Decks by bells17

Other Decks in Programming

Transcript

  1. ▶ @bells17 ▶ Software Engineer ▶ 普段やってること: + Kubernetes 関連コンポーネントの開発

    + Kubernetes as a Service開発 ▶ Kubernetes SIG-Docs Japanese localization reviewer ▶ Kubernetes Internal Organizer ▶ #kubenews ▶ @bells17_
  2. 注意点 ▶ Cluster Autoscaler v1.20.2 ベースでのお話になります(諸事情で若⼲古いです) + https://github.com/kubernetes/autoscaler/tree/cluster-autoscaler-1.20.2 ▶ あくまでCluster

    Autoscalerの実装を追った結果での理解の説明になるので、 間違いが含まれている可能性があります ▶ ぶっちゃけロジックが複雑過ぎるので、全体像を掴むのに問題無いと思う レベルで所々ある程度説明を端折ってたりまるめてます
  3. Cluster Autoscaler ▶ Kubernetesクラスター上で動作するPodの要求リソースに応じて Nodeの台数を⾃動で増減させるコンポーネント ▶ Podの要求リソース(resource request)とNodeのキャパシティを⽐較する ことでNode台数の増減をコントロールする ▶

    Node台数の調整を⾏うために、Kubernetesクラスター実⾏環境に応じた Cloud Providerの選択もしくは実装~組み込みが必要となる ▶ #sig-autoscalingによって管理 ▶ Kubernetes v1.8のタイミングでVersion 1.0 (GA)がリリースされた ▶ ScaleUp/ScaleDownを判定するためのロジックが本当に複雑…
  4. 公式でサポートされるCloud Provider ▶ AliCloud ▶ AWS ▶ Azure ▶ BaiduCloud

    ▶ CloudStack ▶ Cluster API ▶ DigitalOcean ▶ Exoscale ▶ GCP ▶ Huawei Cloud ▶ Ionos Cloud Managed Kubernetes ▶ kubemark ▶ OpenStack Magnum ▶ Packet この中に利用したい環境のCloud Providerが無い場合は自作する必要あり
  5. 新たに⽴ち上げようとしたPodはリソース不⾜でpendingステータスに なってしまいます LVCFDUMHFUQPE /".&3&"%:45"5643&45"354"(& OHJOY3VOOJOHE OHJOY3VOOJOHE OHJOY1FOEJOHT LVCFDUMEFTDSJCFQPEOHJOY தུ 

    &WFOUT 5ZQF3FBTPO"HF'SPN.FTTBHF  8BSOJOH'BJMFE4DIFEVMJOHTEFGBVMUTDIFEVMFSOPEFTBSFBWBJMBCMFOPEF T  IBEUBJOU\OPEFSPMFLVCFSOFUFTJPDPOUSPMQMBOFUSVF^ UIBUUIFQPEEJEOUUPMFSBUF *OTV⒏DJFOUDQV
  6. リソースが追加されたことでpendingだったPodが起動されました LVCFDUMEFTDSJCFQPEOHJOY 4UBUVT3VOOJOH தུ  &WFOUT 5ZQF3FBTPO"HF'SPN.FTTBHF  8BSOJOH'BJMFE4DIFEVMJOHNTEFGBVMUTDIFEVMFSOPEFTBSFBWBJMBCMFOPEF T

    IBEUBJOU\OPEFSPMFLVCFSOFUFTJP DPOUSPMQMBOFUSVF^ UIBUUIFQPEEJEOUUPMFSBUF *OTV⒏DJFOUDQV 8BSOJOH'BJMFE4DIFEVMJOHNT YPWFSNT EFGBVMUTDIFEVMFSOPEFTBSFBWBJMBCMFOPEF T IBEUBJOU\OPEFSPMFLVCFSOFUFTJP DPOUSPMQMBOFUSVF^ UIBUUIFQPEEJEOUUPMFSBUF *OTV⒏DJFOUDQV 8BSOJOH'BJMFE4DIFEVMJOHNTEFGBVMUTDIFEVMFSOPEFTBSFBWBJMBCMFOPEF T IBEUBJOU\OPEFSPMFLVCFSOFUFTJP DPOUSPMQMBOFUSVF^ UIBUUIFQPEEJEOUUPMFSBUF OPEF T IBEUBJOU\OPEFLVCFSOFUFTJPOPUSFBEZ^ UIBUUIFQPEEJEOUUPMFSBUF *OTV⒏DJFOUDQV /PSNBM4DIFEVMFENTEFGBVMUTDIFEVMFS4VDDFTTGVMMZBTTJHOFEEFGBVMUOHJOYUPDOMMOQEQGDDTBOECPYXPSLFS /PSNBM5SJHHFSFE4DBMF6QNTDMVTUFSBVUPTDBMFSQPEUSJHHFSFETDBMFVQ<\TPNFOPEFHSPVQ NBY ^> /PSNBM1VMMJOHNTLVCFMFU1VMMJOHJNBHFOHJOY /PSNBM1VMMFENTLVCFMFU4VDDFTTGVMMZQVMMFEJNBHFOHJOYJOT /PSNBM$SFBUFENTLVCFMFU$SFBUFEDPOUBJOFSOHJOY /PSNBM4UBSUFENTLVCFMFU4UBSUFEDPOUBJOFSOHJOY
  7. ScaleUp時のCluster Autoscalerのログはこんな感じ *TUBUJD@BVUPTDBMFSHP>4UBSUJOHNBJOMPPQ *LMPHYHP>1PEEFGBVMUOHJOYJTVOTDIFEVMBCMF *TDBMF@VQHP>6QDPNJOHOPEFT *TDBMF@VQHP>#FTUPQUJPOUPSFTJ[FTPNFOPEFHSPVQ *TDBMF@VQHP>&TUJNBUFEOPEFTOFFEFEJOTPNFOPEFHSPVQ *TDBMF@VQHP>'JOBMTDBMFVQQMBO<\TPNFOPEFHSPVQ NBY ^>

    *TDBMF@VQHP>4DBMFVQTFUUJOHHSPVQTPNFOPEFHSPVQTJ[FUP *FWFOU@TJOL@MPHHJOH@XSBQQFSHP>&WFOU W0CKFDU3FGFSFODF\,JOE$POpH.BQ /BNFTQBDFDMVTUFS BVUPTDBMFSSBODIFS /BNFDMVTUFSBVUPTDBMFSTUBUVT 6*%DEGCFGEDGCCD "1*7FSTJPOW  3FTPVSDF7FSTJPO 'JFME1BUI^ UZQF/PSNBMSFBTPO4DBMFE6Q(SPVQ4DBMFVQTFUUJOHHSPVQTPNFOPEFHSPVQTJ[FUP *FWFOU@TJOL@MPHHJOH@XSBQQFSHP>&WFOU W0CKFDU3FGFSFODF\,JOE$POpH.BQ /BNFTQBDFDMVTUFS BVUPTDBMFSSBODIFS /BNFDMVTUFSBVUPTDBMFSTUBUVT 6*%DEGCFGEDGCCD "1*7FSTJPOW  3FTPVSDF7FSTJPO 'JFME1BUI^ UZQF/PSNBMSFBTPO4DBMFE6Q(SPVQ4DBMFVQTFUUJOHHSPVQTPNFOPEFHSPVQTJ[FUP *FWFOU@TJOL@MPHHJOH@XSBQQFSHP>&WFOU W0CKFDU3FGFSFODF\,JOE1PE /BNFTQBDFEFGBVMU  /BNFOHJOY 6*%EEFDEDDCGGFB "1*7FSTJPOW 3FTPVSDF7FSTJPO 'JFME1BUI^ UZQF /PSNBMSFBTPO5SJHHFSFE4DBMF6QQPEUSJHHFSFETDBMFVQ<\TPNFOPEFHSPVQ NBY ^>
  8. Cluster Autocalerのログからunneeded nodeに選ばれTaintが付与されたことがわかります (削除⾃体はunneeded nodeに選ばれてから時間を少し置くのでまだされません) *TUBUJD@BVUPTDBMFSHP>4UBSUJOHNBJOMPPQ *TUBUJD@BVUPTDBMFSHP>$BMDVMBUJOHVOOFFEFEOPEFT *QSF@pMUFSJOH@QSPDFTTPSHP>4LJQQJOHDOMMOQKEGSRTBOECPYNBTUFSOPOPEFHSPVQDPOpH *TDBMF@EPXOHP>/PEFDOMMOQEQGDDTBOECPYXPSLFSJTOPUTVJUBCMFGPSSFNPWBMDQVVUJMJ[BUJPOUPPCJH 

     *TDBMF@EPXOHP>/PEFDOMMOQEQGDDTBOECPYXPSLFSJTOPUTVJUBCMFGPSSFNPWBMDQVVUJMJ[BUJPOUPPCJH   *TDBMF@EPXOHP>/PEFDOMMOQEQGDDTBOECPYXPSLFSDQVVUJMJ[BUJPO *TUBUJD@BVUPTDBMFSHP>DOMMOQEQGDDTBOECPYXPSLFSJTVOOFFEFETJODF 65$N EVSBUJPOT *TUBUJD@BVUPTDBMFSHP>4DBMFEPXOTUBUVTVOOFFEFE0OMZGBMTFMBTU4DBMF6Q5JNF  65$N MBTU4DBMF%PXO%FMFUF5JNF 65$ N MBTU4DBMF%PXO'BJM5JNF 65$N TDBMF%PXO'PSCJEEFOGBMTF JT%FMFUF*O1SPHSFTTGBMTFTDBMF%PXO*O$PPMEPXOGBMTF *TUBUJD@BVUPTDBMFSHP>4UBSUJOHTDBMFEPXO *TDBMF@EPXOHP>DOMMOQEQGDDTBOECPYXPSLFSXBTVOOFFEFEGPST *TDBMF@EPXOHP>/PDBOEJEBUFTGPSTDBMFEPXO *EFMFUFHP>4VDDFTTGVMMZBEEFE%FMFUJPO$BOEJEBUF5BJOUPOOPEFDOMMOQEQGDDTBOECPYXPSLFS
  9. ScaleDown時のCluster Autoscalerのログはこんな感じ *TUBUJD@BVUPTDBMFSHP>DOMMOQEQGDDTBOECPYXPSLFSJTVOOFFEFETJODF  65$N EVSBUJPONT *TUBUJD@BVUPTDBMFSHP>4DBMFEPXOTUBUVTVOOFFEFE0OMZGBMTFMBTU4DBMF6Q5JNF 65$N MBTU4DBMF%PXO%FMFUF5JNF 65$N

     MBTU4DBMF%PXO'BJM5JNF 65$N TDBMF%PXO'PSCJEEFOGBMTFJT%FMFUF*O1SPHSFTTGBMTF TDBMF%PXO*O$PPMEPXOGBMTF *TUBUJD@BVUPTDBMFSHP>4UBSUJOHTDBMFEPXO *TDBMF@EPXOHP>DOMMOQEQGDDTBOECPYXPSLFSXBTVOOFFEFEGPSNT *TDBMF@EPXOHP>4DBMFEPXOSFNPWJOHFNQUZOPEFDOMMOQEQGDDTBOECPYXPSLFS *FWFOU@TJOL@MPHHJOH@XSBQQFSHP>&WFOU W0CKFDU3FGFSFODF\,JOE$POpH.BQ /BNFTQBDFDMVTUFSBVUPTDBMFSSBODIFS  /BNFDMVTUFSBVUPTDBMFSTUBUVT 6*%DEGCFGEDGCCD "1*7FSTJPOW 3FTPVSDF7FSTJPO 'JFME1BUI^ UZQF /PSNBMSFBTPO4DBMF%PXO&NQUZ4DBMFEPXOSFNPWJOHFNQUZOPEFDOMMOQEQGDDTBOECPYXPSLFS *FWFOU@TJOL@MPHHJOH@XSBQQFSHP>&WFOU W0CKFDU3FGFSFODF\,JOE$POpH.BQ /BNFTQBDFDMVTUFSBVUPTDBMFSSBODIFS  /BNFDMVTUFSBVUPTDBMFSTUBUVT 6*%DEGCFGEDGCCD "1*7FSTJPOW 3FTPVSDF7FSTJPO 'JFME1BUI^ UZQF /PSNBMSFBTPO4DBMF%PXO&NQUZ4DBMFEPXOSFNPWJOHFNQUZOPEFDOMMOQEQGDDTBOECPYXPSLFS *EFMFUFHP>4VDDFTTGVMMZBEEFE5P#F%FMFUFE5BJOUPOOPEFDOMMOQEQGDDTBOECPYXPSLFS *FWFOU@TJOL@MPHHJOH@XSBQQFSHP>&WFOU W0CKFDU3FGFSFODF\,JOE/PEF /BNFTQBDF /BNFDOMMOQEQGDD TBOECPYXPSLFS 6*%GBGCBFFBDFCCDFCGE "1*7FSTJPOW 3FTPVSDF7FSTJPO 'JFME1BUI^ UZQF/PSNBMSFBTPO 4DBMF%PXOOPEFSFNPWFECZDMVTUFSBVUPTDBMFS
  10. 動作イメージまとめ ▶ 紹介したイメージのようにPodのリソース要求に対してNode側が リソース不⾜に陥った場合、Cluster AutoscalerはNodeの増加を⾏い、 リソースを補填する ▶ 反対にScaleDownを⾏う際は、削除可能なNodeのシミュレートを⾏い、 unneededなNodeが⾒つかった場合は⼀定の時間を置いてから、 uneededなNodeの削除を⾏い台数の削減を⾏う

    ▶ uneededなNodeが⾒つかった際、基本的には削除前に ”DeletionCandidateOfClusterAutoscaler”というTaintが付与され、 対象NodeにPodがスケジューリングされるのを防⽌する ▶ 削除処理中は”ToBeDeletedTaint”というTaintが付与され、削除処理が 実施中であることが確認できるようになっている
  11. 主なプロセス(go routineなど) ▶ main loop: ScaleUp/ScaleDown処理を担当 ▶ CloudProviderNodeInstancesCache Refresh loop:

    + 各NodeGroupに所属するInstance(Node)⼀覧のキャッシュを定期的に リフレッシュする ▶ http server: 以下の2つのEndpointを提供するhttp server ▶ /metrics: prometheus形式のメトリクス取得⽤Endpoint ▶ /health-check: ヘルスチェック⽤Endpoint ▶ 起動するhttp serverのデフォルトポートは8085
  12. CloudProviderNodeInstancesCache Refresh loop 以下の処理を2分毎に実⾏してるだけ // Refresh refreshes cache. func (cache

    *CloudProviderNodeInstancesCache) Refresh() { klog.Infof("Start refreshing cloud provider node instances cache") refreshStart := time.Now() nodeGroups := cache.cloudProvider.NodeGroups() cache.removeEntriesForNonExistingNodeGroupsLocked(nodeGroups) for _, nodeGroup := range nodeGroups { nodeGroupInstances, err := nodeGroup.Nodes() if err != nil { klog.Errorf("Failed to get cloud provider node instance for node group %v, error %v", nodeGroup.Id(), err) } cache.updateCacheEntryLocked(nodeGroup, &cloudProviderNodeInstancesCacheEntry{nodeGroupInstances, time.Now()}) } klog.Infof("Refresh cloud provider node instances cache finished, refresh took %v", time.Now().Sub(refreshStart)) } https://github.com/kubernetes/autoscaler/blob/cluster-autoscaler-1.20.2/cluster-autoscaler/clusterstate/utils/node_instances_cache.go#L154-L176
  13. 2. NodeGroup/Nodeの状態チェック 1. CloudProvider側のInstance情報には存在して、k8s Nodeには存在しない Node(unregisteredNodes)が登録から⼀定時間経過していれば、そのNodeを 削除して処理を中断する + ⻑期間unregisteredNodesである=Nodeのprovisioningに失敗したとみなされたので削除さ れる

    2. ClusterStateRegistryにUnreadyであると判断されたNode数が⼀定数を超えた場合はCluster がUnhealthyだと判断されたら処理を中断する 3. Node作成中にCloudProviderからエラーだと判断されたInstanceがあった場合、それらの Instanceを削除 4. Cloud Providerの各NodeGroupのInstance数がNodeGroupの想定サイズ内に収まっていない 場合、Instance数をNodeGroupの想定サイズに収まるように台数削減を⾏い、処理を中断する
  14. 3. ScaleUp 1. unschedulableなPodが存在しなければScaleUpをSkip(そもそもScaleUpする必要が無い) 2. CloudProviderから取得できるNodeGroupをフィルタリング~unschedulablePodの スケジューリングシュミレーションをパスしたNodeGroupをScaleUp候補にする 3. ScaleUp候補のNodeGroupの中からExpanderを⽤いて最終的にScaleUpを⾏うNodeGroupを 選定する

    + 選択可能なExpanderはrandom/most-pods/least-waste/price/priorityの5つ 4. 選定されたNodeGroupと類似のNodeGroupをScaleUpを⾏うNodeGroupに追加する + 類似NodeGroupの追加は“--balance-similar-node-groups”オプションをtrueに設定していた 場合のみ 5. ScaleUpを⾏うNodeGroup⼀覧の内、どのNodeGroupを何台増加するかの調整を⾏う + NodeGroupの台数増加は可能な限り均等に⾏われる + トータルで増加するNode台数はスケジューリングシュミレーションの際に決定される 6. 上記の調整内容を元にScaleUpを実⾏ ~ 成功したら処理を終了(ScaleDownプロセスには移⾏しない)
  15. 4. ScaleDown準備 1. ScaleUpが⾏われなかった場合はScaleDownに関する処理を開始 2. Nodeリソース(GPU or CPU or Mem)の中で最も使⽤率の⾼いリソースの使⽤率が⼀定値以下

    であるNodeからScaleDown候補となるuneededNode⼀覧の⽣成を⾏う a. emptyNode: 以下のような条件を満たすPodを持たないNode 1. Mirro Podではない 2. DaemonSet Podではない 3. ReplicaSetやJobなどに紐付いていない 4. etc b. ⾮emptyNodeの中で以下のような条件をNode + 対象Nodeの削除をブロッキングするPodが無い + 対象Nodeで現在動作しているPodの中で再スケジューリングが必要なPodが全て他のNodeに 再スケジューリングできる + 再スケジューリングできるかはScheduling Framework PreFilter/Filterによって検証する
  16. 5. ScaleDown 1. unneededNodesに追加されてから⼀定時間が経過したNodeを対象にScaleDown処理を開始 2. 対象となるunneededNodesからemptyNodesを抽出 3. emptyNodesが存在すれば全てのemptyNodesの削除を実施 + 削除処理はNode単位でgo

    routineを利⽤して⾮同期に⾏う + 削除前にNodeに”ToBeDeletedTaint”Taintを付与する 4. 削除開始したemptyNodesが1つ以上あった場合はScaleDown処理を終了する 5. emptyNodesの削除が⾏われなかった場合は、⾮emptyNodesの削除処理を⾏う 6. ⾮emptyNodesの中から1つのNodeを取り出しemptyNodesの際と同様にgo routineで⾮同期 での削除を開始する 7. もし、ScaleDownを開始するNodeが1つもなかった場合はunneededNodesに対して "DeletionCandidateOfClusterAutoscaler"Taintの付与を⾏う
  17. Karpenter ▶ AWSが作成した独⾃のCluster Autoscaler ▶ 最新バージョンは現在v0.7.3なのでまだGAしてない? + 今回調査したKarpenterもこのバージョン ▶ controller-runtimeによるOperator形式で実装されている

    ▶ autoscalingを⾏うための設定をprovisioning Custom Resourceとして定義 することでautoscalingを実現 ▶ Cloud Provider機能によって任意の実⾏環境でKarpenterを動作させること ができるが、現在の実装はAWSのみ ▶ コードの実装がCluster Autoscalerの1000倍くらい簡単でコードもきれい logo: https://github.com/aws/karpenter/blob/v0.7.3/website/static/logo.png
  18. KarpenterとCluster Autoscaler ▶ https://karpenter.sh/v0.7.3/concepts/ によるとCluster Autoscalerと ⽐べ以下のような特徴があるとのこと + Group-less node

    provisioning: + NodeGroupのような概念はなく、直接provisioningするNodeを管理する + またKarpenterによって起動された訳ではないNodeはScaleDownしない + Scheduling enforcement: + kube-schedulerを介さずにbinpackingしたPodを provisioningしたNodeにKarpenterが直接スケジュールする + なので例えばCSIStorageCapacityの情報など、kube-schedulerであればScheduling Frameworkがスケジューリング可能かをチェックするような厳密なチェックを⾏わずに スケジューリングを⾏う + Designed to handle the full flexibility of the cloud: + クラウド側インスタンスタイプの柔軟な選択が可能 logo: https://github.com/aws/karpenter/blob/v0.7.3/website/static/logo.png
  19. 感想 ▶ ScaleUp/Downのロジックがめっちゃ複雑だった… ▶ とはいえ計算対象からMirror PodやDaemonSet Podなどを取り除く必要があるなど、 いろんなロジックで計算しなければいけないことを知れたのは⾯⽩かった ▶ また、シミュレーションの際にPreFilter/Filterを⾏ったチェックをしていたり、ScaleUpが

    必要なNode数をFirst Fit Decreasing bin-packing approximation algorithmという アルゴリズムでPodの箱詰めを⾏って計算しているというやり⽅も興味深かった ▶ Karpenterのロジックと⽐べるとだいぶ複雑なので、ある程度Karpenterで間に合うレベル であればこのくらいシンプルな⽅が個⼈的には良いかなという気も… + とはいえスケジューリング可能かのチェックがかなり簡易的だしKarpenter側で直接 Nodeにスケジューリングしてる箇所は場合によっては問題を発⽣させる可能性が⾼く なるので難しいところ
  20. 参考資料 ▶ https://github.com/kubernetes/autoscaler/tree/cluster-autoscaler-1.20.2/cluster-autoscaler ▶ https://github.com/kubernetes/autoscaler/tree/cluster-autoscaler-1.20.2 ▶ https://kubernetes.io/blog/2016/07/autoscaling-in-kubernetes/ ▶ https://docs.oracle.com/ja-jp/iaas/Content/ContEng/Tasks/contengusingclusterautoscaler.htm ▶

    https://docs.aws.amazon.com/eks/latest/userguide/autoscaling.html ▶ https://cloud.google.com/kubernetes-engine/docs/concepts/cluster-autoscaler ▶ https://docs.microsoft.com/ja-jp/azure/aks/cluster-autoscaler ▶ https://en.wikipedia.org/wiki/Bin_packing_problem ▶ https://en.wikipedia.org/wiki/First-fit-decreasing_bin_packing ▶ https://github.com/aws/karpenter/tree/v0.7.3 ▶ https://karpenter.sh/v0.7.3/concepts/ ▶ https://karpenter.sh/v0.7.3/provisioner/ ▶ https://karpenter.sh/v0.7.3/aws/provisioning/ ▶ https://github.com/aws/karpenter/blob/v0.7.3/designs/v1alpha4-api.md ▶ https://github.com/aws/karpenter/blob/v0.7.3/designs/termination.md ▶ https://github.com/aws/karpenter/blob/v0.7.3/designs/limits.md ▶ https://github.com/aws/karpenter/blob/v0.7.3/designs/bin-packing.md
  21. インデックス ▶ Cloud Provider実装について ▶ 主なオプション ▶ ClusterSnapshot ▶ PredicateChecker

    ▶ Estimator ▶ Expander⼀覧 ▶ ClusterStateStrategy ▶ ScaleDown props ▶ ScaleUp/Down ざっくりフロー
  22. Cloud Provider ▶ Cluster Autoscalerに組み込んで実⾏される ▶ Goで定義されたインターフェイスのメソッドを実装して組み込むこと で、独⾃のCloud Provider実装をCluster Autoscalerから利⽤することが

    できる ▶ Goのインターフェイスであるインターフェイスは↓にある https://github.com/kubernetes/autoscaler/blob/cluster-autoscaler- 1.20.2/cluster-autoscaler/cloudprovider/cloud_provider.go
  23. CloudProvider Interface type CloudProvider interface { Name() string NodeGroups() []NodeGroup

    NodeGroupForNode(*apiv1.Node) (NodeGroup, error) Pricing() (PricingModel, errors.AutoscalerError) GetAvailableMachineTypes() ([]string, error) NewNodeGroup(machineType string, labels map[string]string, systemLabels map[string]string, taints []apiv1.Taint, extraResources map[string]resource.Quantity) (NodeGroup, error) GetResourceLimiter() (*ResourceLimiter, error) GPULabel() string GetAvailableGPUTypes() map[string]struct{} Cleanup() error Refresh() error }
  24. NodeGroup Interface type NodeGroup interface { MaxSize() int MinSize() int

    TargetSize() (int, error) IncreaseSize(delta int) error DeleteNodes([]*apiv1.Node) error DecreaseTargetSize(delta int) error Id() string Debug() string Nodes() ([]Instance, error) TemplateNodeInfo() (*schedulerframework.NodeInfo, error) Exist() bool Create() (NodeGroup, error) Delete() error Autoprovisioned() bool }
  25. 独⾃のCloud Providerを実装する ▶ CloudProvider/NodeGroupの2つのインターフェイスを満たした実装を⾏う ▶ 上記CloudProviderインスタンスを⽣成するBuilderを実装する ▶ Cluster Autoscalerのmain.goをコピーして、上記Builderを利⽤してCloud Providerを

    ⽣成できるように書き換える (Cluster Autoscalerはライブラリとして分離されてないためこういった対応が必要) ▶ mainifestsファイルについては各CloudProviderのexampleにあるものが参考になる
  26. 注意点 ▶ NodeGroup内のNode削除を⾏うDeleteNodesは実際のNodeの削除完了が成功するまで 待機する必要がある ▶ Cloud Provider Interfaceの中には”NewNodeGroup”というメソッドがあり、まるで現在 稼働中のクラスターに新たなNodeGroupを追加できるかのように思えるが、実際にはこの メソッドが利⽤されることは無い点に注意

    + これは現在最新のv1.23の実装でも同様 + NewNodeGroupメソッドを利⽤した新たなNodeGroupの追加を⾏いたい場合は、 Cluster Autoscalerの拡張ポイントの1つであるNodeGroupManager processorの独⾃実装 が必要 + 不要なNodeGroupを削除するNodeGroup.Deleteも同様に呼び出されることは無い
  27. 主なオプション オプション名 説明 cloud-provider 利⽤するCloud Provider cloud-config 選択したCloud Providerに渡される設定ファイルのパス Cloud側に接続するためのAPIキーなどを主に設定する

    scan-interval Cluster Autoscalerのループ間隔を設定する デフォルト: 10秒 namespace Cluster Autoscalerを起動するNamespace Statusを保存するConfigMapとLeaderElectionのResourceLockに指定したNamespaceが利⽤される デフォルト: kube-system node-autoprovisioning-enabled 本来であればtrueにすれば必要に応じて新たなNodeGroupを作成してくれるオプションなはず しかし実装が無いので現実には機能しない scale-down-enabled ScaleDown機能を有効にするかどうか? デフォルト: true balance-similar-node-groups trueにするとScaleUpの際に選ばれたNodeGroupと類似のNodeGroupをScaleUp処理に追加するようになる expander ScaleUpの際に候補となるNodeGroupから実際にScaleUpするNodeGroupを選ぶ際のアルゴリズムを選択する 組み込まれてるexpanderはrandom/most-pods/least-waste/price/priorityの5つ デフォルト: random estimator NodeGroupをScaleUpする際に何台のNode追加すれば対象NodeGroupのNodeにスケジューリング可能なunschedulablePodを全て スケジューリングできるかを計算するアルゴリズムを選択する 組み込まれてるのはbinpacking1つのみ max-nodes-total Cluster AutoscalerがScaleUpする総Node数(このNode数を超えるサイズにはScaleUpされない) デフォルト: 0(無制限) cores-total クラスターの最⼩:最⼤CPU数(この範囲を超えるサイズにはScaleUp/Downを⾏わない) デフォルト: 0: 320000 (単位: コア数 format <最⼩値>:<最⼤値>) memory-total クラスターの最⼩:最⼤メモリ数(この範囲を超えるサイズにはScaleUp/Downを⾏わない) デフォルト: 0: 6400000(単位: GiB format <最⼩値>:<最⼤値>)
  28. PredicateChecker ▶ PredicateCheckerはScaleUp/Downを⾏うための以下のチェックを⾏うためのインターフェイス + 対象のPodを対象のNodeにスケジューリング可能か? + 対象のPodをスケジューリング可能なNodeはどれか? ▶ 実装はScheduling FrameworkのPreFilter/Filterによってチェックを⾏う

    SchedulerBasedPredicateChecker1つのみ + Scheduling Frameworkに設定はデフォルトでのみ使⽤可 + 設定変更などしたい場合は独⾃実装が必要 ▶ ScaleUpでの主な利⽤箇所: + 対象NodeGroupにスケジューリング可能なPodがあるかの判定に利⽤ + Binpacking AlgorithmによるPodを詰め込めるNodeの検索に利⽤ ▶ ScaleDownでの主な利⽤箇所: + ScaleDown候補のNodeで動かしている再スケジューリングが必要なPodの 再スケジューリング先のNodeを探すために利⽤
  29. Estimator ▶ ScaleUpを⾏う際に対象NodeGroupのNodeを何台増やせばよいかを計算するために利⽤される ▶ First Fit Decreasing bin-packing approximation algorithm(bin-packing

    algorithm)という アルゴリズムを⽤いて計算される ▶ ⼤まかな⼿順は以下 + Podの要求リソース(CPU+Memory)をスコアリングする + スコアを元にサイズの⼤きなもの順にPodをソート + ソートされた順番で以下のようにPodのスケジューリングをテスト + PredicateCheckerを使⽤してPodをスケジューリング可能なNodeを探す + ⾒つかればClusterSnapshot内の対象NodeにPodを追加 + ⾒つからなければClusterSnapshot内に新規Nodeを作成し、PodをそのNodeに追加する + 上記テストを通して新規追加されたNode数を計算する
  30. Expander⼀覧 ▶ random: ランダムにNodeGroupを選択 ▶ most-pods: スケジューリング可能なunschedulablePodの種類が最も多いNodeGroupを選択 ▶ least-waste: 対象NodeGroupにスケジューリング可能なunschedulablePodを全てスケジューリングした

    際に、最もNodeに無駄な空き容量が発⽣しないNodeGroupを選択 ▶ price: 最も費⽤効果が⾼くクラスターの優先Nodeサイズと⼀致するNodeGroupを選択 + 利⽤するにはCloudProvider側でPricingメソッドの実装が必要 ▶ priority: ConfigMapで設定した設定ファイルに基づき最も⾼い優先度NodeGroupを選択 # 設定ファイル例 apiVersion: v1 kind: ConfigMap metadata: name: cluster-autoscaler-priority-expander data: priorities: |- 10: - .*t2\.large.* - .*t3\.large.* 50: - .*m4\.4xlarge.*
  31. ClusterStateStrategy 1 プロパティ名 説明 nodes 型: []*apiv1.Node ⼀⾔で説明するとobtainNodeListsが返すallNodesがデータの実体 allNodesは⼀部加⼯されることはあってもa.AllNodeLister().List()の取得結果ほぼそのまま=すべてのnode⼀覧 unregisteredNodes

    型: map[string]UnregisteredNode csr.cloudProviderNodeInstancesにあってallNodesには無いnode情報 つまり、実際のk8s nodeには存在しないCloudProviderから取得できるinstance情報 nodeInfosForGroups 型: map[string]*schedulerframework.NodeInfo core_utils.GetNodeInfosForGroupsが返すnodeInfosForGroupsがデータ実体 CloudProviderのNodeGroup毎に1つのschedulerframework.NodeInfoを⽣成して設定している このNodeInfoはsanitizeNodeInfo/sanitizeTemplateNodeによって様々なtaintを除去している perNodeGroupReadiness 型:map[string]Readiness 各ステータス別のnode数をカウントしたReadinessをNodeGroup単位で保持している Readinessのデータ: Deleted: "ToBeDeletedByClusterAutoscaler" taintのあるnode数 NotStarted: readyではないけどnode.CreationTimestamp+15分を過ぎたnode数 Registered: allNodesに含まれているnode数 LongUnregistered: unregisteredNodesに含まれるnodeの内、Cluster AutoscalerにUnregisteredNodeだと判断されてから15分を超 過したnode数(オプションのmax-node-provision-timeで何分以上をLongUnregisteredに含めるかを設定できる) Unregistered: unregisteredNodesに含まれるnodeの内LongUnregisteredではないnode数 totalReadiness 型: Readiness perNodeGroupReadinessの(NodeGroup別でない)総合計版
  32. ClusterStateStrategy 2 プロパティ名 説明 cloudProviderNodeInstances 型: map[string][]cloudprovider.Instance (map[<NodeFroup ID>])[]cloudprovider.Instance) NodeGroup単位でcache.fetchCloudProviderNodeInstancesForNodeGroupがキャッシュしたInstance⼀覧を格納したデータ

    cloudProviderNodeInstances 型: map[string][]cloudprovider.Instance 前回のループ時のcloudProviderNodeInstancesを保存してる acceptableRanges 型: map[string]AcceptableRange NodeGroupのTargetSizeの直近での変動数の最⼤/最⼩値を管理するデータ AcceptableRangeのデータ: MinNodes: 現在~直近で実際に動くであろうnodeの最⼩数 - scaleup予定のノード数は実際にはまだ稼働していないかもしれない MaxNodes: 現在~直近で実際に動くであろうnodeの最⼤数 - scaledown予定のノード数はまだ稼働している可能性がある CurrentTarget: NodeGroup.TargetSize()の値そのまんま incorrectNodeGroupSizes 型: map[string]IncorrectNodeGroupSize NodeGroup単位で、readiness.Registered(k8s nodeに登録されているnode数)がacceptableRange.MaxNodesと acceptableRange.MinNodesの間に収まってないものがあれば、それらをincorrectNodeGroupSizesとして保存する 想定よりも多くなったnodeを削除するために利⽤される
  33. ClusterStateStrategy 3 プロパティ名 説明 scaleUpRequests 型: map[string]*ScaleUpRequest(map[<NodeGroup ID>]*ScaleUpRequest) 現在ScaleUpを実⾏中のNodeGroupと追加したnode数(Increase)を管理するデータ ScaleUpRequestのデータ:

    NodeGroup: NodeGroupオブジェクト Increase: 追加node数 Time: 追加開始時刻 ExpectedAddTime: node追加完了期限 scaleUpFailures 型: map[string][]ScaleUpFailure(map[<NodeGroup ID>][]ScaleUpFailure) ScaleUpに失敗したNodeGroupのデータを保存している ScaleUpFailureのデータ: NodeGroup: NodeGroupオブジェクト Reason: 失敗理由 Time: 失敗確認時刻 scaleDownRequests 型: []*ScaleDownRequest 現在のScaleDownの実⾏情報を管理するデータ RegisterScaleDownのデータ: NodeGroup: NodeGroupオブジェクト NodeName: Node名 Time: 削除開始時刻 ExpectedAddTime: node削除完了期限 backoff ScaleUpのbackoff管理オブジェクト 要はpod起動のbackoffみたいに1度scaleupに失敗したら⼀定以上の間隔を開けてリトライして、更に失敗したら更に実⾏間隔を空け て、という制御を⾏うためのプロパティ
  34. ScaleDown props プロパティ名 説明 unneededNodes 型: map[string]time.Time (map[<nodename><初めて追加された時刻>]) ScaleDown候補のNode⼀覧 simulator.FindNodesToRemoveによってnodesToRemove(削除可能node)と判断されたnode⼀覧と各nodeが初めて

    unneededNodesに追加された時刻が⼊る unneededNodesList 型: []*apiv1.Node simulator.FindNodesToRemoveによってnodesToRemove(削除可能node)と判断されたnode⼀覧が設定される unremovableNodes 型: map[string]time.Time (map[<nodename>]<ttl>) 何らかの理由で削除不可だと判断されたnode⼀覧とttlを管理している ttlを過ぎるまでは削除されず、checkNodeUtilizationでsd.unremovableNodesにあるnodeは削除不能状態にあるノードだというエ ラーが返る 要するにttlを設けることで短期間で連続で対象nodeがunremovableであるかどうかを毎回チェックするのを避けるための仕組みだ と考えられる unremovableNodeReasons map[string]*simulator.UnremovableNode 各種チェックによって、何らかの理由で削除不可だと判断されたnodeの⼀覧と各nodeの削除不可だと判断された理由などを管理し ている podLocationHints 型: map[string]string (map[<pod名>]<findPlaceForによって選ばれた次のスケジューリング候補node名>) FindNodesToRemove/findPlaceForによって対象nodeを削除した際に、そのnode内部で動いていた再スケジューリングが必要な podが動作する次のnode候補はどれか?というがを詰め込まれている nodeUtilizationMap 型: map[string]simulator.UtilizationInfo (map[nodename]UtilizationInfo) node毎にcheckNodeUtilizationで計算したUtilizationInfoが⼊っている
  35. ざっくりフロー ~ 初期化処理編 1 1. 各種データの初期化 2. Node⼀覧(all/ready)の取得 3. Nodeが空なら処理中断する

    4. CloudProviderをRefresh 5. 優先度が設定されていない or ⼀定値以下のスケジュール済Pod⼀覧を詰め込んだ ClusterSnapshotを⽣成(スケジューリングなどのシュミレートに使⽤) 6. ClusterStateRegistryの各種データアップデート 7. CloudProvider側のInstance情報には存在して、k8s Nodeには存在しない Node(unregisteredNodes)が登録から⼀定時間経過していれば、そのNodeを 削除して処理中断する + ⻑期間unregisteredNodesである=Nodeのprovisioningに失敗したとみなされたので削除される
  36. ざっくりフロー ~ 初期化処理編 2 8. ClusterStateRegistryにUnreadyであると判断されたNode数が⼀定数を超えた場合はCluster がUnhealthyだと判断された処理中断する 9. Node作成中にCloudProviderからエラーだと判断されたInstanceがあった場合、それらの Instanceを削除

    10. Cloud Providerの各NodeGroupのInstance数がNodeGroupの想定サイズ内に収まっていない 場合、Instance数をNodeGroupの想定サイズに収まるように台数削減を⾏い、処理中断する 11. unschedulableなPod⼀覧の中で、preemptionによってNodeから追い出され、次の起動候補 Node(nominatedNode)がされているPodをClusterSnapshot内のnominatedNodeに追加 12. これから起動予定のNode(UpcomingNode)と、そのNode内で起動が決まってる Pod(Mirror PodやDaemonset Podなど)をClusterSnapshotに追加
  37. ざっくりフロー ~ ScaleUp実施判定編 1. unschedulableなPodが無ければScaleUpをスキップ 2. readyなNode数が”--max-nodes-total”オプションで設定した数を超過していた場合は ScaleUpをスキップ + ”—max-nodes-total”はNodeの最⼤数なので、これを超過していた場合はScaleUpしない

    3. unschedulableなPodがすべて2秒以内に作成されたものであったならScaleUpをスキップ + 恐らくまだSchedulerによってNodeを割り当てられていなかったりするためにスキップして ると思われる 4. 上記チェックをパスした場合はScaleUp処理を開始する
  38. ざっくりフロー ~ ScaleUp編 1 1. NodeGroup⼀覧に対して、以下のチェックを⾏いScaleUp候補となるNodeGroupの フィルタリングを⾏う a. NodeGroupがScaleUp可能な状態であるか? b.

    現在のNodeGroupのInstance数がNodeGroupの最⼤値に達しているか? c. 1台以上NodeをScaleUpすることで、NodeGroupごとに消費可能なリソース量(GPU/CPU/ Memory)の最⼤値を超過するか? + 消費可能なリソース量というのは”—memory-total”などのオプションで設定される
  39. ざっくりフロー ~ ScaleUp編 2 2. フィルタを通過したNodeGroupが作成予定のNodeに対して以下のシュミレートを⾏う a. Scheduling FrameworkのPreFilter/Filterを利⽤して、作成予定Nodeにスケジューリング可 能なunschedulablePod⼀覧のシュミレート

    b. First Fit Decreasing bin-packing approximation algorithmを⽤いたPodのスケジューリング に必要なNode数のシュミレート 3. シュミレートした結果、1つ以上のPodがスケジューリング可能であれば、ScaleUp対象の NodeGroup候補に加える
  40. ざっくりフロー ~ ScaleUp編 3 4. “--expander”オプションにより設定されたアルゴリズムにより、NodeGroup候補より1つの NodeGroupを選定 + 選択可能なexpanderはrandom/most-pods/least-waste/price/priorityの5つ 5.

    選ばれたNodeGroupでシュミレートした追加Node数を追加した結果、”--max-nodes-total” オプションで設定した数を超過する場合にはSaleUp処理を中断する 6. “--balance-similar-node-groups”オプションをtrueに設定していた場合、以下の条件を満たす NodeGroupが類似NodeGroupとしてScaleUp対象のNodeGroupに追加される a. 選定されたNodeGroupとリソース容量が類似してる b. (⼀部を除いた)ラベル情報が⼀致してる c. 選定されたNodeGroupにスケジューリング可能なPodをすべてスケジューリングできる d. NodeGroupがScaleUp可能な状態である
  41. ざっくりフロー ~ ScaleDown準備編 1 1. ScaleDown機能が有効であれば、ScaleDown処理を開始する 2. 全てのNodeから以下の条件を満たすScaleDown候補Nodeリストを作成する a. NodeGroupに所属しているNodeであり

    b. 対象NodeGroupの現在のNode数がNodeGroupの最⼩値に達していないこと 3. ScaleDown候補Nodeリストを更に以下の条件でフィルタリングする a. ”ToBeDeletedTaint”Taintを持っていたら除外 b. “cluster-autoscaler.kubernetes.io/scale-down-disabled”=true annotationを持っていたら 除外 c. Nodeリソース(GPU or CPU or Mem)の中で最も使⽤率の⾼いリソースの使⽤率が⼀定値を超 えていたら除外
  42. ざっくりフロー ~ ScaleDown準備編 2 4. フィルタリングしたScaleDown候補から以下の条件を満たすemptyNode⼀覧を⽣成 + emptyNodeとは(おおまかにいうと)以下の条件を満たすpodが1つも無いNodeのこと a. Mirro

    Podではない b. DaemonSet Podではない c. ReplicaSetやJobなどに紐付いていない d. 削除中のPodではない e. Terminal State Podではない f. HostPathまたはEmptyDirのボリューム(=LocalVolume)を使⽤していない g. Podに紐づくPDBがある場合、紐づくPDBのpdb.Status.DisruptionsAllowedが1以上であること h. etc (複雑過ぎて書ききれない) 5. ScaleDown候補NodeをemptyNodeと⾮emptyNodeに分割
  43. ざっくりフロー ~ ScaleDown準備編 3 6. ⾮emptyNodeを更に、前回のScaleDown処理で既にunneededNodesに選ばれたものかどうかで 2つに分割 7. 既にunneededNodesに選ばれていた⾮emptyNodeを対象に以下の条件で更にNodeを絞り込む a.

    対象Nodeの削除をブロッキングするPodは無いか? b. 対象Nodeで現在動作しているPodの中で再スケジューリングが必要なPodが全て他のNodeに 再スケジューリングできるか? + 再スケジューリングできるかはScheduling Framework PreFilter/Filterによって検証する 8. まだunneededNodesに選ばれてない⾮emptyNodeの⼀部を対象に上記と同様のNodeの絞り込み をする
  44. ざっくりフロー ~ ScaleDown編 1 1. unneededNodesを更に以下の条件で絞り込む a. ready statusでかつunneededNodesに追加されてから⼀定時間が経過したNode b.

    unready statusでかつunneededNodesに追加されてから⼀定時間が経過したNode c. Nodeが所属するNodeGroupの現在の数-対象NodeGroupで現在削除中のNode数が NodeGroupの最⼩値を下回っていないNode 2. 絞り込んだunneededNodesからemptyNodesを⽣成 3. emptyNodesが存在すれば全てのemptyNodesの削除を実施 + 削除処理はNode単位でgo routineを利⽤して⾮同期に⾏う + 削除前にNodeに”ToBeDeletedTaint”Taintを付与する 4. 削除開始したemptyNodesが1つ以上あった場合はScaleDown処理を終了する