Skip to content

Commit 531c0da

Browse files
craig[bot]sumeerbhola
andcommitted
Merge #111918
111918: mma: more normalization of constraints r=kvoli a=sumeerbhola We now also do some normalization of constraints (in addition to voter constraints). This helps with a better choice of where to add a non-voter if the voter constraints are temporarily unsatisfiable. This was motivated by looking at the config in #106559 (though this is unrelated to the bug there). Informs #103320 Epic: CRDB-25222 Release note: None Co-authored-by: sumeerbhola <[email protected]>
2 parents ba272b0 + 41d517c commit 531c0da

File tree

4 files changed

+270
-36
lines changed

4 files changed

+270
-36
lines changed

pkg/kv/kvserver/allocator/mma/constraint.go

Lines changed: 117 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -127,8 +127,6 @@ func (ic internedConstraint) less(b internedConstraint) bool {
127127
// constraints are in increasing order using internedConstraint.less.
128128
type constraintsConj []internedConstraint
129129

130-
type conjunctionRelationship int
131-
132130
func (nconf *normalizedSpanConfig) uninternedConfig() roachpb.SpanConfig {
133131
var conf roachpb.SpanConfig
134132
conf.NumReplicas = nconf.numReplicas
@@ -158,6 +156,8 @@ func (nconf *normalizedSpanConfig) uninternedConfig() roachpb.SpanConfig {
158156
return conf
159157
}
160158

159+
type conjunctionRelationship int
160+
161161
// Relationship between conjunctions used for structural normalization. This
162162
// relationship is solely defined based on the conjunctions, and not based on
163163
// what stores actually match. It simply assumes that if two conjuncts are not
@@ -211,13 +211,13 @@ func (cc constraintsConj) relationship(b constraintsConj) conjunctionRelationshi
211211
if extraInB > 0 && extraInCC == 0 {
212212
return conjStrictSuperset
213213
}
214-
// (extraInCC == 0 || extraInBB > 0) && (extraInB == 0 || extraInCC > 0)
214+
// (extraInCC == 0 || extraInB > 0) && (extraInB == 0 || extraInCC > 0)
215215
// =>
216-
// (extraInCC == 0 && extraInB == 0) || (extraInBB > 0 && extraInCC > 0)
216+
// (extraInCC == 0 && extraInB == 0) || (extraInB > 0 && extraInCC > 0)
217217
if extraInCC == 0 && extraInB == 0 {
218218
return conjEqualSet
219219
}
220-
// (extraInBB > 0 && extraInCC > 0)
220+
// (extraInB > 0 && extraInCC > 0)
221221
if inBoth > 0 {
222222
return conjIntersecting
223223
}
@@ -576,6 +576,118 @@ func doStructuralNormalization(conf *normalizedSpanConfig) (*normalizedSpanConfi
576576
}
577577
}
578578
conf.voterConstraints = vc
579+
580+
// We are done with normalizing voter constraints. We also do some basic
581+
// normalization for constraints: we have seen examples where the
582+
// constraints are under-specified and give freedom in the choice of
583+
// constraintsForAddingNonVoter() that isn't actually there. Note that when
584+
// all the voter constraints are satisfied (which we try to do before
585+
// satisfying non-voter constraints), then the under-specification does not
586+
// hurt the choice made in constraintsForAddingNonVoter(). However, we could
587+
// have situations where an outage of some kind is preventing all the voter
588+
// constraints from being satisfied, and now we need to place a non-voter --
589+
// placing that non-voter correctly will avoid the need to move it later
590+
// when the voter constraint does get satisfied.
591+
//
592+
// For example, see the config from #106559 in testdata/normalize_config.
593+
// The constraints for us-west-1 and us-east-1 are under-specified in
594+
// needing only 1 replica, while voter constraints specify we need 2
595+
// replicas in each. Consider if it were left under-specified, and we had
596+
// only 3 voters, 2 in us-west-1 and 1 in us-east-1 and we were temporarily
597+
// unable to add a voter in us-east-1. Say we lose the non-voter too, and
598+
// need to add one. With the under-specified constraint we could add the
599+
// non-voter anywhere, since we think we are allowed 2 replicas with the
600+
// empty constraint conjunction. This is technically true, but once we have
601+
// the required second voter in us-east-1, we will need to move that
602+
// non-voter to us-central-1, which is wasteful.
603+
if emptyConstraintIndex >= 0 {
604+
// Recompute the relationship since voterConstraints have changed.
605+
emptyVoterConstraintIndex = -1
606+
rels = rels[:0]
607+
for i := range conf.voterConstraints {
608+
if len(conf.voterConstraints[i].constraints) == 0 {
609+
// We don't actually use emptyVoterConstraintIndex later, but it is
610+
// harmless to recompute, and will avoid subtle bugs if we change the
611+
// logic below to start using it.
612+
emptyVoterConstraintIndex = i
613+
}
614+
for j := range conf.constraints {
615+
rels = append(rels, relationshipVoterAndAll{
616+
voterIndex: i,
617+
allIndex: j,
618+
voterAndAllRel: conf.voterConstraints[i].constraints.relationship(
619+
conf.constraints[j].constraints),
620+
})
621+
}
622+
}
623+
// Sort these relationships in the order we want to examine them.
624+
sort.Slice(rels, func(i, j int) bool {
625+
return rels[i].voterAndAllRel < rels[j].voterAndAllRel
626+
})
627+
// Ignore conjIntersecting.
628+
index = 0
629+
for rels[index].voterAndAllRel == conjIntersecting {
630+
index++
631+
}
632+
voterConstraintHasEqualityWithConstraint := make([]bool, len(conf.voterConstraints))
633+
// For conjEqualSet, if we can grab from the emptyConstraintIndex, do so.
634+
for ; index < len(rels) && rels[index].voterAndAllRel <= conjEqualSet; index++ {
635+
rel := rels[index]
636+
voterConstraintHasEqualityWithConstraint[rel.voterIndex] = true
637+
if rel.allIndex == emptyConstraintIndex {
638+
// rel.voterIndex must be emptyVoterConstraintIndex.
639+
continue
640+
}
641+
if conf.constraints[rel.allIndex].numReplicas < conf.voterConstraints[rel.voterIndex].numReplicas {
642+
toAddCount := conf.voterConstraints[rel.voterIndex].numReplicas -
643+
conf.constraints[rel.allIndex].numReplicas
644+
availableCount := conf.constraints[emptyConstraintIndex].numReplicas
645+
if availableCount < toAddCount {
646+
toAddCount = availableCount
647+
}
648+
conf.constraints[emptyConstraintIndex].numReplicas -= toAddCount
649+
conf.constraints[rel.allIndex].numReplicas += toAddCount
650+
}
651+
}
652+
// For conjStrictSubset, if the subset relationship is with
653+
// emptyConstraintIndex, grab from there.
654+
for ; index < len(rels) && rels[index].voterAndAllRel <= conjStrictSubset; index++ {
655+
rel := rels[index]
656+
if rel.allIndex != emptyConstraintIndex {
657+
continue
658+
}
659+
if voterConstraintHasEqualityWithConstraint[rel.voterIndex] {
660+
// Already has a corresponding constraint, that we have considered in
661+
// the previous loop.
662+
continue
663+
}
664+
availableCount := conf.constraints[emptyConstraintIndex].numReplicas
665+
if availableCount > 0 {
666+
toAddCount := conf.voterConstraints[rel.voterIndex].numReplicas
667+
if toAddCount > availableCount {
668+
toAddCount = availableCount
669+
}
670+
conf.constraints[emptyConstraintIndex].numReplicas -= toAddCount
671+
conf.constraints = append(conf.constraints, internedConstraintsConjunction{
672+
numReplicas: toAddCount,
673+
constraints: conf.voterConstraints[rel.voterIndex].constraints,
674+
})
675+
}
676+
}
677+
// We may have appended to conf.constraints in the previous loop. We like
678+
// to keep the empty constraint as the last one, so do a swap if needed.
679+
n := len(conf.constraints) - 1
680+
if n != emptyConstraintIndex {
681+
conf.constraints[n], conf.constraints[emptyConstraintIndex] =
682+
conf.constraints[emptyConstraintIndex], conf.constraints[n]
683+
}
684+
// If the empty constraint does not have any replicas due to the
685+
// normalization, discard it.
686+
if conf.constraints[n].numReplicas == 0 {
687+
conf.constraints = conf.constraints[:n]
688+
}
689+
}
690+
579691
return conf, err
580692
}
581693

pkg/kv/kvserver/allocator/mma/testdata/normalize_config

Lines changed: 74 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,8 @@ input:
2020
output:
2121
num-replicas=5 num-voters=3
2222
constraints:
23-
:5
23+
+region=a:3
24+
:2
2425
voter-constraints:
2526
+region=a:3
2627

@@ -114,7 +115,9 @@ output:
114115
constraints:
115116
+region=b:1
116117
+region=c:1
117-
:3
118+
+region=a,-zone=a1:1
119+
+region=a,+zone=a1:1
120+
:1
118121
voter-constraints:
119122
+region=a,+zone=a1:1
120123
+region=a,-zone=a1:1
@@ -144,7 +147,8 @@ output:
144147
+region=b:1
145148
+region=c:1
146149
+region=d:1
147-
:2
150+
+region=a,-zone=a1:1
151+
+region=a,+zone=a1:1
148152
voter-constraints:
149153
+region=a,+zone=a1:1
150154
+region=a,-zone=a1:1
@@ -221,7 +225,8 @@ output:
221225
+region=c:1
222226
+region=d:1
223227
+region=e:1
224-
:4
228+
+region=f:2
229+
:2
225230
voter-constraints:
226231
+region=f:2
227232
+region=e:1
@@ -263,7 +268,8 @@ output:
263268
+region=c:1
264269
+region=d:1
265270
+region=e:1
266-
:4
271+
+region=f:2
272+
:2
267273
voter-constraints:
268274
+region=f:2
269275
+region=c:1
@@ -293,7 +299,9 @@ output:
293299
constraints:
294300
+region=b,+zone=b2:1
295301
+region=c,+zone=c2:1
296-
:3
302+
+region=a,-zone=a1:1
303+
+region=b,+zone=b1:1
304+
:1
297305
voter-constraints:
298306
+region=b,+zone=b1:1
299307
+region=a,-zone=a1:1
@@ -356,7 +364,7 @@ output:
356364
+region=b,+zone=b2:1
357365
+region=c,+zone=c2:1
358366
+region=d,+zone=d2:1
359-
:1
367+
+region=e:1
360368
voter-constraints:
361369
+region=e:1
362370
+region=c,+zone=c2:1
@@ -387,3 +395,62 @@ output:
387395
+region=a,+zone=a1:2
388396
+region=a,+zone=a2:2
389397
+region=a,+zone=a3:1
398+
399+
# Config from #106559.
400+
normalize num-replicas=6 num-voters=5
401+
constraint num-replicas=1 +region=eu-west-1
402+
constraint num-replicas=1 +region=us-central-1
403+
constraint num-replicas=1 +region=us-east-1
404+
constraint num-replicas=1 +region=us-west-1
405+
voter-constraint num-replicas=2 +region=us-west-1
406+
voter-constraint num-replicas=2 +region=us-east-1
407+
----
408+
input:
409+
num-replicas=6 num-voters=5
410+
constraints:
411+
+region=eu-west-1:1
412+
+region=us-central-1:1
413+
+region=us-east-1:1
414+
+region=us-west-1:1
415+
voter-constraints:
416+
+region=us-west-1:2
417+
+region=us-east-1:2
418+
output:
419+
num-replicas=6 num-voters=5
420+
constraints:
421+
+region=eu-west-1:1
422+
+region=us-central-1:1
423+
+region=us-east-1:2
424+
+region=us-west-1:2
425+
voter-constraints:
426+
+region=us-west-1:2
427+
+region=us-east-1:2
428+
+region=eu-west-1:1
429+
430+
# Config from #122292. Note that the normalization over-specifies by adding a
431+
# voter constraint for +region=a:1. Based on the original configuration,
432+
# either +region=a:1 or +region=b:1 could be added, but we cannot express a
433+
# disjunction.
434+
normalize num-replicas=4 num-voters=3
435+
constraint num-replicas=1 +region=a
436+
constraint num-replicas=1 +region=b
437+
constraint num-replicas=1 +region=c
438+
voter-constraint num-replicas=2 +region=c
439+
----
440+
input:
441+
num-replicas=4 num-voters=3
442+
constraints:
443+
+region=a:1
444+
+region=b:1
445+
+region=c:1
446+
voter-constraints:
447+
+region=c:2
448+
output:
449+
num-replicas=4 num-voters=3
450+
constraints:
451+
+region=a:1
452+
+region=b:1
453+
+region=c:2
454+
voter-constraints:
455+
+region=c:2
456+
+region=a:1

0 commit comments

Comments
 (0)