Skip to content

Commit

Permalink
Prioritize newer instance type generations
Browse files Browse the repository at this point in the history
  • Loading branch information
cristim committed Jul 7, 2022
1 parent bc4a656 commit 68c860e
Show file tree
Hide file tree
Showing 8 changed files with 149 additions and 13 deletions.
16 changes: 16 additions & 0 deletions cloudformation/stacks/AutoSpotting/template.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -240,6 +240,20 @@ Parameters:
- "capacity-optimized"
- "lowest-price"
Default: "capacity-optimized-prioritized"
PrioritizedInstanceTypesBias:
Type: "String"
Description: >
"Controls the ordering of instance types when using the capacity-optimized-prioritized
Spot allocation strategy. By default, using the 'lowest_price' bias it sorts instances by Spot price,
giving a softer preference than the 'lowest_price' Spot allocation strategy.
Alternatively, you can prefer newer instance types by using the 'prefer_newer_generations' bias",
which still oders instance types by price but penalizes instances from older generations by adding
10% to their hourly price for each older generation when considering them for the sorted list. For
example, a C5 instance type will be penalized by 10% over C6i, while a C4 will be penalized by 20%."
AllowedValues:
- prefer_newer_generations
- lowest_price
Default: prefer_newer_generations
SpotPricePercentageBuffer:
Default: "10.0"
Description: >
Expand Down Expand Up @@ -413,6 +427,8 @@ Resources:
- Ref: "Regions"
SPOT_ALLOCATION_STRATEGY:
Ref: SpotAllocationStrategy
PRIORITIZED_INSTANCE_TYPES_BIAS:
Ref: PrioritizedInstanceTypesBias
SPOT_PRICE_BUFFER_PERCENTAGE:
Ref: "SpotPricePercentageBuffer"
SPOT_PRODUCT_DESCRIPTION:
Expand Down
40 changes: 34 additions & 6 deletions core/autoscaling_configuration.go
Original file line number Diff line number Diff line change
Expand Up @@ -110,6 +110,10 @@ const (
// SpotAllocationStrategyTag is the name of the tag set on the AutoScaling Group that
// can override the global value of the SpotAllocationStrategy parameter
SpotAllocationStrategyTag = "autospotting_spot_allocation_strategy"

// PrioritizedInstanceTypesBiasTag is the name of the tag set on the AutoScaling Group that
// can override the global value of the PrioritizedInstanceTypesBias parameter
PrioritizedInstanceTypesBiasTag = "autospotting_prioritized_instance_types_bias"
)

// AutoScalingConfig stores some group-specific configurations that can override
Expand Down Expand Up @@ -151,6 +155,10 @@ type AutoScalingConfig struct {
// Further information about this is available at
// https://docs.aws.amazon.com/AWSEC2/latest/UserGuide/ec2-fleet-allocation-strategy.html
SpotAllocationStrategy string

// PrioritizedInstanceTypesBias can be used to tweak the ordering of the instance types when using the
//"capacity-optimized-prioritized" allocation strategy, biasing towards newer instance types.
PrioritizedInstanceTypesBias string
}

func (a *autoScalingGroup) loadPercentageOnDemand(tagValue *string) (int64, bool) {
Expand Down Expand Up @@ -286,6 +294,21 @@ func (a *autoScalingGroup) loadSpotAllocationStrategy() bool {
return false
}

func (a *autoScalingGroup) loadPrioritizedInstanceTypesBiasTag() bool {
a.config.PrioritizedInstanceTypesBias = a.region.conf.PrioritizedInstanceTypesBias

tagValue := a.getTagValue(PrioritizedInstanceTypesBiasTag)

if tagValue != nil {
log.Printf("Loaded PrioritizedInstanceTypesBiasTag value %v from tag %v\n", *tagValue, PrioritizedInstanceTypesBiasTag)
a.config.PrioritizedInstanceTypesBias = *tagValue
return true
}

debug.Println("Couldn't find tag", PrioritizedInstanceTypesBiasTag, "on the group", a.name, "using the default configuration")
return false
}

func (a *autoScalingGroup) loadGP2ConversionThreshold() bool {
// setting the default value
a.config.GP2ConversionThreshold = a.region.conf.GP2ConversionThreshold
Expand Down Expand Up @@ -433,32 +456,37 @@ func (a *autoScalingGroup) loadConfigFromTags() bool {
}

if a.LoadCronSchedule() {
log.Println("Found and applied configuration for Spot Price")
log.Println("Found and applied configuration for Cron Schedule")
ret = true
}

if a.LoadCronTimezone() {
log.Println("Found and applied configuration for Spot Price")
log.Println("Found and applied configuration for Cron Timezone")
ret = true
}

if a.LoadCronScheduleState() {
log.Println("Found and applied configuration for Spot Price")
log.Println("Found and applied configuration for Cron Schedule State")
ret = true
}

if a.loadPatchBeanstalkUserdata() {
log.Println("Found and applied configuration for Spot Price")
log.Println("Found and applied configuration for Beanstalk Userdata")
ret = true
}

if a.loadGP2ConversionThreshold() {
log.Println("Found and applied configuration for Spot Price")
log.Println("Found and applied configuration for GP2 Conversion Threshold")
ret = true
}

if a.loadSpotAllocationStrategy() {
log.Println("Found and applied configuration for Spot Price")
log.Println("Found and applied configuration for Spot Allocation Strategy")
ret = true
}

if a.loadPrioritizedInstanceTypesBiasTag() {
log.Println("Found and applied configuration for Prioritized Instance Types Bias")
ret = true
}

Expand Down
6 changes: 6 additions & 0 deletions core/config.go
Original file line number Diff line number Diff line change
Expand Up @@ -275,6 +275,12 @@ func ParseConfig(conf *Config) {
"replacement actions when executed in cron mode\n"+
"\tExample: ./AutoSpotting --billing_only true\n")

flagSet.StringVar(&conf.PrioritizedInstanceTypesBias, "prioritized_instance_types_bias", "lower_cost",
"\n\tControls the ordering of instance types when using the capacity-optimized-prioritized\n"+
"\tSpot allocation strategy. By default, using the 'lower_cost' bias it sorts instances by Spot price\n"+
"\tAlternatively, you can bias towards newer instance types by using the 'prefer_newer_generations' bias\n"+
"\tExample: ./AutoSpotting --prioritized_instance_types_bias lower_cost\n")

printVersion := flagSet.Bool("version", false, "Print version number and exit.\n")

if err := flagSet.Parse(os.Args[1:]); err != nil {
Expand Down
3 changes: 2 additions & 1 deletion core/instance_actions.go
Original file line number Diff line number Diff line change
Expand Up @@ -54,7 +54,8 @@ func (i *instance) launchSpotReplacement() (*string, error) {
}

defer i.deleteLaunchTemplate(lt)
instanceTypes, err := i.getCompatibleSpotInstanceTypesListSortedAscendingByPrice(
instanceTypes, err := i.getCompatibleSpotInstanceTypesList(
i.asg.config.PrioritizedInstanceTypesBias,
i.asg.getAllowedInstanceTypes(i),
i.asg.getDisallowedInstanceTypes(i))

Expand Down
6 changes: 4 additions & 2 deletions core/instance_manager.go
Original file line number Diff line number Diff line change
Expand Up @@ -29,8 +29,9 @@ type instances interface {
}

type acceptableInstance struct {
instanceTI instanceTypeInformation
price float64
instanceTI instanceTypeInformation
price float64
generationDelta int64
}

type instanceTypeInformation struct {
Expand All @@ -47,6 +48,7 @@ type instanceTypeInformation struct {
instanceStoreIsSSD bool
hasEBSOptimization bool
EBSThroughput float32
generationDelta int64
}

func makeInstances() instances {
Expand Down
75 changes: 72 additions & 3 deletions core/instance_queries.go
Original file line number Diff line number Diff line change
Expand Up @@ -9,11 +9,14 @@ import (
"fmt"
"log"
"path/filepath"
"regexp"
"sort"
"strconv"
"strings"

"github.com/aws/aws-sdk-go/aws"
"github.com/aws/aws-sdk-go/service/ec2"
ec2instancesinfo "github.com/cristim/ec2-instances-info"
)

func (i *instance) calculatePrice(spotCandidate instanceTypeInformation) float64 {
Expand Down Expand Up @@ -298,7 +301,7 @@ func (i *instance) isAllowed(instanceType string, allowedList []string, disallow
return true
}

func (i *instance) getCompatibleSpotInstanceTypesListSortedAscendingByPrice(allowedList []string,
func (i *instance) getCompatibleSpotInstanceTypesList(PrioritizationBias string, allowedList []string,
disallowedList []string) ([]*string, error) {
current := i.typeInfo
var acceptableInstanceTypes []acceptableInstance
Expand Down Expand Up @@ -333,7 +336,7 @@ func (i *instance) getCompatibleSpotInstanceTypesListSortedAscendingByPrice(allo
"with candidate", candidate.instanceType, "with price", candidatePrice)

if i.isAllowed(candidate.instanceType, allowedList, disallowedList) && i.isCompatible(&candidate, candidatePrice, attachedVolumesNumber) {
acceptableInstanceTypes = append(acceptableInstanceTypes, acceptableInstance{candidate, candidatePrice})
acceptableInstanceTypes = append(acceptableInstanceTypes, acceptableInstance{candidate, candidatePrice, candidate.generationDelta})
log.Println("\tMATCH FOUND, added", candidate.instanceType, "to launch candidates list for instance", *i.InstanceId)
} else if candidate.instanceType != "" {
debug.Println("Non compatible option found:", candidate.instanceType, "at", candidatePrice, " - discarding")
Expand All @@ -342,9 +345,24 @@ func (i *instance) getCompatibleSpotInstanceTypesListSortedAscendingByPrice(allo

if acceptableInstanceTypes != nil {
sort.Slice(acceptableInstanceTypes, func(i, j int) bool {
if PrioritizationBias == "prefer_newer_generations" {
log.Printf("Sorting biased towards newer instance types, comparing %v"+
" of generation delta %v and price %v(adjusted to %v) with %v of generation delta %v and price %v (adjusted to %v)\n",
acceptableInstanceTypes[i].instanceTI.instanceType,
acceptableInstanceTypes[i].generationDelta,
acceptableInstanceTypes[i].price,
acceptableInstanceTypes[i].price*(1.0+0.1*float64(acceptableInstanceTypes[i].generationDelta)),
acceptableInstanceTypes[j].instanceTI.instanceType,
acceptableInstanceTypes[j].generationDelta,
acceptableInstanceTypes[j].price,
acceptableInstanceTypes[j].price*(1.0+0.1*float64(acceptableInstanceTypes[j].generationDelta)))
return acceptableInstanceTypes[i].price*(1.0+0.1*float64(acceptableInstanceTypes[i].generationDelta)) <
acceptableInstanceTypes[j].price*(1.0+0.1*float64(acceptableInstanceTypes[j].generationDelta))
}
return acceptableInstanceTypes[i].price < acceptableInstanceTypes[j].price

})
debug.Println("List of cheapest compatible spot instances found, sorted ascending by price: ",
log.Println("List of cheapest compatible spot instances found, sorted ascending by price/bias: ",
acceptableInstanceTypes)
var result []*string
for _, ai := range acceptableInstanceTypes {
Expand Down Expand Up @@ -411,3 +429,54 @@ func (i *instance) isUnattachedSpotInstanceLaunchedForAnEnabledASG() bool {
}
return false
}

func calculateGenerationDelta(data *ec2instancesinfo.InstanceData,
instanceType string,
itfic *InstanceTypeFamilyInfoCache,
itmgc *InstanceTypeMaxGenerationCache) int64 {

family, generation := calculateInstanceTypeGeneration(instanceType, itfic)

if (*itmgc)[family] != 0 {
mg := (*itmgc)[family]
debug.Println("Found in cache for family", family, "latest generation ", mg)
delta := mg - generation
debug.Println("Calculated generation delta for instance type", instanceType, "of generation", generation, "to be", delta)
return delta
}

maxGeneration := generation
for i := range *data {
f, g := calculateInstanceTypeGeneration((*data)[i].InstanceType, itfic)
if f == family && g > maxGeneration {
maxGeneration = g
}
}
log.Println("Caching maxgeneration", maxGeneration, "for family", family,
"while processing instance type", instanceType, "of generation", generation)
(*itmgc)[family] = maxGeneration
delta := maxGeneration - generation
debug.Println("Calculated generation delta for instance type", instanceType, "of generation", generation, "to be", delta)
return delta
}

// for c5ad.2xlarge returns the tuple ("c", 5)
// for inf1.6xlarge returns the tuple ("inf", 1)
// for g5g.4xlarge returns the tuple ("g", 5)
func calculateInstanceTypeGeneration(InstanceType string, itfic *InstanceTypeFamilyInfoCache) (string, int64) {
if it := (*itfic)[InstanceType]; it != nil {
return it.family, it.generation
}

re := regexp.MustCompile(`^(\w+)(\d+)(\w+)?\.\w+$`)
match := re.FindStringSubmatch(InstanceType)
if len(match) == 0 {
(*itfic)[InstanceType] = &InstanceTypeFamilyInfo{family: InstanceType, generation: 1}
return InstanceType, 1
}
family := match[1]
generation, _ := strconv.ParseInt(match[2], 10, 64)

(*itfic)[InstanceType] = &InstanceTypeFamilyInfo{family: family, generation: generation}
return family, generation
}
2 changes: 1 addition & 1 deletion core/instance_queries_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -894,7 +894,7 @@ func Test_getCompatibleSpotInstanceTypesListSortedAscendingByPrice(t *testing.T)
i.asg = tt.asg
allowedList := tt.allowedList
disallowedList := tt.disallowedList
retValue, err := i.getCompatibleSpotInstanceTypesListSortedAscendingByPrice(allowedList, disallowedList)
retValue, err := i.getCompatibleSpotInstanceTypesList("", allowedList, disallowedList)
var retInstTypes []string
for _, retval := range retValue {
retInstTypes = append(retInstTypes, *retval)
Expand Down
14 changes: 14 additions & 0 deletions core/region.go
Original file line number Diff line number Diff line change
Expand Up @@ -55,6 +55,16 @@ type prices struct {
// The key in this map is the availability zone
type spotPriceMap map[string]float64

type InstanceTypeFamilyInfo struct {
family string
generation int64
generationDelta int64
}
type InstanceTypeFamilyInfoCache map[string]*InstanceTypeFamilyInfo

// Stores the maximum generation for each instance type
type InstanceTypeMaxGenerationCache map[string]int64

func (r *region) enabled() bool {

var enabledRegions []string
Expand Down Expand Up @@ -224,6 +234,9 @@ func (r *region) determineInstanceTypeInformation(cfg *Config) {

var info instanceTypeInformation

itfic := make(InstanceTypeFamilyInfoCache)
itmgc := make(InstanceTypeMaxGenerationCache)

for _, it := range *cfg.InstanceData {

var price prices
Expand All @@ -250,6 +263,7 @@ func (r *region) determineInstanceTypeInformation(cfg *Config) {
virtualizationTypes: it.LinuxVirtualizationTypes,
hasEBSOptimization: it.EBSOptimized,
EBSThroughput: it.EBSThroughput,
generationDelta: calculateGenerationDelta(cfg.InstanceData, it.InstanceType, &itfic, &itmgc),
}

if it.Storage != nil {
Expand Down

0 comments on commit 68c860e

Please sign in to comment.