Skip to content
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

Implement spreading allocations based on a target node attribute #4527

Merged
merged 17 commits into from
Aug 1, 2018

Conversation

preetapan
Copy link
Contributor

@preetapan preetapan commented Jul 23, 2018

This PR implements support for spreading allocations across a given target node attribute (datacenter, rack etc) in the scheduler.

Spread can be configured at the task group level, or inherited from the job for all task groups. Spread can be combined with affinities, and given weights.

Builds on top of #4513 and #4512, note to reviewers - its easier if you review this after those two PRs have been merged (should happen sometime this week).

Open questions:

  • Allowing empty spread targets - should that infer available values for the attribute (eg, dc) at schedule time and then assume even split across all?
  • Interpretation of the "percent" field in each spread target, currently its normalized by dividing by the sum across all targets. This means if there is a single target with a spread percentage of 50, it really means it gets 100% of all allocations. We can make the tradeoff of requiring explicit configuration (ie enforcing that the percentages must add up to 100%) instead of doing normalization, to avoid this issue

@@ -2185,6 +2189,13 @@ func (j *Job) Validate() error {
}
}

for idx, spread := range j.Spreads {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Should error if specifying it on system job?

@@ -5384,6 +5407,82 @@ func (a *Affinity) Validate() error {
return mErr.ErrorOrNil()
}

type Spread struct {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Comments

str string
}

type SpreadTarget struct {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Don't intermix struct definitions and methods. Move SpreadTarget and its methods either below or above Spread

if s.str != "" {
return s.str
}
s.str = fmt.Sprintf("%s %v", s.Value, s.Percent)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

%q %v%%?


type SpreadTarget struct {
Value string
Percent uint32
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Validate percent is 0-100? Should we also validate that the sum of these isn't greater than 100?

@@ -110,7 +124,7 @@ func (p *propertySet) setConstraint(constraint *structs.Constraint, taskGroup st

// populateExisting is a helper shared when setting the constraint to populate
// the existing values.
func (p *propertySet) populateExisting(constraint *structs.Constraint) {
func (p *propertySet) populateExisting(targetAttribute string) {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why does this take the targetAttribute? Just read off of p.targetAttribute

job *structs.Job
tg *structs.TaskGroup
jobSpreads []*structs.Spread
tgSpreadInfo map[string]spreadAttributeMap
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can you put some comments on these non-obvious fields and on the types

}
}

// Check if there is a distinct property
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Wrong comment

combinedSpreads = append(combinedSpreads, tg.Spreads...)
combinedSpreads = append(combinedSpreads, iter.jobSpreads...)
for _, spread := range combinedSpreads {
sumWeight := uint32(0)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

sumPercents?

nValue, errorMsg, usedCount := pset.UsedCount(option.Node, tgName)
// Skip if there was errors in resolving this attribute to compute used counts
if errorMsg != "" {
continue
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Log?

desiredCount, ok := spreadDetails.desiredCounts[nValue]
if !ok {
// Warn about missing ratio
iter.ctx.Logger().Printf("[WARN] sched: missing desired distribution percentage for attribute value %v in spread stanza for job %v", nValue, iter.job.ID)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I wouldn't log this since it will be noisy if the user doesn't enumerate every value

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Also talked about this offline, but I don't think this behavior is right. Refer to what we talked about

@@ -3336,6 +3347,10 @@ type TaskGroup struct {
// Affinities can be specified at the task group level to express
// scheduling preferences.
Affinities []*Affinity

// Spread can be specified at the task group level to express spreading
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Will it be possible to specify on job level and have the same behavior as update{} where it cascade / inherit down from job -> spread to job -> group -> spread?

Similar question for affinity{}

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@jippi yes this and affinities both work similar to the update stanza and cascade down.

mErr.Errors = append(mErr.Errors, outer)
if j.Type == JobTypeSystem {
if j.Spreads != nil {
mErr.Errors = append(mErr.Errors, fmt.Errorf("System jobs may not have a s stanza"))
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

a s -> a spread

Weight int
// Attribute is the node attribute used as the spread criteria
Attribute string
// Weight is the relative weight of this spread, useful when there are multiple
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Blank line between fields when you add comments

sumPercent += target.Percent
}
if sumPercent > 100 {
mErr.Errors = append(mErr.Errors, errors.New("Sum of spread target percentages must not be greater than 100"))
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

should we make it 100%?

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Sum of spread target percentages must not be greater than 100%; got %d%%

// for each attribute value
type SpreadTarget struct {
// Value is a single attribute value, like "dc1"
Value string
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Same comment here about spacing

}
return mErr.ErrorOrNil()
}

// SpreadTarget is used to specify desired percentages
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

80 width lines? Why is this so truncated

desiredCount, ok = spreadDetails.desiredCounts[ImplicitTarget]
if !ok {
// The desired count for this attribute is zero if it gets here
// don't boost the score
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Should the score be negative here? If I have specified targets that add up to 100% and this attribute isn't there, the operator is saying it shouldn't be selected?

}
if float64(usedCount) < desiredCount {
// Calculate the relative weight of this specific spread attribute
spreadWeight := float64(spreadDetails.weight) / float64(iter.sumSpreadWeights)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Add whitespace to improve readability

continue
}
}
if float64(usedCount) < desiredCount {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why is this only boosting the score? Shouldn't we add a penalty when you are > than desired count?

// Get the nodes property value
nValue, ok := getProperty(option, pset.targetAttribute)
currentAttributeCount := uint64(0)
if ok {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If !ok shouldn't we return a negative score?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

good catch, will fix.
i conflated !ok with the case where that specific value is not present in the combinedUse map.

}
if currentAttributeCount < minCount {
// Small positive boost for attributes with min count
return evenSpreadBoost
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This should be a function of the discrepancy

// don't boost the score
continue
// so use the maximum possible penalty for this node
totalSpreadScore += -1.0
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

-= 1.0

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

You are falling through to the rest of the code at 140-149 which is incorrect

if currentAttributeCount < minCount {
// Small positive boost for attributes with min count
return evenSpreadBoost
// positive boost for attributes with min count
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The first two cases can be simplified to if currentAttributeCount != minCount {

return evenSpreadBoost
// Maximum possible boost when there is another attribute with
// more allocations
return 1.0
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why isn't this delta based from the max? You could imagine attributes with the following alloc counts:

{a1: 2, a2: 3, a3: 4}
min = 2, max = 4.

a1 should have. a score higher than a2?

Copy link
Contributor

@dadgar dadgar left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Small changes requested but LGTM

@@ -171,6 +185,9 @@ func evenSpreadScoreBoost(pset *propertySet, option *structs.Node) float64 {
currentAttributeCount := uint64(0)
if ok {
currentAttributeCount = combinedUseMap[nValue]
} else {
// If the attribute isn't set on the node, it should get the maximum possible penalty
return -1.0
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nValue, ok := getProperty(option, pset.targetAttribute)
if !ok {
  return -1
}
currentAttributeCount := uint64(combinedUseMap[nValue]) // Defaults to 0 anyways

} else if currentAttributeCount > minCount {
// Negative boost if attribute count is greater than minimum
if currentAttributeCount != minCount {
// Boost based on delta between current and min
return deltaBoost
} else {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

if currentAttribute != minCount {
 return deltaBoost
} else if minCount == maxCount {
  return -1.0
}

// Penalty based on delta from max value
delta := int(maxCount - minCount)
deltaBoost = float64(delta) / float64(minCount)
return deltaBoost

@@ -2185,6 +2189,19 @@ func (j *Job) Validate() error {
}
}

if j.Type == JobTypeSystem {
if j.Spreads != nil {
mErr.Errors = append(mErr.Errors, fmt.Errorf("System jobs may not have a s stanza"))
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

have a s stanza?

sumPercent += target.Percent
}
if sumPercent > 100 {
mErr.Errors = append(mErr.Errors, errors.New("Sum of spread target percentages must not be greater than 100"))
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Sum of spread target percentages must not be greater than 100%; got %d%%

@@ -602,6 +602,89 @@ func TestServiceSched_JobRegister_DistinctProperty_TaskGroup_Incr(t *testing.T)
h.AssertEvalStatus(t, structs.EvalStatusComplete)
}

// Test job registration with spread configured
func TestServiceSched_Spread(t *testing.T) {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Add one for even spread?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

done

&structs.Spread{
Attribute: "${node.datacenter}",
Weight: 100,
SpreadTarget: []*structs.SpreadTarget{
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can we maybe make this test a subtest that takes the percentages and expectations and runs through all values asserting the right outcome? [(100, 0), (90,10), (80,20), ...]

@@ -5479,7 +5479,7 @@ func (s *Spread) Validate() error {
sumPercent += target.Percent
}
if sumPercent > 100 {
mErr.Errors = append(mErr.Errors, errors.New("Sum of spread target percentages must not be greater than 100"))
mErr.Errors = append(mErr.Errors, errors.New(fmt.Sprintf("Sum of spread target percentages must not be greater than 100%%; got %d%%", sumPercent)))
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

maybe just fmt.Errorf

step := uint32(10)

for i := 0; i < 10; i++ {
name := fmt.Sprintf("%d%% in dc1", start)
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

this test cycles through combinations of spread across both data centers, going from (100, 0) to (0, 100) increments of 10% at a time.

@preetapan preetapan merged commit a38546d into f-affinities-spread Aug 1, 2018
@preetapan preetapan mentioned this pull request Sep 4, 2018
@github-actions
Copy link

I'm going to lock this pull request because it has been closed for 120 days ⏳. This helps our maintainers find and focus on the active contributions.
If you have found a problem that seems related to this change, please open a new issue and complete the issue template so we can capture all the details necessary to investigate further.

@github-actions github-actions bot locked as resolved and limited conversation to collaborators Feb 28, 2023
Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

None yet

3 participants