Skip to content

Commit

Permalink
Merge pull request #175 from kartik894/master
Browse files Browse the repository at this point in the history
Added normal/aggressive policies to optimize cost by bidding based on spot prices
  • Loading branch information
xlr-8 authored Dec 13, 2017
2 parents 0898534 + 17a39d1 commit 053135e
Show file tree
Hide file tree
Showing 11 changed files with 477 additions and 29 deletions.
13 changes: 13 additions & 0 deletions START.md
Original file line number Diff line number Diff line change
Expand Up @@ -125,6 +125,8 @@ module "autospotting" {
autospotting_min_on_demand_percentage = "50.0"
autospotting_regions_enabled = "eu*,us*"
on_demand_price_multiplier = "1.0"
spot_price_buffer_percentage = "10.0"
bidding_policy = "normal"
lambda_zipname = "./lambda.zip"
lambda_runtime = "python2.7"
Expand Down Expand Up @@ -224,6 +226,17 @@ Usage of ./autospotting:
or if you want to set your bid price to be higher than the on demand
price to reduce the chances that your spot instances will be terminated.
(default 1)
-spot_price_buffer_percentage float
Percentage Value of the bid above the current spot price. A spot
bid would be placed at a value
current_spot_price * [1 + (spot_price_buffer_percentage/100.0)] .
The main benefit is that it protects the group from running spot instances
that got significantly more expensive than when they were initially launched,
but still somewhat less than the on-demand price. (default 10.0)
-bidding_policy string
Policy choice for spot bid. If set to 'normal', we bid at the
on-demand price. If set to 'aggressive', we bid at a multiple of
the spot price. (default "normal")
-regions="": Regions where it should be activated (comma or whitespace separated
list, also supports globs), by default it runs on all regions.
Example: ./autospotting -regions 'eu-*,us-east-1'
Expand Down
Binary file added autospotting
Binary file not shown.
20 changes: 18 additions & 2 deletions autospotting.go
Original file line number Diff line number Diff line change
Expand Up @@ -42,12 +42,16 @@ func run() {
"min_on_demand_number=%d "+
"min_on_demand_percentage=%.1f "+
"allowed_instance_types=%v "+
"on_demand_price_multiplier=%.2f",
"on_demand_price_multiplier=%.2f"+
"spot_price_buffer_percentage=%.3f"+
"bidding_policy=%s",
conf.Regions,
conf.MinOnDemandNumber,
conf.MinOnDemandPercentage,
conf.AllowedInstanceTypes,
conf.OnDemandPriceMultiplier)
conf.OnDemandPriceMultiplier,
conf.SpotPriceBufferPercentage,
conf.BiddingPolicy)

autospotting.Run(conf.Config)
log.Println("Execution completed, nothing left to do")
Expand Down Expand Up @@ -124,6 +128,18 @@ func (c *cfgData) parseCommandLineFlags() {
"\tset your bid price to be higher than the on demand price to reduce the chances that your\n"+
"\tspot instances will be terminated.")

flag.Float64Var(&c.SpotPriceBufferPercentage, "spot_price_buffer_percentage", 10,
"Percentage Value of the bid above the current spot price. A spot bid would be placed at a value :\n"+
"\tcurrent_spot_price * [1 + (spot_price_buffer_percentage/100.0)]. The main benefit is that\n"+
"\tit protects the group from running spot instances that got significantly more expensive than\n"+
"\twhen they were initially launched, but still somewhat less than the on-demand price. Can be\n"+
"\tenforced using the tag: "+autospotting.SpotPriceBufferPercentageTag+". If the bid exceeds\n"+
"\tthe on-demand price, we place a bid at on-demand price itself.")

flag.StringVar(&c.BiddingPolicy, "bidding_policy", "normal",
"Policy choice for spot bid. If set to 'normal', we bid at the on-demand price. If set to 'aggressive',\n"+
"\twe bid at a percentage value above the spot price. ")

v := flag.Bool("version", false, "Print version number and exit.")

flag.Parse()
Expand Down
12 changes: 12 additions & 0 deletions cloudformation/stacks/AutoSpotting/template.json
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,16 @@
"Description": "Multiplier for the on-demand price. This is useful for volume discounts or if you want to set your bid price to be higher than the on demand price to reduce the chances that your spot instances will be terminated.",
"Type": "Number"
},
"SpotPricePercentageBuffer": {
"Default": "10.0",
"Description": "Percentage Value of the bid above the current spot price. A spot bid would be placed at a value = current_spot_price * [1 + (spot_price_buffer_percentage/100.0)]. The main benefit is that it protects the group from running spot instances that got significantly more expensive than when they were initially launched, but still somewhat less than the on-demand price.",
"Type": "Number"
},
"BiddingPolicy": {
"Default": "normal",
"Description": "Policy choice for spot bid. If set to 'normal', we bid at the on-demand price. If set to 'aggressive', we bid at a multiple of the spot price.",
"Type": "String"
},
"Regions": {
"Default": "",
"Description": "Space separated list of regions where it should run (supports globs), in case you may want to limit it to a smaller set of regions. If unset it will run against all available regions. Example: 'us-east-1 eu-*'",
Expand Down Expand Up @@ -88,6 +98,8 @@
"MIN_ON_DEMAND_NUMBER": { "Ref": "MinOnDemandNumber" },
"MIN_ON_DEMAND_PERCENTAGE": { "Ref": "MinOnDemandPercentage" },
"ON_DEMAND_PRICE_MULTIPLIER": { "Ref": "OnDemandPriceMultiplier" },
"SPOT_PRICE_BUFFER_PERCENTAGE": { "Ref": "SpotPricePercentageBuffer"},
"BIDDING_POLICY": { "Ref": "BiddingPolicy"},
"REGIONS": { "Ref": "Regions" },
"ALLOWED_INSTANCE_TYPES": { "Ref": "AllowedInstanceTypes" }
}
Expand Down
120 changes: 110 additions & 10 deletions core/autoscaling.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,14 +2,13 @@ package autospotting

import (
"errors"
"github.com/aws/aws-sdk-go/aws"
"github.com/aws/aws-sdk-go/service/autoscaling"
"github.com/aws/aws-sdk-go/service/ec2"
"math"
"strconv"
"strings"
"time"

"github.com/aws/aws-sdk-go/aws"
"github.com/aws/aws-sdk-go/service/autoscaling"
"github.com/aws/aws-sdk-go/service/ec2"
)

const (
Expand All @@ -23,9 +22,24 @@ const (
// absolute number.
OnDemandNumberLong = "autospotting_on_demand_number"

// BiddingPolicyTag stores the bidding policy for the spot instance
BiddingPolicyTag = "autospotting_bidding_policy"

// SpotPriceBufferPercentageTag stores percentage value above the
// current spot price to place the bid
SpotPriceBufferPercentageTag = "spot_price_buffer_percentage"

// DefaultMinOnDemandValue stores the default on-demand capacity to be kept
// running in a group managed by autospotting.
DefaultMinOnDemandValue = 0

// DefaultSpotPriceBufferPercentage stores the default percentage value
// above the current spot price to place a bid
DefaultSpotPriceBufferPercentage = 10.0

// DefaultBiddingPolicy stores the default bidding policy for
// the spot bid on a per-group level
DefaultBiddingPolicy = "normal"
)

type autoScalingGroup struct {
Expand Down Expand Up @@ -60,6 +74,21 @@ func (a *autoScalingGroup) loadPercentageOnDemand(tagValue *string) (int64, bool
return DefaultMinOnDemandValue, false
}

func (a *autoScalingGroup) loadSpotPriceBufferPercentage(tagValue *string) (float64, bool) {
spotPriceBufferPercentage, err := strconv.ParseFloat(*tagValue, 64)

if err != nil {
logger.Printf("Error with ParseFloat: %s\n", err.Error())
return DefaultSpotPriceBufferPercentage, false
} else if spotPriceBufferPercentage <= 0 {
logger.Printf("Ignoring out of range value : %f\n", spotPriceBufferPercentage)
return DefaultSpotPriceBufferPercentage, false
}

logger.Printf("Loaded SpotPriceBufferPercentage value to %f from tag %s\n", spotPriceBufferPercentage, SpotPriceBufferPercentageTag)
return spotPriceBufferPercentage, true
}

func (a *autoScalingGroup) loadNumberOnDemand(tagValue *string) (int64, bool) {
onDemand, err := strconv.Atoi(*tagValue)
if err != nil {
Expand Down Expand Up @@ -88,18 +117,72 @@ func (a *autoScalingGroup) loadConfOnDemand() bool {
return done
}
}
} else {
debug.Println("Couldn't find tag", tagKey)
}
debug.Println("Couldn't find tag", tagKey)
}
return false
}

func (a *autoScalingGroup) loadBiddingPolicy(tagValue *string) (string, bool) {
biddingPolicy := *tagValue
if biddingPolicy != "aggressive" {
return DefaultBiddingPolicy, false
}

logger.Printf("Loaded BiddingPolicy value with %s from tag %s\n", biddingPolicy, BiddingPolicyTag)
return biddingPolicy, true
}

func (a *autoScalingGroup) loadConfSpot() bool {
tagValue := a.getTagValue(BiddingPolicyTag)
if tagValue == nil {
debug.Println("Couldn't find tag", BiddingPolicyTag)
return false
}
if newValue, done := a.loadBiddingPolicy(tagValue); done {
a.region.conf.BiddingPolicy = newValue
logger.Println("BiddingPolicy =", a.region.conf.BiddingPolicy)
return done
}
return false
}

func (a *autoScalingGroup) loadConfSpotPrice() bool {

tagValue := a.getTagValue(SpotPriceBufferPercentageTag)
if tagValue == nil {
return false
}

newValue, done := a.loadSpotPriceBufferPercentage(tagValue)
if !done {
debug.Println("Couldn't find tag", SpotPriceBufferPercentageTag)
return false
}

a.region.conf.SpotPriceBufferPercentage = newValue
return done
}

// Add configuration of other elements here: prices, whitelisting, etc
func (a *autoScalingGroup) loadConfigFromTags() bool {

if a.loadConfOnDemand() {
resOnDemandConf := a.loadConfOnDemand()

resSpotConf := a.loadConfSpot()

resSpotPriceConf := a.loadConfSpotPrice()

if resOnDemandConf {
logger.Println("Found and applied configuration for OnDemand value")
}
if resSpotConf {
logger.Println("Found and applied configuration for Spot Bid")
}
if resSpotPriceConf {
logger.Println("Found and applied configuration for Spot Price")
}
if resOnDemandConf || resSpotConf || resSpotPriceConf {
return true
}
return false
Expand Down Expand Up @@ -131,6 +214,10 @@ func (a *autoScalingGroup) loadDefaultConfig() bool {
done := false
a.minOnDemand = DefaultMinOnDemandValue

if a.region.conf.SpotPriceBufferPercentage <= 0 {
a.region.conf.SpotPriceBufferPercentage = DefaultSpotPriceBufferPercentage
}

if a.region.conf.MinOnDemandNumber != 0 {
a.minOnDemand, done = a.loadDefaultConfigNumber()
}
Expand Down Expand Up @@ -536,6 +623,20 @@ func (a *autoScalingGroup) getAllowedInstanceTypes(baseInstance *instance) []str

}

func (a *autoScalingGroup) getPricetoBid(
baseOnDemandPrice float64, currentSpotPrice float64) float64 {

logger.Println("BiddingPolicy: ", a.region.conf.BiddingPolicy)

if a.region.conf.BiddingPolicy == DefaultBiddingPolicy {
logger.Println("Launching spot instance with a bid =", baseOnDemandPrice)
return baseOnDemandPrice
}

logger.Println("Launching spot instance with a bid =", math.Min(baseOnDemandPrice, currentSpotPrice*(1.0+a.region.conf.SpotPriceBufferPercentage/100.0)))
return math.Min(baseOnDemandPrice, currentSpotPrice*(1.0+a.region.conf.SpotPriceBufferPercentage/100.0))
}

func (a *autoScalingGroup) launchCheapestSpotInstance(
azToLaunchIn *string) error {

Expand Down Expand Up @@ -569,12 +670,11 @@ func (a *autoScalingGroup) launchCheapestSpotInstance(
baseOnDemandPrice := baseInstance.price

currentSpotPrice := newInstance.pricing.spot[*azToLaunchIn]

logger.Println("Finished searching for best spot instance in ", *azToLaunchIn)
logger.Println("Replacing an on-demand", *baseInstance.InstanceType,
"instance having the ondemand price", baseOnDemandPrice)
logger.Println("Launching best compatible instance:", newInstanceType,
"with current spot price:", currentSpotPrice)
"with the current spot price:", currentSpotPrice)

lc := a.getLaunchConfiguration()

Expand All @@ -584,7 +684,7 @@ func (a *autoScalingGroup) launchCheapestSpotInstance(
*azToLaunchIn)

logger.Println("Bidding for spot instance for ", a.name)
return a.bidForSpotInstance(spotLS, baseOnDemandPrice)
return a.bidForSpotInstance(spotLS, a.getPricetoBid(baseOnDemandPrice, currentSpotPrice))
}

func (a *autoScalingGroup) loadSpotInstanceRequest(
Expand Down
Loading

0 comments on commit 053135e

Please sign in to comment.