diff --git a/cluster-autoscaler/expander/price/price.go b/cluster-autoscaler/expander/price/price.go new file mode 100644 index 0000000000..0a1ed8295e --- /dev/null +++ b/cluster-autoscaler/expander/price/price.go @@ -0,0 +1,84 @@ +/* +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 price + +import ( + "time" + + "k8s.io/autoscaler/cluster-autoscaler/cloudprovider" + "k8s.io/autoscaler/cluster-autoscaler/expander" + "k8s.io/kubernetes/plugin/pkg/scheduler/schedulercache" + + "github.com/golang/glog" +) + +// TODO: add preferred node +type priceBased struct { + pricingModel cloudprovider.PricingModel +} + +// NewStrategy returns an expansion strategy that picks nodes based on price and preferred node type. +func NewStrategy(pricingModel cloudprovider.PricingModel) expander.Strategy { + return &priceBased{ + pricingModel: pricingModel, + } +} + +// BestOption selects option based on cost and preferred node type. +func (p *priceBased) BestOption(expansionOptions []expander.Option, nodeInfos map[string]*schedulercache.NodeInfo) *expander.Option { + var bestOption *expander.Option + bestOptionScore := 0.0 + now := time.Now() + then := now.Add(time.Hour) + +nextoption: + for i, option := range expansionOptions { + nodeInfo, found := nodeInfos[option.NodeGroup.Id()] + if !found { + glog.Warningf("No node info for %s", option.NodeGroup.Id()) + continue + } + nodePrice, err := p.pricingModel.NodePrice(nodeInfo.Node(), now, then) + if err != nil { + glog.Warningf("Failed to calculate node price for %s: %v", option.NodeGroup.Id(), err) + continue + } + totalNodePrice := nodePrice * float64(option.NodeCount) + totalPodPrice := 0.0 + for _, pod := range option.Pods { + podPrice, err := p.pricingModel.PodPrice(pod, now, then) + if err != nil { + glog.Warningf("Failed to calculate pod price for %s/%s: %v", pod.Namespace, pod.Name, err) + continue nextoption + } + totalPodPrice += podPrice + } + if totalPodPrice == 0 { + glog.Warningf("Total pod price is 0, skipping %s", option.NodeGroup.Id()) + continue + } + + optionScore := totalNodePrice / totalPodPrice + glog.V(5).Infof("Price of %s expansion is %f - ratio %f", option.NodeGroup.Id(), totalNodePrice) + + if bestOption == nil || bestOptionScore > optionScore { + bestOption = &expansionOptions[i] + bestOptionScore = optionScore + } + } + return bestOption +} diff --git a/cluster-autoscaler/expander/price/price_test.go b/cluster-autoscaler/expander/price/price_test.go new file mode 100644 index 0000000000..3b9aa60326 --- /dev/null +++ b/cluster-autoscaler/expander/price/price_test.go @@ -0,0 +1,150 @@ +/* +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 price + +import ( + "fmt" + "testing" + "time" + + "k8s.io/autoscaler/cluster-autoscaler/expander" + + "k8s.io/autoscaler/cluster-autoscaler/cloudprovider/test" + . "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" +) + +type testPricingModel struct { + nodePrice map[string]float64 + podPrice map[string]float64 +} + +func (tpm *testPricingModel) NodePrice(node *apiv1.Node, startTime time.Time, endTime time.Time) (float64, error) { + if price, found := tpm.nodePrice[node.Name]; found { + return price, nil + } + return 0.0, fmt.Errorf("price for node %v not found", node.Name) +} + +func (tpm *testPricingModel) PodPrice(node *apiv1.Pod, startTime time.Time, endTime time.Time) (float64, error) { + if price, found := tpm.podPrice[node.Name]; found { + return price, nil + } + return 0.0, fmt.Errorf("price for pod %v not found", node.Name) +} + +func TestPriceExpander(t *testing.T) { + n1 := BuildTestNode("n1", 1000, 1000) + n2 := BuildTestNode("n2", 10000, 1000) + + p1 := BuildTestPod("p1", 1000, 0) + p2 := BuildTestPod("p2", 500, 0) + + provider := testprovider.NewTestCloudProvider(nil, nil) + provider.AddNodeGroup("ng1", 1, 10, 1) + provider.AddNodeGroup("ng2", 1, 10, 1) + provider.AddNode("ng1", n1) + provider.AddNode("ng2", n2) + ng1, _ := provider.NodeGroupForNode(n1) + ng2, _ := provider.NodeGroupForNode(n2) + + ni1 := schedulercache.NewNodeInfo() + ni1.SetNode(n1) + ni2 := schedulercache.NewNodeInfo() + ni2.SetNode(n2) + nodeInfosForGroups := map[string]*schedulercache.NodeInfo{ + "ng1": ni1, "ng2": ni2, + } + + // All node groups accept the same set of pods + options := []expander.Option{ + { + NodeGroup: ng1, + NodeCount: 2, + Pods: []*apiv1.Pod{p1, p2}, + Debug: "1", + }, + { + NodeGroup: ng2, + NodeCount: 1, + Pods: []*apiv1.Pod{p1, p2}, + Debug: "2", + }, + } + + // First node group is cheapter + assert.Equal(t, "1", NewStrategy(&testPricingModel{ + podPrice: map[string]float64{ + "p1": 20.0, + "p2": 10.0, + }, + nodePrice: map[string]float64{ + "n1": 20.0, + "n2": 200.0, + }, + }).BestOption(options, nodeInfosForGroups).Debug) + + // Second node group is cheapter + assert.Equal(t, "2", NewStrategy(&testPricingModel{ + podPrice: map[string]float64{ + "p1": 20.0, + "p2": 10.0, + }, + nodePrice: map[string]float64{ + "n1": 200.0, + "n2": 100.0, + }, + }).BestOption(options, nodeInfosForGroups).Debug) + + // First group accept 1 pod and second accepts 2. + options2 := []expander.Option{ + { + NodeGroup: ng1, + NodeCount: 2, + Pods: []*apiv1.Pod{p1}, + Debug: "1", + }, + { + NodeGroup: ng2, + NodeCount: 1, + Pods: []*apiv1.Pod{p1, p2}, + Debug: "2", + }, + } + + // Both node groups are equally expensive. However 2 + // accept two pods. + assert.Equal(t, "2", NewStrategy(&testPricingModel{ + podPrice: map[string]float64{ + "p1": 20.0, + "p2": 10.0, + }, + nodePrice: map[string]float64{ + "n1": 200.0, + "n2": 200.0, + }, + }).BestOption(options2, nodeInfosForGroups).Debug) + + // Errors are expected + assert.Nil(t, NewStrategy(&testPricingModel{ + podPrice: map[string]float64{}, + nodePrice: map[string]float64{}, + }).BestOption(options2, nodeInfosForGroups)) +}