Merge pull request #103 from MaciekPytel/node_group_sets3
Function to balance scale-up between node groups
This commit is contained in:
commit
c5e9f479de
|
|
@ -0,0 +1,154 @@
|
|||
/*
|
||||
Copyright 2017 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 nodegroupset
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"sort"
|
||||
|
||||
"k8s.io/autoscaler/cluster-autoscaler/cloudprovider"
|
||||
"k8s.io/autoscaler/cluster-autoscaler/utils/errors"
|
||||
|
||||
"github.com/golang/glog"
|
||||
)
|
||||
|
||||
// ScaleUpInfo contains information about planned scale-up of a single NodeGroup
|
||||
type ScaleUpInfo struct {
|
||||
// Group is the group to be scaled-up
|
||||
Group cloudprovider.NodeGroup
|
||||
// CurrentSize is the current size of the Group
|
||||
CurrentSize int
|
||||
// NewSize is the size the Group will be scaled-up to
|
||||
NewSize int
|
||||
// MaxSize is the maximum allowed size of the Group
|
||||
MaxSize int
|
||||
}
|
||||
|
||||
// String is used for printing ScaleUpInfo for logging, etc
|
||||
func (s ScaleUpInfo) String() string {
|
||||
return fmt.Sprintf("{%v %v->%v (max: %v)}", s.Group.Id(), s.CurrentSize, s.NewSize, s.MaxSize)
|
||||
}
|
||||
|
||||
// BalanceScaleUpBetweenGroups distributes a given number of nodes between
|
||||
// given set of NodeGroups. The nodes are added to smallest group first, trying
|
||||
// to make the group sizes as evenly balanced as possible.
|
||||
//
|
||||
// Returns ScaleUpInfos for groups that need to be resized.
|
||||
//
|
||||
// MaxSize of each group will be respected. If newNodes > total free capacity
|
||||
// of all NodeGroups it will be capped to total capacity. In particular if all
|
||||
// group already have MaxSize, empty list will be returned.
|
||||
func BalanceScaleUpBetweenGroups(groups []cloudprovider.NodeGroup, newNodes int) ([]ScaleUpInfo, *errors.AutoscalerError) {
|
||||
if len(groups) == 0 {
|
||||
return []ScaleUpInfo{}, errors.NewAutoscalerError(
|
||||
errors.InternalError, "Can't balance scale up between 0 groups")
|
||||
}
|
||||
|
||||
// get all data from cloudprovider, build data structure
|
||||
scaleUpInfos := make([]ScaleUpInfo, 0)
|
||||
totalCapacity := 0
|
||||
for _, ng := range groups {
|
||||
currentSize, err := ng.TargetSize()
|
||||
if err != nil {
|
||||
return []ScaleUpInfo{}, errors.NewAutoscalerError(
|
||||
errors.CloudProviderError,
|
||||
"failed to get node group size: %v", err)
|
||||
}
|
||||
maxSize := ng.MaxSize()
|
||||
if currentSize == maxSize {
|
||||
// group already maxed, ignore it
|
||||
continue
|
||||
}
|
||||
info := ScaleUpInfo{
|
||||
Group: ng,
|
||||
CurrentSize: currentSize,
|
||||
NewSize: currentSize,
|
||||
MaxSize: maxSize}
|
||||
scaleUpInfos = append(scaleUpInfos, info)
|
||||
totalCapacity += maxSize - currentSize
|
||||
}
|
||||
if totalCapacity < newNodes {
|
||||
glog.Infof("Requested scale-up (%v) exceeds node group set capacity, capping to %v", newNodes, totalCapacity)
|
||||
newNodes = totalCapacity
|
||||
}
|
||||
|
||||
// The actual balancing algorithm.
|
||||
// Sort the node groups by current size and just loop over nodes adding
|
||||
// to smallest group. If a group hits max size remove it from the list
|
||||
// (by moving it to start of the list and increasing startIndex).
|
||||
//
|
||||
// In each iteration we either allocate one node, or 'remove' a maxed out
|
||||
// node group, so this will terminate in O(#nodes + #node groups) steps.
|
||||
// We already know that newNodes <= total capacity, so we don't have to
|
||||
// worry about accidentally removing all node groups while we still
|
||||
// have nodes to allocate.
|
||||
//
|
||||
// Loop invariants:
|
||||
// 1. i < startIndex -> scaleUpInfos[i].CurrentSize == scaleUpInfos[i].MaxSize
|
||||
// 2. i >= startIndex -> scaleUpInfos[i].CurrentSize < scaleUpInfos[i].MaxSize
|
||||
// 3. startIndex <= currentIndex < len(scaleUpInfos)
|
||||
// 4. currentIndex <= i < j -> scaleUpInfos[i].CurrentSize <= scaleUpInfos[j].CurrentSize
|
||||
// 5. startIndex <= i < j < currentIndex -> scaleUpInfos[i].CurrentSize == scaleUpInfos[j].CurrentSize
|
||||
// 6. startIndex <= i < currentIndex <= j -> scaleUpInfos[i].CurrentSize <= scaleUpInfos[j].CurrentSize + 1
|
||||
sort.Slice(scaleUpInfos, func(i, j int) bool {
|
||||
return scaleUpInfos[i].CurrentSize < scaleUpInfos[j].CurrentSize
|
||||
})
|
||||
startIndex := 0
|
||||
currentIndex := 0
|
||||
for newNodes > 0 {
|
||||
currentInfo := &scaleUpInfos[currentIndex]
|
||||
|
||||
if currentInfo.NewSize < currentInfo.MaxSize {
|
||||
// Add a node to group on currentIndex
|
||||
currentInfo.NewSize++
|
||||
newNodes--
|
||||
} else {
|
||||
// Group on currentIndex is full. Remove it from the array.
|
||||
// Removing is done by swapping the group with the first
|
||||
// group still in array and moving the start of the array.
|
||||
// Every group between startIndex and currentIndex has the
|
||||
// same size, so we can swap without breaking ordering.
|
||||
scaleUpInfos[startIndex], scaleUpInfos[currentIndex] = scaleUpInfos[currentIndex], scaleUpInfos[startIndex]
|
||||
startIndex++
|
||||
}
|
||||
|
||||
// Update currentIndex.
|
||||
// If we removed a group in this loop currentIndex may be equal to startIndex-1,
|
||||
// in which case both branches of below if will make currentIndex == startIndex.
|
||||
if currentIndex < len(scaleUpInfos)-1 && currentInfo.NewSize > scaleUpInfos[currentIndex+1].NewSize {
|
||||
// Next group has exactly one less node, than current one.
|
||||
// We will increase it in next iteration.
|
||||
currentIndex++
|
||||
} else {
|
||||
// We reached end of array, or a group larger than the current one.
|
||||
// All groups from startIndex to currentIndex have the same size.
|
||||
// So we're moving to the beginning of array to loop over all of
|
||||
// them once again.
|
||||
currentIndex = startIndex
|
||||
}
|
||||
}
|
||||
|
||||
// Filter out groups that haven't changed size
|
||||
result := make([]ScaleUpInfo, 0)
|
||||
for _, info := range scaleUpInfos {
|
||||
if info.NewSize != info.CurrentSize {
|
||||
result = append(result, info)
|
||||
}
|
||||
}
|
||||
|
||||
return result, nil
|
||||
}
|
||||
|
|
@ -0,0 +1,160 @@
|
|||
/*
|
||||
Copyright 2017 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 nodegroupset
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"k8s.io/autoscaler/cluster-autoscaler/cloudprovider"
|
||||
"k8s.io/autoscaler/cluster-autoscaler/cloudprovider/test"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
func TestBalanceSingleGroup(t *testing.T) {
|
||||
provider := testprovider.NewTestCloudProvider(nil, nil)
|
||||
provider.AddNodeGroup("ng1", 1, 10, 1)
|
||||
|
||||
// just one node
|
||||
scaleUpInfo, err := BalanceScaleUpBetweenGroups(provider.NodeGroups(), 1)
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, 1, len(scaleUpInfo))
|
||||
assert.Equal(t, 2, scaleUpInfo[0].NewSize)
|
||||
|
||||
// multiple nodes
|
||||
scaleUpInfo, err = BalanceScaleUpBetweenGroups(provider.NodeGroups(), 4)
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, 1, len(scaleUpInfo))
|
||||
assert.Equal(t, 5, scaleUpInfo[0].NewSize)
|
||||
}
|
||||
|
||||
func TestBalanceUnderMaxSize(t *testing.T) {
|
||||
provider := testprovider.NewTestCloudProvider(nil, nil)
|
||||
provider.AddNodeGroup("ng1", 1, 10, 1)
|
||||
provider.AddNodeGroup("ng2", 1, 10, 3)
|
||||
provider.AddNodeGroup("ng3", 1, 10, 5)
|
||||
provider.AddNodeGroup("ng4", 1, 10, 5)
|
||||
|
||||
// add a single node
|
||||
scaleUpInfo, err := BalanceScaleUpBetweenGroups(provider.NodeGroups(), 1)
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, 1, len(scaleUpInfo))
|
||||
assert.Equal(t, 2, scaleUpInfo[0].NewSize)
|
||||
|
||||
// add multiple nodes to single group
|
||||
scaleUpInfo, err = BalanceScaleUpBetweenGroups(provider.NodeGroups(), 2)
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, 1, len(scaleUpInfo))
|
||||
assert.Equal(t, 3, scaleUpInfo[0].NewSize)
|
||||
|
||||
// add nodes to groups of different sizes, divisible
|
||||
scaleUpInfo, err = BalanceScaleUpBetweenGroups(provider.NodeGroups(), 4)
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, 2, len(scaleUpInfo))
|
||||
assert.Equal(t, 4, scaleUpInfo[0].NewSize)
|
||||
assert.Equal(t, 4, scaleUpInfo[1].NewSize)
|
||||
assert.True(t, scaleUpInfo[0].Group.Id() == "ng1" || scaleUpInfo[1].Group.Id() == "ng1")
|
||||
assert.True(t, scaleUpInfo[0].Group.Id() == "ng2" || scaleUpInfo[1].Group.Id() == "ng2")
|
||||
|
||||
// add nodes to groups of different sizes, non-divisible
|
||||
// we expect new sizes to be 4 and 5, doesn't matter which group gets how many
|
||||
scaleUpInfo, err = BalanceScaleUpBetweenGroups(provider.NodeGroups(), 5)
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, 2, len(scaleUpInfo))
|
||||
assert.Equal(t, 9, scaleUpInfo[0].NewSize+scaleUpInfo[1].NewSize)
|
||||
assert.True(t, scaleUpInfo[0].NewSize == 4 || scaleUpInfo[0].NewSize == 5)
|
||||
assert.True(t, scaleUpInfo[0].Group.Id() == "ng1" || scaleUpInfo[1].Group.Id() == "ng1")
|
||||
assert.True(t, scaleUpInfo[0].Group.Id() == "ng2" || scaleUpInfo[1].Group.Id() == "ng2")
|
||||
|
||||
// add nodes to all groups, divisible
|
||||
scaleUpInfo, err = BalanceScaleUpBetweenGroups(provider.NodeGroups(), 10)
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, 4, len(scaleUpInfo))
|
||||
for _, info := range scaleUpInfo {
|
||||
assert.Equal(t, 6, info.NewSize)
|
||||
}
|
||||
}
|
||||
|
||||
func TestBalanceHittingMaxSize(t *testing.T) {
|
||||
provider := testprovider.NewTestCloudProvider(nil, nil)
|
||||
provider.AddNodeGroup("ng1", 1, 1, 1)
|
||||
provider.AddNodeGroup("ng2", 1, 3, 1)
|
||||
provider.AddNodeGroup("ng3", 1, 10, 3)
|
||||
provider.AddNodeGroup("ng4", 1, 7, 5)
|
||||
groupsMap := make(map[string]cloudprovider.NodeGroup)
|
||||
for _, group := range provider.NodeGroups() {
|
||||
groupsMap[group.Id()] = group
|
||||
}
|
||||
|
||||
getGroups := func(names ...string) []cloudprovider.NodeGroup {
|
||||
result := make([]cloudprovider.NodeGroup, 0)
|
||||
for _, n := range names {
|
||||
result = append(result, groupsMap[n])
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
toMap := func(suiList []ScaleUpInfo) map[string]ScaleUpInfo {
|
||||
result := make(map[string]ScaleUpInfo, 0)
|
||||
for _, sui := range suiList {
|
||||
result[sui.Group.Id()] = sui
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
// Just one maxed out group
|
||||
scaleUpInfo, err := BalanceScaleUpBetweenGroups(getGroups("ng1"), 1)
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, 0, len(scaleUpInfo))
|
||||
|
||||
// Smallest group already maxed out, add one node
|
||||
scaleUpInfo, err = BalanceScaleUpBetweenGroups(getGroups("ng1", "ng2"), 1)
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, 1, len(scaleUpInfo))
|
||||
assert.Equal(t, "ng2", scaleUpInfo[0].Group.Id())
|
||||
assert.Equal(t, 2, scaleUpInfo[0].NewSize)
|
||||
|
||||
// Smallest group already maxed out, too many nodes (should cap to max capacity)
|
||||
scaleUpInfo, err = BalanceScaleUpBetweenGroups(getGroups("ng1", "ng2"), 5)
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, 1, len(scaleUpInfo))
|
||||
assert.Equal(t, "ng2", scaleUpInfo[0].Group.Id())
|
||||
assert.Equal(t, 3, scaleUpInfo[0].NewSize)
|
||||
|
||||
// First group maxes out before proceeding to next one
|
||||
scaleUpInfo, err = BalanceScaleUpBetweenGroups(getGroups("ng2", "ng3"), 4)
|
||||
assert.Equal(t, 2, len(scaleUpInfo))
|
||||
scaleUpMap := toMap(scaleUpInfo)
|
||||
assert.Equal(t, 3, scaleUpMap["ng2"].NewSize)
|
||||
assert.Equal(t, 5, scaleUpMap["ng3"].NewSize)
|
||||
|
||||
// Last group maxes out before previous one
|
||||
scaleUpInfo, err = BalanceScaleUpBetweenGroups(getGroups("ng2", "ng3", "ng4"), 9)
|
||||
assert.Equal(t, 3, len(scaleUpInfo))
|
||||
scaleUpMap = toMap(scaleUpInfo)
|
||||
assert.Equal(t, 3, scaleUpMap["ng2"].NewSize)
|
||||
assert.Equal(t, 8, scaleUpMap["ng3"].NewSize)
|
||||
assert.Equal(t, 7, scaleUpMap["ng4"].NewSize)
|
||||
|
||||
// Use all capacity, cap to max
|
||||
scaleUpInfo, err = BalanceScaleUpBetweenGroups(getGroups("ng2", "ng3", "ng4"), 900)
|
||||
assert.Equal(t, 3, len(scaleUpInfo))
|
||||
scaleUpMap = toMap(scaleUpInfo)
|
||||
assert.Equal(t, 3, scaleUpMap["ng2"].NewSize)
|
||||
assert.Equal(t, 10, scaleUpMap["ng3"].NewSize)
|
||||
assert.Equal(t, 7, scaleUpMap["ng4"].NewSize)
|
||||
}
|
||||
Loading…
Reference in New Issue