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