From 0b7bb3ddfc469033d142d36e25409089ed7b6fdf Mon Sep 17 00:00:00 2001 From: Kubernetes Prow Robot <20407524+k8s-ci-robot@users.noreply.github.com> Date: Tue, 7 May 2024 04:03:05 -0700 Subject: [PATCH] Merge pull request #6792 from com6056/jrodgers-expander-least-nodes add least-nodes expander to cluster-autoscaler --- cluster-autoscaler/FAQ.md | 2 + cluster-autoscaler/expander/expander.go | 2 + .../expander/factory/expander_factory.go | 2 + .../expander/leastnodes/leastnodes.go | 61 ++++++++++ .../expander/leastnodes/leastnodes_test.go | 113 ++++++++++++++++++ .../expander/mostpods/mostpods.go | 1 + 6 files changed, 181 insertions(+) create mode 100644 cluster-autoscaler/expander/leastnodes/leastnodes.go create mode 100644 cluster-autoscaler/expander/leastnodes/leastnodes_test.go diff --git a/cluster-autoscaler/FAQ.md b/cluster-autoscaler/FAQ.md index 38c9221b5b8e..c59dd48e1dcc 100644 --- a/cluster-autoscaler/FAQ.md +++ b/cluster-autoscaler/FAQ.md @@ -737,6 +737,8 @@ smaller nodes at once. * `least-waste` - selects the node group that will have the least idle CPU (if tied, unused memory) after scale-up. This is useful when you have different classes of nodes, for example, high CPU or high memory nodes, and only want to expand those when there are pending pods that need a lot of those resources. +* `least-nodes` - selects the node group that will use the least number of nodes after scale-up. This is useful when you want to minimize the number of nodes in the cluster and instead opt for fewer larger nodes. Useful when chained with the `most-pods` expander before it to ensure that the node group selected can fit the most pods on the fewest nodes. + * `price` - select the node group that will cost the least and, at the same time, whose machines would match the cluster size. This expander is described in more details [HERE](https://github.com/kubernetes/autoscaler/blob/master/cluster-autoscaler/proposals/pricing.md). Currently it works only for GCE, GKE and Equinix Metal (patches welcome.) diff --git a/cluster-autoscaler/expander/expander.go b/cluster-autoscaler/expander/expander.go index 9aa2eccb16b1..01700a3b943c 100644 --- a/cluster-autoscaler/expander/expander.go +++ b/cluster-autoscaler/expander/expander.go @@ -31,6 +31,8 @@ var ( MostPodsExpanderName = "most-pods" // LeastWasteExpanderName selects a node group that leaves the least fraction of CPU and Memory LeastWasteExpanderName = "least-waste" + // LeastNodesExpanderName selects a node group that uses the least number of nodes + LeastNodesExpanderName = "least-nodes" // PriceBasedExpanderName selects a node group that is the most cost-effective and consistent with // the preferred node size for the cluster PriceBasedExpanderName = "price" diff --git a/cluster-autoscaler/expander/factory/expander_factory.go b/cluster-autoscaler/expander/factory/expander_factory.go index 68d41c15d214..4dedd7fac372 100644 --- a/cluster-autoscaler/expander/factory/expander_factory.go +++ b/cluster-autoscaler/expander/factory/expander_factory.go @@ -21,6 +21,7 @@ import ( "k8s.io/autoscaler/cluster-autoscaler/context" "k8s.io/autoscaler/cluster-autoscaler/expander" "k8s.io/autoscaler/cluster-autoscaler/expander/grpcplugin" + "k8s.io/autoscaler/cluster-autoscaler/expander/leastnodes" "k8s.io/autoscaler/cluster-autoscaler/expander/mostpods" "k8s.io/autoscaler/cluster-autoscaler/expander/price" "k8s.io/autoscaler/cluster-autoscaler/expander/priority" @@ -82,6 +83,7 @@ func (f *Factory) RegisterDefaultExpanders(cloudProvider cloudprovider.CloudProv f.RegisterFilter(expander.RandomExpanderName, random.NewFilter) f.RegisterFilter(expander.MostPodsExpanderName, mostpods.NewFilter) f.RegisterFilter(expander.LeastWasteExpanderName, waste.NewFilter) + f.RegisterFilter(expander.LeastNodesExpanderName, leastnodes.NewFilter) f.RegisterFilter(expander.PriceBasedExpanderName, func() expander.Filter { if _, err := cloudProvider.Pricing(); err != nil { klog.Fatalf("Couldn't access cloud provider pricing for %s expander: %v", expander.PriceBasedExpanderName, err) diff --git a/cluster-autoscaler/expander/leastnodes/leastnodes.go b/cluster-autoscaler/expander/leastnodes/leastnodes.go new file mode 100644 index 000000000000..80c8c782a60a --- /dev/null +++ b/cluster-autoscaler/expander/leastnodes/leastnodes.go @@ -0,0 +1,61 @@ +/* +Copyright 2016 The Kubernetes Authors. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package leastnodes + +import ( + "math" + + "k8s.io/autoscaler/cluster-autoscaler/expander" + schedulerframework "k8s.io/kubernetes/pkg/scheduler/framework" +) + +type leastnodes struct { +} + +// NewFilter returns a scale up filter that picks the node group that uses the least number of nodes +func NewFilter() expander.Filter { + return &leastnodes{} +} + +// BestOptions selects the expansion option that uses the least number of nodes +func (m *leastnodes) BestOptions(expansionOptions []expander.Option, nodeInfo map[string]*schedulerframework.NodeInfo) []expander.Option { + leastNodes := math.MaxInt + var leastOptions []expander.Option + + for _, option := range expansionOptions { + // Don't think this is possible, but just in case + if option.NodeCount == 0 { + continue + } + + if option.NodeCount == leastNodes { + leastOptions = append(leastOptions, option) + continue + } + + if option.NodeCount < leastNodes { + leastNodes = option.NodeCount + leastOptions = []expander.Option{option} + } + } + + if len(leastOptions) == 0 { + return nil + } + + return leastOptions +} diff --git a/cluster-autoscaler/expander/leastnodes/leastnodes_test.go b/cluster-autoscaler/expander/leastnodes/leastnodes_test.go new file mode 100644 index 000000000000..4221befad626 --- /dev/null +++ b/cluster-autoscaler/expander/leastnodes/leastnodes_test.go @@ -0,0 +1,113 @@ +/* +Copyright 2016 The Kubernetes Authors. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package leastnodes + +import ( + "testing" + + "github.com/stretchr/testify/assert" + + "k8s.io/autoscaler/cluster-autoscaler/expander" +) + +func TestLeastNodes(t *testing.T) { + for _, tc := range []struct { + name string + expansionOptions []expander.Option + expectedExpansionOptions []expander.Option + }{ + { + name: "no options", + expansionOptions: nil, + expectedExpansionOptions: nil, + }, + { + name: "no valid options", + expansionOptions: []expander.Option{ + {Debug: "EO0", NodeCount: 0}, + }, + expectedExpansionOptions: nil, + }, + { + name: "1 valid option", + expansionOptions: []expander.Option{ + {Debug: "EO0", NodeCount: 2}, + }, + expectedExpansionOptions: []expander.Option{ + {Debug: "EO0", NodeCount: 2}, + }, + }, + { + name: "2 valid options, not equal", + expansionOptions: []expander.Option{ + {Debug: "EO0", NodeCount: 2}, + {Debug: "EO1", NodeCount: 1}, + }, + expectedExpansionOptions: []expander.Option{ + {Debug: "EO1", NodeCount: 1}, + }, + }, + { + name: "3 valid options, 2 equal", + expansionOptions: []expander.Option{ + {Debug: "EO0", NodeCount: 6}, + {Debug: "EO1", NodeCount: 2}, + {Debug: "EO2", NodeCount: 2}, + }, + expectedExpansionOptions: []expander.Option{ + {Debug: "EO1", NodeCount: 2}, + {Debug: "EO2", NodeCount: 2}, + }, + }, + { + name: "3 valid options, all equal", + expansionOptions: []expander.Option{ + {Debug: "EO0", NodeCount: 8}, + {Debug: "EO1", NodeCount: 8}, + {Debug: "EO2", NodeCount: 8}, + }, + expectedExpansionOptions: []expander.Option{ + {Debug: "EO0", NodeCount: 8}, + {Debug: "EO1", NodeCount: 8}, + {Debug: "EO2", NodeCount: 8}, + }, + }, + { + name: "6 valid options, 1 invalid option, 3 equal", + expansionOptions: []expander.Option{ + {Debug: "EO0", NodeCount: 23}, + {Debug: "EO1", NodeCount: 0}, + {Debug: "EO2", NodeCount: 5}, + {Debug: "EO3", NodeCount: 8}, + {Debug: "EO4", NodeCount: 5}, + {Debug: "EO5", NodeCount: 5}, + {Debug: "EO6", NodeCount: 22}, + }, + expectedExpansionOptions: []expander.Option{ + {Debug: "EO2", NodeCount: 5}, + {Debug: "EO4", NodeCount: 5}, + {Debug: "EO5", NodeCount: 5}, + }, + }, + } { + t.Run(tc.name, func(t *testing.T) { + e := NewFilter() + ret := e.BestOptions(tc.expansionOptions, nil) + assert.Equal(t, ret, tc.expectedExpansionOptions) + }) + } +} diff --git a/cluster-autoscaler/expander/mostpods/mostpods.go b/cluster-autoscaler/expander/mostpods/mostpods.go index 2bc5d8481e4b..77f1662ca716 100644 --- a/cluster-autoscaler/expander/mostpods/mostpods.go +++ b/cluster-autoscaler/expander/mostpods/mostpods.go @@ -37,6 +37,7 @@ func (m *mostpods) BestOptions(expansionOptions []expander.Option, nodeInfo map[ for _, option := range expansionOptions { if len(option.Pods) == maxPods { maxOptions = append(maxOptions, option) + continue } if len(option.Pods) > maxPods {