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{
|
ObjectMeta: metav1.ObjectMeta{
|
||||||
Name: name,
|
Name: name,
|
||||||
SelfLink: fmt.Sprintf("/api/v1/nodes/%s", name),
|
SelfLink: fmt.Sprintf("/api/v1/nodes/%s", name),
|
||||||
|
Labels: map[string]string{},
|
||||||
},
|
},
|
||||||
Status: apiv1.NodeStatus{
|
Status: apiv1.NodeStatus{
|
||||||
Capacity: apiv1.ResourceList{
|
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.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
|
return node
|
||||||
}
|
}
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue