Merge pull request #97 from MaciekPytel/node_group_sets
Function to compare nodeinfos to find similar nodegroups
This commit is contained in:
commit
db583abb62
|
|
@ -0,0 +1,110 @@
|
|||
/*
|
||||
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 (
|
||||
"math"
|
||||
|
||||
"k8s.io/apimachinery/pkg/api/resource"
|
||||
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
||||
apiv1 "k8s.io/kubernetes/pkg/api/v1"
|
||||
"k8s.io/kubernetes/plugin/pkg/scheduler/schedulercache"
|
||||
)
|
||||
|
||||
const (
|
||||
// MaxAllocatableDifferenceRatio describes how Node.Status.Allocatable can differ between
|
||||
// groups in the same NodeGroupSet
|
||||
MaxAllocatableDifferenceRatio = 0.05
|
||||
// MaxFreeDifferenceRatio describes how free resources (allocatable - daemon and system pods)
|
||||
// can differ between groups in the same NodeGroupSet
|
||||
MaxFreeDifferenceRatio = 0.05
|
||||
)
|
||||
|
||||
func compareResourceMapsWithTolerance(resources map[apiv1.ResourceName][]resource.Quantity,
|
||||
maxDifferenceRatio float64) bool {
|
||||
for _, qtyList := range resources {
|
||||
if len(qtyList) != 2 {
|
||||
return false
|
||||
}
|
||||
larger := math.Max(float64(qtyList[0].MilliValue()), float64(qtyList[1].MilliValue()))
|
||||
smaller := math.Min(float64(qtyList[0].MilliValue()), float64(qtyList[1].MilliValue()))
|
||||
if larger-smaller > larger*maxDifferenceRatio {
|
||||
return false
|
||||
}
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
// IsNodeInfoSimilar returns true if two NodeInfos are similar enough to consider
|
||||
// the NodeGroups they come from part of the same NodeGroupSet. The criteria are
|
||||
// somewhat arbitrary, but generally we check if resources provided by both nodes
|
||||
// are similar enough to likely be the same type of machine and if the set of labels
|
||||
// is the same (except for a pre-defined set of labels like hostname or zone).
|
||||
func IsNodeInfoSimilar(n1, n2 *schedulercache.NodeInfo) bool {
|
||||
capacity := make(map[apiv1.ResourceName][]resource.Quantity)
|
||||
allocatable := make(map[apiv1.ResourceName][]resource.Quantity)
|
||||
free := make(map[apiv1.ResourceName][]resource.Quantity)
|
||||
nodes := []*schedulercache.NodeInfo{n1, n2}
|
||||
for _, node := range nodes {
|
||||
for res, quantity := range node.Node().Status.Capacity {
|
||||
capacity[res] = append(capacity[res], quantity)
|
||||
}
|
||||
for res, quantity := range node.Node().Status.Allocatable {
|
||||
allocatable[res] = append(allocatable[res], quantity)
|
||||
}
|
||||
requested := node.RequestedResource()
|
||||
for res, quantity := range (&requested).ResourceList() {
|
||||
freeRes := node.Node().Status.Allocatable[res].DeepCopy()
|
||||
freeRes.Sub(quantity)
|
||||
free[res] = append(free[res], freeRes)
|
||||
}
|
||||
}
|
||||
for _, qtyList := range capacity {
|
||||
if len(qtyList) != 2 || qtyList[0].Cmp(qtyList[1]) != 0 {
|
||||
return false
|
||||
}
|
||||
}
|
||||
// For allocatable and free we allow resource quantities to be within a few % of each other
|
||||
if !compareResourceMapsWithTolerance(allocatable, MaxAllocatableDifferenceRatio) {
|
||||
return false
|
||||
}
|
||||
if !compareResourceMapsWithTolerance(free, MaxFreeDifferenceRatio) {
|
||||
return false
|
||||
}
|
||||
|
||||
labels := make(map[string][]string)
|
||||
for _, node := range nodes {
|
||||
for label, value := range node.Node().ObjectMeta.Labels {
|
||||
if label == metav1.LabelHostname {
|
||||
continue
|
||||
}
|
||||
if label == metav1.LabelZoneFailureDomain {
|
||||
continue
|
||||
}
|
||||
if label == metav1.LabelZoneRegion {
|
||||
continue
|
||||
}
|
||||
labels[label] = append(labels[label], value)
|
||||
}
|
||||
}
|
||||
for _, labelValues := range labels {
|
||||
if len(labelValues) != 2 || labelValues[0] != labelValues[1] {
|
||||
return false
|
||||
}
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
|
@ -0,0 +1,122 @@
|
|||
/*
|
||||
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/apimachinery/pkg/api/resource"
|
||||
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
||||
. "k8s.io/autoscaler/cluster-autoscaler/utils/test"
|
||||
apiv1 "k8s.io/kubernetes/pkg/api/v1"
|
||||
"k8s.io/kubernetes/plugin/pkg/scheduler/schedulercache"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
func checkNodesSimilar(t *testing.T, n1, n2 *apiv1.Node, shouldEqual bool) {
|
||||
checkNodesSimilarWithPods(t, n1, n2, []*apiv1.Pod{}, []*apiv1.Pod{}, shouldEqual)
|
||||
}
|
||||
|
||||
func checkNodesSimilarWithPods(t *testing.T, n1, n2 *apiv1.Node, pods1, pods2 []*apiv1.Pod, shouldEqual bool) {
|
||||
ni1 := schedulercache.NewNodeInfo(pods1...)
|
||||
ni1.SetNode(n1)
|
||||
ni2 := schedulercache.NewNodeInfo(pods2...)
|
||||
ni2.SetNode(n2)
|
||||
assert.Equal(t, shouldEqual, IsNodeInfoSimilar(ni1, ni2))
|
||||
}
|
||||
|
||||
func TestIdenticalNodesSimilar(t *testing.T) {
|
||||
n1 := BuildTestNode("node1", 1000, 2000)
|
||||
n2 := BuildTestNode("node2", 1000, 2000)
|
||||
checkNodesSimilar(t, n1, n2, true)
|
||||
}
|
||||
|
||||
func TestNodesSimilarVariousRequirements(t *testing.T) {
|
||||
n1 := BuildTestNode("node1", 1000, 2000)
|
||||
|
||||
// Different CPU capacity
|
||||
n2 := BuildTestNode("node2", 1000, 2000)
|
||||
n2.Status.Capacity[apiv1.ResourceCPU] = *resource.NewMilliQuantity(1001, resource.DecimalSI)
|
||||
checkNodesSimilar(t, n1, n2, false)
|
||||
|
||||
// Same CPU capacity, but slightly different allocatable
|
||||
n3 := BuildTestNode("node3", 1000, 2000)
|
||||
n3.Status.Allocatable[apiv1.ResourceCPU] = *resource.NewMilliQuantity(999, resource.DecimalSI)
|
||||
checkNodesSimilar(t, n1, n3, true)
|
||||
|
||||
// Same CPU capacity, significantly different allocatable
|
||||
n4 := BuildTestNode("node4", 1000, 2000)
|
||||
n4.Status.Allocatable[apiv1.ResourceCPU] = *resource.NewMilliQuantity(500, resource.DecimalSI)
|
||||
checkNodesSimilar(t, n1, n4, false)
|
||||
|
||||
// One with GPU, one without
|
||||
n5 := BuildTestNode("node5", 1000, 2000)
|
||||
n5.Status.Capacity[apiv1.ResourceNvidiaGPU] = *resource.NewQuantity(1, resource.DecimalSI)
|
||||
n5.Status.Allocatable[apiv1.ResourceNvidiaGPU] = n5.Status.Capacity[apiv1.ResourceNvidiaGPU]
|
||||
checkNodesSimilar(t, n1, n5, false)
|
||||
}
|
||||
|
||||
func TestNodesSimilarVariousRequirementsAndPods(t *testing.T) {
|
||||
n1 := BuildTestNode("node1", 1000, 2000)
|
||||
p1 := BuildTestPod("pod1", 500, 1000)
|
||||
p1.Spec.NodeName = "node1"
|
||||
|
||||
// Different allocatable, but same free
|
||||
n2 := BuildTestNode("node2", 1000, 2000)
|
||||
n2.Status.Allocatable[apiv1.ResourceCPU] = *resource.NewMilliQuantity(500, resource.DecimalSI)
|
||||
n2.Status.Allocatable[apiv1.ResourceMemory] = *resource.NewQuantity(1000, resource.DecimalSI)
|
||||
checkNodesSimilarWithPods(t, n1, n2, []*apiv1.Pod{p1}, []*apiv1.Pod{}, false)
|
||||
|
||||
// Same requests of pods
|
||||
n3 := BuildTestNode("node3", 1000, 2000)
|
||||
p3 := BuildTestPod("pod3", 500, 1000)
|
||||
p3.Spec.NodeName = "node3"
|
||||
checkNodesSimilarWithPods(t, n1, n3, []*apiv1.Pod{p1}, []*apiv1.Pod{p3}, true)
|
||||
|
||||
// Similar allocatable, similar pods
|
||||
n4 := BuildTestNode("node4", 1000, 2000)
|
||||
n4.Status.Allocatable[apiv1.ResourceCPU] = *resource.NewMilliQuantity(999, resource.DecimalSI)
|
||||
p4 := BuildTestPod("pod4", 501, 1001)
|
||||
p4.Spec.NodeName = "node4"
|
||||
checkNodesSimilarWithPods(t, n1, n4, []*apiv1.Pod{p1}, []*apiv1.Pod{p4}, true)
|
||||
}
|
||||
|
||||
func TestNodesSimilarVariousLabels(t *testing.T) {
|
||||
n1 := BuildTestNode("node1", 1000, 2000)
|
||||
n1.ObjectMeta.Labels["test-label"] = "test-value"
|
||||
n1.ObjectMeta.Labels["character"] = "winnie the pooh"
|
||||
|
||||
n2 := BuildTestNode("node2", 1000, 2000)
|
||||
n2.ObjectMeta.Labels["test-label"] = "test-value"
|
||||
|
||||
// Missing character label
|
||||
checkNodesSimilar(t, n1, n2, false)
|
||||
|
||||
n2.ObjectMeta.Labels["character"] = "winnie the pooh"
|
||||
checkNodesSimilar(t, n1, n2, true)
|
||||
|
||||
// Different hostname labels shouldn't matter
|
||||
n1.ObjectMeta.Labels[metav1.LabelHostname] = "node1"
|
||||
n2.ObjectMeta.Labels[metav1.LabelHostname] = "node2"
|
||||
checkNodesSimilar(t, n1, n2, true)
|
||||
|
||||
// Different zone shouldn't matter either
|
||||
n1.ObjectMeta.Labels[metav1.LabelZoneFailureDomain] = "mars-olympus-mons1-b"
|
||||
n2.ObjectMeta.Labels[metav1.LabelZoneFailureDomain] = "us-houston1-a"
|
||||
checkNodesSimilar(t, n1, n2, true)
|
||||
}
|
||||
|
|
@ -63,6 +63,7 @@ func BuildTestNode(name string, millicpu int64, mem int64) *apiv1.Node {
|
|||
ObjectMeta: metav1.ObjectMeta{
|
||||
Name: name,
|
||||
SelfLink: fmt.Sprintf("/api/v1/nodes/%s", name),
|
||||
Labels: map[string]string{},
|
||||
},
|
||||
Status: apiv1.NodeStatus{
|
||||
Capacity: apiv1.ResourceList{
|
||||
|
|
@ -78,7 +79,10 @@ func BuildTestNode(name string, millicpu int64, mem int64) *apiv1.Node {
|
|||
node.Status.Capacity[apiv1.ResourceMemory] = *resource.NewQuantity(mem, resource.DecimalSI)
|
||||
}
|
||||
|
||||
node.Status.Allocatable = node.Status.Capacity
|
||||
node.Status.Allocatable = apiv1.ResourceList{}
|
||||
for k, v := range node.Status.Capacity {
|
||||
node.Status.Allocatable[k] = v
|
||||
}
|
||||
|
||||
return node
|
||||
}
|
||||
|
|
|
|||
Loading…
Reference in New Issue