Skip to content

Commit

Permalink
Merge pull request #91 from mwielgus/price-3
Browse files Browse the repository at this point in the history
GCE pricing model
  • Loading branch information
mwielgus authored May 26, 2017
2 parents e11fa3d + 80bf191 commit 5988fec
Show file tree
Hide file tree
Showing 4 changed files with 275 additions and 4 deletions.
2 changes: 1 addition & 1 deletion cluster-autoscaler/cloudprovider/gce/gce_cloud_provider.go
Original file line number Diff line number Diff line change
Expand Up @@ -94,7 +94,7 @@ func (gce *GceCloudProvider) NodeGroupForNode(node *apiv1.Node) (cloudprovider.N

// Pricing returns pricing model for this cloud provider or error if not available.
func (gce *GceCloudProvider) Pricing() (cloudprovider.PricingModel, error) {
return nil, cloudprovider.ErrNotImplemented
return &GcePriceModel{}, nil
}

// GceRef contains s reference to some entity in GCE/GKE world.
Expand Down
162 changes: 162 additions & 0 deletions cluster-autoscaler/cloudprovider/gce/gce_price_model.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,162 @@
/*
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"
"time"

metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
apiv1 "k8s.io/kubernetes/pkg/api/v1"
)

// GcePriceModel implements PriceModel interface for GCE.
type GcePriceModel struct {
}

const (
//TODO: Move it to a config file.
cpuPricePerHour = 0.033174
memoryPricePerHourPerGb = 0.004446
preemptibleDiscount = 0.00698 / 0.033174
gpuPricePerHour = 0.700

gigabyte = 1024.0 * 1024.0 * 1024.0
preemptibleLabel = "cloud.google.com/gke-preemptible"
)

var (
instancePrices = map[string]float64{
"n1-standard-1": 0.0475,
"n1-standard-2": 0.0950,
"n1-standard-4": 0.1900,
"n1-standard-8": 0.3800,
"n1-standard-16": 0.7600,
"n1-standard-32": 1.5200,
"n1-standard-64": 3.0400,
"f1-micro": 0.0076,
"g1-small": 0.0257,
"n1-highmem-2": 0.1184,
"n1-highmem-4": 0.2368,
"n1-highmem-8": 0.4736,
"n1-highmem-16": 0.9472,
"n1-highmem-32": 1.8944,
"n1-highmem-64": 3.7888,
"n1-highcpu-2": 0.0709,
"n1-highcpu-4": 0.1418,
"n1-highcpu-8": 0.2836,
"n1-highcpu-16": 0.5672,
"n1-highcpu-32": 1.1344,
"n1-highcpu-64": 2.2688,
}

preemptiblePrices = map[string]float64{
"n1-standard-1": 0.0100,
"n1-standard-2": 0.0200,
"n1-standard-4": 0.0400,
"n1-standard-8": 0.0800,
"n1-standard-16": 0.1600,
"n1-standard-32": 0.3200,
"n1-standard-64": 0.6400,
"f1-micro": 0.0035,
"g1-small": 0.0070,
"n1-highmem-2": 0.0250,
"n1-highmem-4": 0.0500,
"n1-highmem-8": 0.1000,
"n1-highmem-16": 0.2000,
"n1-highmem-32": 0.4000,
"n1-highmem-64": 0.8000,
"n1-highcpu-2": 0.0150,
"n1-highcpu-4": 0.0300,
"n1-highcpu-8": 0.0600,
"n1-highcpu-16": 0.1200,
"n1-highcpu-32": 0.2400,
"n1-highcpu-64": 0.4800,
}
)

// NodePrice returns a price of running the given node for a given period of time.
// All prices are in USD.
func (model *GcePriceModel) NodePrice(node *apiv1.Node, startTime time.Time, endTime time.Time) (float64, error) {
price := 0.0
basePriceFound := false
if node.Labels != nil {
if machineType, found := node.Labels[metav1.LabelInstanceType]; found {
var priceMapToUse map[string]float64
if node.Labels[preemptibleLabel] == "true" {
priceMapToUse = preemptiblePrices
} else {
priceMapToUse = instancePrices
}
if basePricePerHour, found := priceMapToUse[machineType]; found {
price = basePricePerHour * getHours(startTime, endTime)
basePriceFound = true
}
}
}
if !basePriceFound {
price = getBasePrice(node.Status.Capacity, startTime, endTime)
if node.Labels != nil && node.Labels[preemptibleLabel] == "true" {
price = price * preemptibleDiscount
}
}
// TODO: handle ssd.

price += getAdditionalPrice(node.Status.Capacity, startTime, endTime)
return price, nil
}

func getHours(startTime time.Time, endTime time.Time) float64 {
minutes := math.Ceil(float64(endTime.Sub(startTime)) / float64(time.Minute))
hours := minutes / 60.0
return hours
}

// PodPrice returns a theoretical minimum priece of running a pod for a given
// period of time on a perfectly matching machine.
func (model *GcePriceModel) PodPrice(pod *apiv1.Pod, startTime time.Time, endTime time.Time) (float64, error) {
price := 0.0
for _, container := range pod.Spec.Containers {
price += getBasePrice(container.Resources.Requests, startTime, endTime)
price += getAdditionalPrice(container.Resources.Requests, startTime, endTime)
}
return price, nil
}

func getBasePrice(resources apiv1.ResourceList, startTime time.Time, endTime time.Time) float64 {
if len(resources) == 0 {
return 0
}
hours := getHours(startTime, endTime)
price := 0.0
cpu := resources[apiv1.ResourceCPU]
mem := resources[apiv1.ResourceMemory]
price += float64(cpu.MilliValue()) / 1000.0 * cpuPricePerHour * hours
price += float64(mem.Value()) / gigabyte * memoryPricePerHourPerGb * hours
return price
}

func getAdditionalPrice(resources apiv1.ResourceList, startTime time.Time, endTime time.Time) float64 {
if len(resources) == 0 {
return 0
}
hours := getHours(startTime, endTime)
price := 0.0
gpu := resources[apiv1.ResourceNvidiaGPU]
price += float64(gpu.MilliValue()) / 1000.0 * gpuPricePerHour * hours
return price
}
109 changes: 109 additions & 0 deletions cluster-autoscaler/cloudprovider/gce/gce_price_model_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,109 @@
/*
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"

"k8s.io/apimachinery/pkg/api/resource"
. "k8s.io/autoscaler/cluster-autoscaler/utils/test"
apiv1 "k8s.io/kubernetes/pkg/api/v1"

"github.com/stretchr/testify/assert"
)

func TestGetNodePrice(t *testing.T) {
labels1, _ := buildGenericLabels(GceRef{
Name: "kubernetes-minion-group",
Project: "mwielgus-proj",
Zone: "us-central1-b"},
"n1-standard-8", "sillyname")

labels2, _ := buildGenericLabels(GceRef{
Name: "kubernetes-minion-group",
Project: "mwielgus-proj",
Zone: "us-central1-b"},
"n1-standard-8", "sillyname")
labels2[preemptibleLabel] = "true"

model := &GcePriceModel{}
now := time.Now()

// regular
node1 := BuildTestNode("sillyname1", 8000, 30*1024*1024*1024)
node1.Labels = labels1
price1, err := model.NodePrice(node1, now, now.Add(time.Hour))
assert.NoError(t, err)

// preemptable
node2 := BuildTestNode("sillyname2", 8000, 30*1024*1024*1024)
node2.Labels = labels2
price2, err := model.NodePrice(node2, now, now.Add(time.Hour))
assert.NoError(t, err)
// preemptable nodes should be way cheaper than regular.
assert.True(t, price1 > 3*price2)

// custom node
node3 := BuildTestNode("sillyname3", 8000, 30*1024*1024*1024)
price3, err := model.NodePrice(node3, now, now.Add(time.Hour))
assert.NoError(t, err)
// custom nodes should be slightly more expensive than regular.
assert.True(t, price1 < price3)
assert.True(t, price1*1.2 > price3)

// regular with gpu
node4 := BuildTestNode("sillyname4", 8000, 30*1024*1024*1024)
node4.Status.Capacity[apiv1.ResourceNvidiaGPU] = *resource.NewQuantity(1, resource.DecimalSI)
node4.Labels = labels1
price4, err := model.NodePrice(node4, now, now.Add(time.Hour))

// preemptable with gpu
node5 := BuildTestNode("sillyname5", 8000, 30*1024*1024*1024)
node5.Labels = labels2
node5.Status.Capacity[apiv1.ResourceNvidiaGPU] = *resource.NewQuantity(1, resource.DecimalSI)
price5, err := model.NodePrice(node5, now, now.Add(time.Hour))

// Nodes with GPU are way more expensive than regular.
// Being preemptable doesn't bring much of a discount (less than 50%).
assert.True(t, price4 > price5)
assert.True(t, price4 < 1.5*price5)
assert.True(t, price4 > 2*price1)

// small custom node
node6 := BuildTestNode("sillyname6", 1000, 3750*1024*1024)
price6, err := model.NodePrice(node6, now, now.Add(time.Hour))
assert.NoError(t, err)
// 8 times smaller node shoul be 8 times less expensive.
assert.True(t, math.Abs(price3-8*price6) < 0.1)
}

func TestGetPodPrice(t *testing.T) {
pod1 := BuildTestPod("a1", 100, 500*1024*1024)
pod2 := BuildTestPod("a2", 2*100, 2*500*1024*1024)

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)
}
6 changes: 3 additions & 3 deletions cluster-autoscaler/utils/test/test_utils.go
Original file line number Diff line number Diff line change
Expand Up @@ -58,7 +58,7 @@ func BuildTestPod(name string, cpu int64, mem int64) *apiv1.Pod {
}

// BuildTestNode creates a node with specified capacity.
func BuildTestNode(name string, cpu int64, mem int64) *apiv1.Node {
func BuildTestNode(name string, millicpu int64, mem int64) *apiv1.Node {
node := &apiv1.Node{
ObjectMeta: metav1.ObjectMeta{
Name: name,
Expand All @@ -71,8 +71,8 @@ func BuildTestNode(name string, cpu int64, mem int64) *apiv1.Node {
},
}

if cpu >= 0 {
node.Status.Capacity[apiv1.ResourceCPU] = *resource.NewMilliQuantity(cpu, resource.DecimalSI)
if millicpu >= 0 {
node.Status.Capacity[apiv1.ResourceCPU] = *resource.NewMilliQuantity(millicpu, resource.DecimalSI)
}
if mem >= 0 {
node.Status.Capacity[apiv1.ResourceMemory] = *resource.NewQuantity(mem, resource.DecimalSI)
Expand Down

0 comments on commit 5988fec

Please sign in to comment.