Skip to content

Commit dc99ab3

Browse files
committed
add least-nodes expander to cluster-autoscaler
1 parent 3fd892a commit dc99ab3

File tree

6 files changed

+110
-0
lines changed

6 files changed

+110
-0
lines changed

cluster-autoscaler/FAQ.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -737,6 +737,8 @@ smaller nodes at once.
737737
* `least-waste` - selects the node group that will have the least idle CPU (if tied, unused memory)
738738
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.
739739

740+
* `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.
741+
740742
* `price` - select the node group that will cost the least and, at the same time, whose machines
741743
would match the cluster size. This expander is described in more details
742744
[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.)

cluster-autoscaler/expander/expander.go

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,8 @@ var (
3131
MostPodsExpanderName = "most-pods"
3232
// LeastWasteExpanderName selects a node group that leaves the least fraction of CPU and Memory
3333
LeastWasteExpanderName = "least-waste"
34+
// LeastNodesExpanderName selects a node group that uses the least number of nodes
35+
LeastNodesExpanderName = "least-nodes"
3436
// PriceBasedExpanderName selects a node group that is the most cost-effective and consistent with
3537
// the preferred node size for the cluster
3638
PriceBasedExpanderName = "price"

cluster-autoscaler/expander/factory/expander_factory.go

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@ import (
2121
"k8s.io/autoscaler/cluster-autoscaler/context"
2222
"k8s.io/autoscaler/cluster-autoscaler/expander"
2323
"k8s.io/autoscaler/cluster-autoscaler/expander/grpcplugin"
24+
"k8s.io/autoscaler/cluster-autoscaler/expander/leastnodes"
2425
"k8s.io/autoscaler/cluster-autoscaler/expander/mostpods"
2526
"k8s.io/autoscaler/cluster-autoscaler/expander/price"
2627
"k8s.io/autoscaler/cluster-autoscaler/expander/priority"
@@ -82,6 +83,7 @@ func (f *Factory) RegisterDefaultExpanders(cloudProvider cloudprovider.CloudProv
8283
f.RegisterFilter(expander.RandomExpanderName, random.NewFilter)
8384
f.RegisterFilter(expander.MostPodsExpanderName, mostpods.NewFilter)
8485
f.RegisterFilter(expander.LeastWasteExpanderName, waste.NewFilter)
86+
f.RegisterFilter(expander.LeastNodesExpanderName, leastnodes.NewFilter)
8587
f.RegisterFilter(expander.PriceBasedExpanderName, func() expander.Filter {
8688
if _, err := cloudProvider.Pricing(); err != nil {
8789
klog.Fatalf("Couldn't access cloud provider pricing for %s expander: %v", expander.PriceBasedExpanderName, err)
Lines changed: 61 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,61 @@
1+
/*
2+
Copyright 2016 The Kubernetes Authors.
3+
4+
Licensed under the Apache License, Version 2.0 (the "License");
5+
you may not use this file except in compliance with the License.
6+
You may obtain a copy of the License at
7+
8+
http://www.apache.org/licenses/LICENSE-2.0
9+
10+
Unless required by applicable law or agreed to in writing, software
11+
distributed under the License is distributed on an "AS IS" BASIS,
12+
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
See the License for the specific language governing permissions and
14+
limitations under the License.
15+
*/
16+
17+
package leastnodes
18+
19+
import (
20+
"math"
21+
22+
"k8s.io/autoscaler/cluster-autoscaler/expander"
23+
schedulerframework "k8s.io/kubernetes/pkg/scheduler/framework"
24+
)
25+
26+
type leastnodes struct {
27+
}
28+
29+
// NewFilter returns a scale up filter that picks the node group that uses the least number of nodes
30+
func NewFilter() expander.Filter {
31+
return &leastnodes{}
32+
}
33+
34+
// BestOptions selects the expansion option that uses the least number of nodes
35+
func (m *leastnodes) BestOptions(expansionOptions []expander.Option, nodeInfo map[string]*schedulerframework.NodeInfo) []expander.Option {
36+
leastNodes := math.MaxInt
37+
var leastOptions []expander.Option
38+
39+
for _, option := range expansionOptions {
40+
// Don't think this is possible, but just in case
41+
if option.NodeCount == 0 {
42+
continue
43+
}
44+
45+
if option.NodeCount == leastNodes {
46+
leastOptions = append(leastOptions, option)
47+
continue
48+
}
49+
50+
if option.NodeCount < leastNodes {
51+
leastNodes = option.NodeCount
52+
leastOptions = []expander.Option{option}
53+
}
54+
}
55+
56+
if len(leastOptions) == 0 {
57+
return nil
58+
}
59+
60+
return leastOptions
61+
}
Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,42 @@
1+
/*
2+
Copyright 2016 The Kubernetes Authors.
3+
4+
Licensed under the Apache License, Version 2.0 (the "License");
5+
you may not use this file except in compliance with the License.
6+
You may obtain a copy of the License at
7+
8+
http://www.apache.org/licenses/LICENSE-2.0
9+
10+
Unless required by applicable law or agreed to in writing, software
11+
distributed under the License is distributed on an "AS IS" BASIS,
12+
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
See the License for the specific language governing permissions and
14+
limitations under the License.
15+
*/
16+
17+
package leastnodes
18+
19+
import (
20+
"testing"
21+
22+
"github.com/stretchr/testify/assert"
23+
24+
"k8s.io/autoscaler/cluster-autoscaler/expander"
25+
)
26+
27+
func TestLeastNodes(t *testing.T) {
28+
e := NewFilter()
29+
30+
eo0 := expander.Option{Debug: "EO0", NodeCount: 2}
31+
ret := e.BestOptions([]expander.Option{eo0}, nil)
32+
assert.Equal(t, ret, []expander.Option{eo0})
33+
34+
eo1 := expander.Option{Debug: "EO1", NodeCount: 1}
35+
ret = e.BestOptions([]expander.Option{eo0, eo1}, nil)
36+
assert.Equal(t, ret, []expander.Option{eo1})
37+
38+
eo1b := expander.Option{Debug: "EO1b", NodeCount: 1}
39+
ret = e.BestOptions([]expander.Option{eo0, eo1, eo1b}, nil)
40+
assert.NotEqual(t, ret, []expander.Option{eo1})
41+
assert.ObjectsAreEqual(ret, []expander.Option{eo1, eo1b})
42+
}

cluster-autoscaler/expander/mostpods/mostpods.go

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,7 @@ func (m *mostpods) BestOptions(expansionOptions []expander.Option, nodeInfo map[
3737
for _, option := range expansionOptions {
3838
if len(option.Pods) == maxPods {
3939
maxOptions = append(maxOptions, option)
40+
continue
4041
}
4142

4243
if len(option.Pods) > maxPods {

0 commit comments

Comments
 (0)