-
-
Notifications
You must be signed in to change notification settings - Fork 319
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Added SpotInstance Percentage Multiplier with dynamic tags #175
Conversation
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Thanks for this contribution, this piece of functionality is great to have, I'm really excited about it and looking forward to see it merged.
START.md
Outdated
runtime hour before spot termination is free of charge. | ||
(default 1.1) | ||
|
||
-spot_price_multiplier_flag bool |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I would prefer to have this a multi-option flag instead of a boolean, perhaps named something like 'bidding_policy', and accepting string values such as 'normal' and 'aggressive' and document them in detail. This way we can add more policies later on.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The idea would be to have a "policy type" and a tag associated with the configuration of this specific policy, is that correct @cristim ?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Yes
autospotting.go
Outdated
@@ -30,7 +30,8 @@ func main() { | |||
func run() { | |||
log.Println("Starting autospotting agent, build ", Version, "expiring on", ExpirationDate) | |||
|
|||
if isExpired(ExpirationDate) { | |||
if false { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Please don't change the expiration logic.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I'm not sure if the PR is done or not, but please do not leave commented code
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Will change this.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Thanks for the contribution! Always appreciate to see new contributors! :)
I've made one comment which has big impacts, basically the flag option seems unnecessary to me, by defaulting the multiplier to 1.0
, then it removes many code pieces which are made more complicated than they could be.
I had the same question/issue when implementing the flags for the algorithm https://github.com/cristim/autospotting/pull/54; and I realized I could make it easier by removing such cases.
If there is anything that seems unclear to you, or you disagree with, please let me know - I'd be happy to clarify/discuss.
START.md
Outdated
Multiplier for the spot price. Placing bids a fraction of a cent over the | ||
current price would aggressively optimize for cost savings since the last | ||
runtime hour before spot termination is free of charge. | ||
(default 1.1) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I'd rather put 1.0, so that it doesn't change people's cost optimization/bidding strategy.
That could however be a note/advise in the documentation and/or be updated later on if we realize many people are updating this value to something slightly higher.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The 1.0 will place spot instance bids at exactly the current market price, the 1.1 is there to offer a 10% buffer above the current price.
But I also find it a bit confusing, perhaps we should switch to percentages and also support absolute value increments, something like spot_price_buffer_percentage and spot_price_buffer_amount, what do you guys think?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@kartik894, @xlr-8 what do you think about my proposal to use percentage or absolut values for the spot price buffer?
I think something like spot_price_buffer_percentage=10(and make it a default when the aggressive policy is used) is more intuitive than the current spot_price_multiplier=1.1 for setting the bid to 10% over the current spot price. Also, negative percentage numbers don't make sense in this context, but the multiplier gives the users the chance to mess it up by giving 0.9, which will fail to launch any instances.
This is also quite consistent to the existing percentage/value concept which we use for keeping on-demand capacity, where we can have an absolute value as well as a percentage. This would bid X+current_price, where X may be given as a small fractional number, such as spot_price_buffer=0.001.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Your various comments made me think of: https://github.com/cristim/autospotting/pull/175/files#r156044213
We need to check the value anyway, just like it's done for the percentage/raw-value for on-demand instances. Also this value can't be set at an autoscaling-level if I'm not mistaken. Don't know if that's a goal, but if so some code need to be adapted.
But I agree that percentage might make more sense here, as it's a multiplier rather a raw number.
autospotting.go
Outdated
@@ -30,7 +30,8 @@ func main() { | |||
func run() { | |||
log.Println("Starting autospotting agent, build ", Version, "expiring on", ExpirationDate) | |||
|
|||
if isExpired(ExpirationDate) { | |||
if false { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I'm not sure if the PR is done or not, but please do not leave commented code
core/autoscaling.go
Outdated
a.SpotPriceMultiplierFlag = newValue | ||
return done | ||
} | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I like the idea of loading the value dynamically, I guess it should be adapted to have a map of string/func based on the tag's value.
core/autoscaling.go
Outdated
@@ -39,6 +47,8 @@ type autoScalingGroup struct { | |||
// spot instance requests generated for the current group | |||
spotInstanceRequests []*spotInstanceRequest | |||
minOnDemand int64 | |||
|
|||
SpotPriceMultiplierFlag bool |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I believe this variable doesn't need to be exported
core/autoscaling.go
Outdated
|
||
// DefaultSpotPriceMultiplierFlag stores the default flag value for the choice | ||
// of bid on a per-group level | ||
DefaultSpotPriceMultiplierFlag = false |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Regarding @cristim comments I assume that should be the policy type, rather than a flag
core/autoscaling.go
Outdated
@@ -182,6 +239,8 @@ func (a *autoScalingGroup) process() { | |||
a.scanInstances() | |||
a.loadDefaultConfig() | |||
a.loadConfigFromTags() | |||
a.loadDefaultSpotConfig() | |||
a.loadSpotConfigFromTags() |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I think those 2 methods could be put directly in the previous 2 methods (loadDefaultConfig
& loadConfigFromTags
) that were created for that; in case extra elements were to be added.
While re-reading the code I also realised it could be slightly re-arranged in order to load the default configuration and ASG configuration earlier than during the processing, see https://github.com/cristim/autospotting/issues/180.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
loadDefaultConfig has dedicated tests and merging mine with that will be a bit messy. What do you think @cristim ?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I agree with @xlr-8, the purpose of tests is for making such refactoring easier, let's merge these because they do more or less the same thing.
core/autoscaling.go
Outdated
} | ||
|
||
func (a *autoScalingGroup) loadConfSpot() bool { | ||
tagList := [1]string{SpotBidChoice} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Why declaring an array of string of 1 and not directly a string tagKey
?
core/autoscaling.go
Outdated
logger.Println("Launching spot instance with a bid =", baseOnDemandPrice) | ||
return baseOnDemandPrice | ||
} | ||
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I suppose the multiplier could be used here: https://github.com/cristim/autospotting/blob/master/core/region.go#L236 or here: https://github.com/cristim/autospotting/blob/master/core/instance.go#L160-L162 - that would avoid using this method.
Just like this new parameter actually: https://github.com/cristim/autospotting/pull/163/files#diff-17a0faaa6296d309bb099b250ff6ead1R162.
I'd tend to prefer putting them in the instance.go
file as it seems more logical, but the main method would need to take new parameters such as biding_policies
or multipliers
or something like this.
@cristim what are your thoughts on this?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
It can be done, but at the repo level - requestSpotPrices() might be used for another bidding policy in the future and I wouldn't want to touch that. isPriceCompatible() is used to determine the choice of spot instances to be spawned for the current on-demand instance - Also wouldn't want to change that. I did think of these scenarios before I begun the implementation, but I figured it would be best placed in autoscaling.go since that is where you call for the bidPrice to be placed.
core/autoscaling_test.go
Outdated
tt.multiplier, tt.flag, actualPrice, tt.want, currentSpotPrice) | ||
} | ||
} | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Rest of the tests seem good to me!
@xlr-8 we can't entirely get rid of the boolean or the string flag because this is meant to introduce an additional bidding policy, so we basically need a switch to choose between the old and this one and a second piece of configuration (or maybe later more) for tweaking this additional bidding strategy. In theory we could swing to the new bidding strategy only if its configuration tweaking option is different from a default value, but I would prefer to use an explicit, dedicated flag for this switch. |
Alright, thx @cristim I'll update my comments accordingly, sorry for the delay @kartik894 |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I try to update all previous comments, if anything's missing please let me know. As the strategy is to go with policy name then some code has to be updated
core/autoscaling_test.go
Outdated
} | ||
|
||
} | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Would need to be updated to fetch various decided policy names
START.md
Outdated
Multiplier for the spot price. Placing bids a fraction of a cent over the | ||
current price would aggressively optimize for cost savings since the last | ||
runtime hour before spot termination is free of charge. | ||
(default 1.1) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@kartik894, @xlr-8 what do you think about my proposal to use percentage or absolut values for the spot price buffer?
I think something like spot_price_buffer_percentage=10(and make it a default when the aggressive policy is used) is more intuitive than the current spot_price_multiplier=1.1 for setting the bid to 10% over the current spot price. Also, negative percentage numbers don't make sense in this context, but the multiplier gives the users the chance to mess it up by giving 0.9, which will fail to launch any instances.
This is also quite consistent to the existing percentage/value concept which we use for keeping on-demand capacity, where we can have an absolute value as well as a percentage. This would bid X+current_price, where X may be given as a small fractional number, such as spot_price_buffer=0.001.
autospotting.go
Outdated
flag.Float64Var(&c.SpotPriceMultiplier, "spot_price_multiplier", 1.1, | ||
"Multiplier for the spot price. Placing bids a fraction of a cent over the current price\n"+ | ||
"\twould aggressively optimize for cost savings since the last runtime hour before spot termination\n"+ | ||
"\tis free of charge.") |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
It turns out after the per-second billing the last hour is no longer free of charge.
As of now 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.
For example:
- on-demand price: 1
- spot-price: 0.2 but increased to 0.9 over time
- this instance would be terminated by this new policy as soon as the price was exceeding 0.22 (using the vehiculated 10% price buffer we want to use as default the bid price would be 0.22, instead of 1).
With the default policy the spot instance in the above example may run forever after this significant price increase unless the price goes over 1.
This allows us to find other spot instance types that may be priced better as of the moment when the price increased.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
core/autoscaling.go
Outdated
logger.Println("a.BiddingPolicy: ", a.BiddingPolicy) | ||
if a.BiddingPolicy == "aggressive" { | ||
logger.Println("Launching spot instance with a bid =", currentSpotPrice*a.region.conf.SpotPriceMultiplier) | ||
return currentSpotPrice * a.region.conf.SpotPriceMultiplier |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I realised that there might be a potential issue here, if the spot price is close to the on-demand price for some reasons (or the multiplier very high), then having the SpotPriceMultiplier might allow the bidding to go above that value. e.g.:
spotPrice: 0.5
onDemandPrice: 1
spotPriceMultiplier: 3.0
spotBid: => 1.5
# Other case
spotPrice: 0.9 (couldn't find better)
onDemandPrice: 1
spotPriceMultiplier: 1.5
spotBid: => 1.35
I think we might want to check that this potential maximum price isn't higher than the on-demand price, just for the sake of avoiding it; or we might end with spot instances running costing more than on-demand.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Good catch!
We can just take the minimum between ondemand price and the calculated spot price
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Cool, will do.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I approve what I saw so far, although I can't try it out these days.
@xlr-8 do you have any concerns about this or can we merge it?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Couple questions/changes that seem worth checking Also, I haven't yet tested it.
However the overall logic seems good to me, did it react as you expected?
terraform/variables.tf
Outdated
@@ -14,6 +14,16 @@ variable "asg_on_demand_price_multiplier" { | |||
default = "1.0" | |||
} | |||
|
|||
variable "asg_spot_price_buffer_percentage" { | |||
description = "Percentage above the current spot price to place the bid" | |||
default = "1.1" |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Isn't it supposed to be 10.0
?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Ah yes, my bad. Will change and ping you.
terraform/autospotting.tf
Outdated
@@ -4,6 +4,8 @@ module "autospotting" { | |||
autospotting_min_on_demand_number = "${var.asg_min_on_demand_number}" | |||
autospotting_min_on_demand_percentage = "${var.asg_min_on_demand_percentage}" | |||
autospotting_on_demand_price_multiplier = "${var.asg_on_demand_price_multiplier}" | |||
autospotting_spot_price_buffer_percentage = "${var.asg_spot_price_buffer_percentage}" | |||
autospotting_bidding_policy = "${var.asg_bidding_policy}" |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Could run terraform fmt -write=true
to format it properly
terraform/variables.tf
Outdated
description = "Choice of bidding policy for the spot instance" | ||
default = "normal" | ||
} | ||
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Could run terraform fmt -write=true
on those as well; I shall update the doc on this point
core/autoscaling_test.go
Outdated
currentOnDemandPrice: 0.0464, | ||
policy: "aggressive", | ||
want: 0.02376, | ||
}, |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Good testing of all cases! :)
core/autoscaling.go
Outdated
logger.Printf("Ignoring out of range value : %f\n", spotPriceBufferPercentage) | ||
} | ||
return DefaultSpotPriceBufferPercentage, false | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
If you don't mind, I'd rather have the function rewritten as such:
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
}
I believe it works the same way as yours, but just seems more clear to me
|
||
// DefaultBiddingPolicy stores the default bidding policy for | ||
// the spot bid on a per-group level | ||
DefaultBiddingPolicy = "normal" |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I think those new variables (except SpotPriceBufferPercentageTag
) don't need to be exported.
It's most likely true for other older one as well, would need to be corrected. (Opened https://github.com/cristim/autospotting/issues/186)
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Should I leave it as is in this pull request?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I believe it's fine like this, I'll pass on those later on
core/autoscaling.go
Outdated
|
||
return baseOnDemandPrice | ||
} | ||
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I would also rewrite that as:
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))
}
Please note I removed the check of the value, as it seemed already done here: https://github.com/cristim/autospotting/pull/175/files#diff-de828bac595094756ea2a28fa415583bR219
My logic is always to avoid imbricated if
, if there is an error check or direct return, then do it. I find that it usually simplifies reading/understanding of the code.
Cool, code looks good to me, please:
On my side, I'll give it a quick shot tonight on AWS and I'll merge if everything worked as expected. |
Will do the rebasing. The code climate points to cognitive complexity in two sections of the code which I'm not sure how to fix. |
core/autoscaling.go
Outdated
debug.Println("Couldn't find tag", SpotPriceBufferPercentageTag) | ||
} | ||
return false | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I believe this could be rewritten as:
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
}
Which would also solve code climate issue I believe, as the comment about imbricated if/else is something that adds complexity to the code and that code climate dislike.
core/autoscaling.go
Outdated
logger.Println("BiddingPolicy =", a.region.conf.BiddingPolicy) | ||
return done | ||
} | ||
} | ||
} else { | ||
debug.Println("Couldn't find tag", tagKey) | ||
} | ||
} | ||
return false | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Applying the same principle as before, it could be re-written as:
func (a *autoScalingGroup) loadConfSpot() bool {
tagList := []string{BiddingPolicyTag}
loadDyn := map[string]func(*string) (string, bool){
BiddingPolicyTag: a.loadBiddingPolicy,
}
for _, tagKey := range tagList {
tagValue := a.getTagValue(tagKey)
if tagValue == nil {
debug.Println("Couldn't find tag", tagKey)
continue
}
if _, ok := loadDyn[tagKey]; !ok {
debug.Println("No method associated with tag", tagKey)
continue
}
if newValue, done := loadDyn[tagKey](tagValue); done {
a.region.conf.BiddingPolicy = newValue
logger.Println("BiddingPolicy =", a.region.conf.BiddingPolicy)
return done
}
}
return false
}
Or considering we have only one policy, even as simple as:
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
}
I presume both could help/should solve the codeClimate issue
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The first one still failed the CodeClimate run.... So, I used the second one.
Tests on AWS worked as expected! Waiting for the rebase before giving it a final look/merge. Thanks again! |
e8e87e0
to
a1cac1e
Compare
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.
All good! 👍 🎉 |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@kartik894 Huge thanks for this contribution, this is a great feature and I'm really glad to finally see it land.
My pleasure 👍 |
Issue Type
Summary
Added configurable policy to place bids at a multiple of spot instance price or at on-demand price. Also added dynamic tag to enable this feature for an auto-scaling group.
Code review process:
The issue should be tagged with 'review wanted' label before the checklist below
is satisfied from the perspective of the PR author and the code is ready to be
peer-reviewed.
Code contribution checklist
to it.
guidelines.
test coverage doesn't decrease.
make full-test
.variables which are also passed as parameters to the
CloudFormation
and
Terraform
stacks defined as infrastructure code.
support per-group overrides using tags.
configurations.
proven to work using log output from various test runs.
appropriate) and formatted consistently to the existing log output.
new configuration options for both stack parameters and tag overrides.
contribution actually resolves it.