Skip to content

Commit

Permalink
Spot bids: Allow different policies to optimize costs
Browse files Browse the repository at this point in the history
It is now possible to bid at a value based on the spot instance price
('aggressive' policy) rather than on-demand price ('normal' policy). Thus
allowing to bid for a price slightly higher than the spot price. This is
particularly useful to avoid having spot instances that cost too much.

To enable this, the policy has to be set as 'aggressive' ('normal' default) and
a 'buffer percentage' needs to be defined (10% higher default) to know how much
more users are willing to pay above the spot price found. Details about the
tags are provided in START.md.

Those parameters are configurable from Cloudfront/Terraform and the ASGs.
  • Loading branch information
kartik894 committed Dec 13, 2017
1 parent 0898534 commit 17a39d1
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 17a39d1

Please sign in to comment.