206 lines
9.5 KiB
Go
206 lines
9.5 KiB
Go
/*
|
|
Copyright 2016 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 gce
|
|
|
|
import (
|
|
"math"
|
|
"testing"
|
|
"time"
|
|
|
|
apiv1 "k8s.io/api/core/v1"
|
|
"k8s.io/apimachinery/pkg/api/resource"
|
|
"k8s.io/autoscaler/cluster-autoscaler/utils/gpu"
|
|
. "k8s.io/autoscaler/cluster-autoscaler/utils/test"
|
|
"k8s.io/autoscaler/cluster-autoscaler/utils/units"
|
|
|
|
"github.com/stretchr/testify/assert"
|
|
)
|
|
|
|
func testNode(t *testing.T, nodeName string, instanceType string, millicpu int64, mem int64, gpuType string, gpuCount int64, isPreemptible bool, isSpot bool) *apiv1.Node {
|
|
node := BuildTestNode(nodeName, millicpu, mem)
|
|
labels, err := BuildGenericLabels(GceRef{
|
|
Name: "kubernetes-minion-group",
|
|
Project: "mwielgus-proj",
|
|
Zone: "us-central1-b"},
|
|
instanceType,
|
|
nodeName,
|
|
OperatingSystemLinux)
|
|
assert.NoError(t, err)
|
|
if isPreemptible {
|
|
labels[preemptibleLabel] = "true"
|
|
}
|
|
if isSpot {
|
|
labels[spotLabel] = "true"
|
|
}
|
|
if gpuCount > 0 {
|
|
node.Status.Capacity[gpu.ResourceNvidiaGPU] = *resource.NewQuantity(gpuCount, resource.DecimalSI)
|
|
node.Status.Allocatable[gpu.ResourceNvidiaGPU] = *resource.NewQuantity(gpuCount, resource.DecimalSI)
|
|
if gpuType != "" {
|
|
labels[GPULabel] = gpuType
|
|
}
|
|
}
|
|
node.Labels = labels
|
|
return node
|
|
}
|
|
|
|
// this test is meant to cover all the branches in pricing logic, not all possible types of instances
|
|
func TestGetNodePrice(t *testing.T) {
|
|
// tests assert that price(cheaperNode) < priceComparisonCoefficient * price(expensiveNode)
|
|
cases := map[string]struct {
|
|
cheaperNode *apiv1.Node
|
|
expensiveNode *apiv1.Node
|
|
priceComparisonCoefficient float64
|
|
}{
|
|
// instance types
|
|
"e2 is cheaper than n1": {
|
|
cheaperNode: testNode(t, "e2", "e2-standard-8", 8000, 32*units.GiB, "", 0, false, false),
|
|
expensiveNode: testNode(t, "n1", "n1-standard-8", 8000, 30*units.GiB, "", 0, false, false),
|
|
priceComparisonCoefficient: 1,
|
|
},
|
|
"custom nodes are more expensive than n1": {
|
|
cheaperNode: testNode(t, "n1", "n1-standard-8", 8000, 30*units.GiB, "", 0, false, false),
|
|
expensiveNode: testNode(t, "custom", "custom-8", 8000, 30*units.GiB, "", 0, false, false),
|
|
priceComparisonCoefficient: 1,
|
|
},
|
|
"custom nodes are not extremely expensive": {
|
|
cheaperNode: testNode(t, "custom", "custom-8", 8000, 30*units.GiB, "", 0, false, false),
|
|
expensiveNode: testNode(t, "n1", "n1-standard-8", 8000, 30*units.GiB, "", 0, false, false),
|
|
priceComparisonCoefficient: 1.2,
|
|
},
|
|
"custom node price scales linearly": {
|
|
cheaperNode: testNode(t, "small_custom", "custom-1", 1000, 3.75*units.GiB, "", 0, false, false),
|
|
expensiveNode: testNode(t, "large_custom", "custom-8", 8000, 30*units.GiB, "", 0, false, false),
|
|
priceComparisonCoefficient: 1.0 / 7.9,
|
|
},
|
|
"custom node price scales linearly 2": {
|
|
cheaperNode: testNode(t, "large_custom", "custom-8", 8000, 30*units.GiB, "", 0, false, false),
|
|
expensiveNode: testNode(t, "small_custom", "custom-1", 1000, 3.75*units.GiB, "", 0, false, false),
|
|
priceComparisonCoefficient: 8.1,
|
|
},
|
|
// GPUs
|
|
"accelerators are expensive": {
|
|
cheaperNode: testNode(t, "no_accelerators", "n1-standard-8", 8000, 30*units.GiB, "", 0, false, false),
|
|
// #NotFunny
|
|
expensiveNode: testNode(t, "large hadron collider", "n1-standard-8", 8000, 30*units.GiB, "nvidia-tesla-v100", 1, false, false),
|
|
priceComparisonCoefficient: 0.5,
|
|
},
|
|
"GPUs of unknown type are still expensive": {
|
|
cheaperNode: testNode(t, "no_accelerators", "n1-standard-8", 8000, 30*units.GiB, "", 0, false, false),
|
|
expensiveNode: testNode(t, "cyclotron", "n1-standard-8", 8000, 30*units.GiB, "", 1, false, false),
|
|
priceComparisonCoefficient: 0.5,
|
|
},
|
|
"different GPUs have different prices": {
|
|
cheaperNode: testNode(t, "cheap gpu", "n1-standard-8", 8000, 30*units.GiB, "nvidia-tesla-t4", 1, false, false),
|
|
expensiveNode: testNode(t, "large hadron collider", "n1-standard-8", 8000, 30*units.GiB, "nvidia-tesla-v100", 1, false, false),
|
|
priceComparisonCoefficient: 0.5,
|
|
},
|
|
"more GPUs is more expensive": {
|
|
cheaperNode: testNode(t, "1 gpu", "n1-standard-8", 8000, 30*units.GiB, "nvidia-tesla-v100", 1, false, false),
|
|
expensiveNode: testNode(t, "2 gpus", "n1-standard-8", 8000, 30*units.GiB, "nvidia-tesla-v100", 2, false, false),
|
|
priceComparisonCoefficient: 0.7,
|
|
},
|
|
"some instance types have fixed gpu count 1": {
|
|
cheaperNode: testNode(t, "with partitioning", "a2-highgpu-2g", 12000, 85*units.GiB, "nvidia-tesla-a100", 10, false, false),
|
|
expensiveNode: testNode(t, "without partitioning", "a2-highgpu-2g", 12000, 85*units.GiB, "nvidia-tesla-a100", 2, false, false),
|
|
priceComparisonCoefficient: 1.001,
|
|
},
|
|
"some instance types have fixed gpu count 2": {
|
|
cheaperNode: testNode(t, "without partitioning", "a2-highgpu-2g", 12000, 85*units.GiB, "nvidia-tesla-a100", 2, false, false),
|
|
expensiveNode: testNode(t, "with partitioning", "a2-highgpu-2g", 12000, 85*units.GiB, "nvidia-tesla-a100", 10, false, false),
|
|
priceComparisonCoefficient: 1.001,
|
|
},
|
|
// Preemptibles
|
|
"preemtpibles are cheap": {
|
|
cheaperNode: testNode(t, "preempted_i_can_be", "n1-standard-8", 8000, 30*units.GiB, "", 0, true, false),
|
|
expensiveNode: testNode(t, "ondemand", "n1-standard-8", 8000, 30*units.GiB, "", 0, false, false),
|
|
priceComparisonCoefficient: 0.25,
|
|
},
|
|
"custom preemptibles are also cheap": {
|
|
cheaperNode: testNode(t, "preempted_i_can_be", "custom-8", 8000, 30*units.GiB, "", 0, true, false),
|
|
expensiveNode: testNode(t, "ondemand", "custom-8", 8000, 30*units.GiB, "", 0, false, false),
|
|
priceComparisonCoefficient: 0.25,
|
|
},
|
|
"preemtpibles GPUs are (relatively) cheap": {
|
|
cheaperNode: testNode(t, "preempted_i_can_be", "n1-standard-8", 8000, 30*units.GiB, "nvidia-tesla-v100", 2, true, false),
|
|
expensiveNode: testNode(t, "ondemand", "n1-standard-8", 8000, 30*units.GiB, "nvidia-tesla-v100", 2, false, false),
|
|
priceComparisonCoefficient: 0.5,
|
|
},
|
|
"spot vms are cheap": {
|
|
cheaperNode: testNode(t, "spot", "n1-standard-8", 8000, 30*units.GiB, "", 0, false, true),
|
|
expensiveNode: testNode(t, "ondemand", "n1-standard-8", 8000, 30*units.GiB, "", 0, false, false),
|
|
priceComparisonCoefficient: 0.25,
|
|
},
|
|
"custom spot vms are also cheap": {
|
|
cheaperNode: testNode(t, "spot", "custom-8", 8000, 30*units.GiB, "", 0, false, true),
|
|
expensiveNode: testNode(t, "ondemand", "custom-8", 8000, 30*units.GiB, "", 0, false, false),
|
|
priceComparisonCoefficient: 0.25,
|
|
},
|
|
"spot GPUs are (relatively) cheap": {
|
|
cheaperNode: testNode(t, "spot", "n1-standard-8", 8000, 30*units.GiB, "nvidia-tesla-v100", 2, false, true),
|
|
expensiveNode: testNode(t, "ondemand", "n1-standard-8", 8000, 30*units.GiB, "nvidia-tesla-v100", 2, false, false),
|
|
priceComparisonCoefficient: 0.5,
|
|
},
|
|
// Unknown instances
|
|
"unknown cost is similar to its node family": {
|
|
cheaperNode: testNode(t, "unknown", "n1-unknown", 8000, 30*units.GiB, "", 0, false, false),
|
|
expensiveNode: testNode(t, "known", "n1-standard-8", 8000, 30*units.GiB, "", 0, false, false),
|
|
priceComparisonCoefficient: 1.001,
|
|
},
|
|
"unknown cost is similar to its node family 2": {
|
|
cheaperNode: testNode(t, "unknown", "n1-standard-8", 8000, 30*units.GiB, "", 0, false, false),
|
|
expensiveNode: testNode(t, "known", "n1-unknown", 8000, 30*units.GiB, "", 0, false, false),
|
|
priceComparisonCoefficient: 1.001,
|
|
},
|
|
// Custom instances
|
|
"big custom from cheap family is cheaper than small custom from expensive family": {
|
|
cheaperNode: testNode(t, "unknown", "e2-custom", 9000, 32*units.GiB, "", 0, false, false),
|
|
expensiveNode: testNode(t, "known", "n1-custom", 8000, 30*units.GiB, "", 0, false, false),
|
|
priceComparisonCoefficient: 1.001,
|
|
},
|
|
}
|
|
|
|
for tn, tc := range cases {
|
|
t.Run(tn, func(t *testing.T) {
|
|
model := &GcePriceModel{}
|
|
now := time.Now()
|
|
|
|
price1, err := model.NodePrice(tc.cheaperNode, now, now.Add(time.Hour))
|
|
assert.NoError(t, err)
|
|
price2, err := model.NodePrice(tc.expensiveNode, now, now.Add(time.Hour))
|
|
assert.NoError(t, err)
|
|
if price1 >= tc.priceComparisonCoefficient*price2 {
|
|
t.Errorf("Failed price comparison, price1=%v price2=%v price2*coefficient=%v", price1, price2, price2*tc.priceComparisonCoefficient)
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestGetPodPrice(t *testing.T) {
|
|
pod1 := BuildTestPod("a1", 100, 500*units.MiB)
|
|
pod2 := BuildTestPod("a2", 2*100, 2*500*units.MiB)
|
|
|
|
model := &GcePriceModel{}
|
|
now := time.Now()
|
|
|
|
price1, err := model.PodPrice(pod1, now, now.Add(time.Hour))
|
|
assert.NoError(t, err)
|
|
price2, err := model.PodPrice(pod2, now, now.Add(time.Hour))
|
|
assert.NoError(t, err)
|
|
// 2 times bigger pod should cost twice as much.
|
|
assert.True(t, math.Abs(price1*2-price2) < 0.001)
|
|
}
|